diff --git a/.env.example b/.env.example index 3fcb0523a..8ee3d2001 100644 --- a/.env.example +++ b/.env.example @@ -26,7 +26,8 @@ SITE_URL=http://localhost:8080 # Mail/SMTP SMTP_HOST= SMTP_PORT= -SMTP_NAME= +SMTP_FROM_ADDRESS= +SMTP_FROM_NAME= SMTP_USERNAME= SMTP_PASSWORD= @@ -74,9 +75,48 @@ CAPTCHA_SECRET= NEXT_PUBLIC_CAPTCHA_SITE_KEY= +OTEL_TELEMETRY_COLLECTION_ENABLED=false +OTEL_EXPORT_TYPE=prometheus +OTEL_EXPORT_OTLP_ENDPOINT= +OTEL_OTLP_PUSH_INTERVAL= + +OTEL_COLLECTOR_BASIC_AUTH_USERNAME= +OTEL_COLLECTOR_BASIC_AUTH_PASSWORD= + PLAIN_API_KEY= PLAIN_WISH_LABEL_IDS= SSL_CLIENT_CERTIFICATE_HEADER_KEY= ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT=true + +# App Connections + +# aws assume-role connection +INF_APP_CONNECTION_AWS_ACCESS_KEY_ID= +INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY= + +# github oauth connection +INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID= +INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET= + +#github app connection +INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID= +INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET= +INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY= +INF_APP_CONNECTION_GITHUB_APP_SLUG= +INF_APP_CONNECTION_GITHUB_APP_ID= + +#gcp app connection +INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL= + +# azure app connection +INF_APP_CONNECTION_AZURE_CLIENT_ID= +INF_APP_CONNECTION_AZURE_CLIENT_SECRET= + +# datadog +SHOULD_USE_DATADOG_TRACER= +DATADOG_PROFILING_ENABLED= +DATADOG_ENV= +DATADOG_SERVICE= +DATADOG_HOSTNAME= diff --git a/.envrc b/.envrc new file mode 100644 index 000000000..ae4b4fb80 --- /dev/null +++ b/.envrc @@ -0,0 +1,3 @@ +# Learn more at https://direnv.net +# We instruct direnv to use our Nix flake for a consistent development environment. +use flake diff --git a/.github/workflows/check-api-for-breaking-changes.yml b/.github/workflows/check-api-for-breaking-changes.yml index c4cb6e451..3f85326f4 100644 --- a/.github/workflows/check-api-for-breaking-changes.yml +++ b/.github/workflows/check-api-for-breaking-changes.yml @@ -32,10 +32,23 @@ jobs: run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis - name: Start the server run: | - echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env - echo "SECRET_SCANNING_PRIVATE_KEY=some-random" >> .env - echo "SECRET_SCANNING_WEBHOOK_SECRET=some-random" >> .env - docker run --name infisical-api -d -p 4000:4000 -e DB_CONNECTION_URI=$DB_CONNECTION_URI -e REDIS_URL=$REDIS_URL -e JWT_AUTH_SECRET=$JWT_AUTH_SECRET -e ENCRYPTION_KEY=$ENCRYPTION_KEY --env-file .env --entrypoint '/bin/sh' infisical-api -c "npm run migration:latest && ls && node dist/main.mjs" + echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env + echo "SECRET_SCANNING_PRIVATE_KEY=some-random" >> .env + echo "SECRET_SCANNING_WEBHOOK_SECRET=some-random" >> .env + + echo "Examining built image:" + docker image inspect infisical-api | grep -A 5 "Entrypoint" + + docker run --name infisical-api -d -p 4000:4000 \ + -e DB_CONNECTION_URI=$DB_CONNECTION_URI \ + -e REDIS_URL=$REDIS_URL \ + -e JWT_AUTH_SECRET=$JWT_AUTH_SECRET \ + -e ENCRYPTION_KEY=$ENCRYPTION_KEY \ + --env-file .env \ + infisical-api + + echo "Container status right after creation:" + docker ps -a | grep infisical-api env: REDIS_URL: redis://172.17.0.1:6379 DB_CONNECTION_URI: postgres://infisical:infisical@172.17.0.1:5432/infisical?sslmode=disable @@ -43,35 +56,48 @@ jobs: ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218 - uses: actions/setup-go@v5 with: - go-version: '1.21.5' + go-version: "1.21.5" - name: Wait for container to be stable and check logs run: | SECONDS=0 HEALTHY=0 while [ $SECONDS -lt 60 ]; do - if docker ps | grep infisical-api | grep -q healthy; then - echo "Container is healthy." - HEALTHY=1 + # Check if container is running + if docker ps | grep infisical-api; then + # Try to access the API endpoint + if curl -s -f http://localhost:4000/api/docs/json > /dev/null 2>&1; then + echo "API endpoint is responding. Container seems healthy." + HEALTHY=1 + break + fi + else + echo "Container is not running!" + docker ps -a | grep infisical-api break fi + echo "Waiting for container to be healthy... ($SECONDS seconds elapsed)" - - docker logs infisical-api - - sleep 2 - SECONDS=$((SECONDS+2)) + sleep 5 + SECONDS=$((SECONDS+5)) done - + if [ $HEALTHY -ne 1 ]; then echo "Container did not become healthy in time" + echo "Container status:" + docker ps -a | grep infisical-api + echo "Container logs (if any):" + docker logs infisical-api || echo "No logs available" + echo "Container inspection:" + docker inspect infisical-api | grep -A 5 "State" exit 1 fi - name: Install openapi-diff - run: go install github.com/tufin/oasdiff@latest + run: go install github.com/oasdiff/oasdiff@latest - name: Running OpenAPI Spec diff action run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR - name: cleanup + if: always() run: | docker compose -f "docker-compose.dev.yml" down - docker stop infisical-api - docker remove infisical-api + docker stop infisical-api || true + docker rm infisical-api || true \ No newline at end of file diff --git a/.github/workflows/check-fe-ts-and-lint.yml b/.github/workflows/check-fe-ts-and-lint.yml index 17e5a9d74..9b4d363cf 100644 --- a/.github/workflows/check-fe-ts-and-lint.yml +++ b/.github/workflows/check-fe-ts-and-lint.yml @@ -18,18 +18,18 @@ jobs: steps: - name: ☁️ Checkout source uses: actions/checkout@v3 - - name: 🔧 Setup Node 16 + - name: 🔧 Setup Node 20 uses: actions/setup-node@v3 with: - node-version: "16" + node-version: "20" cache: "npm" cache-dependency-path: frontend/package-lock.json - name: 📦 Install dependencies run: npm install working-directory: frontend - name: 🏗️ Run Type check - run: npm run type:check + run: npm run type:check working-directory: frontend - name: 🏗️ Run Link check - run: npm run lint:fix + run: npm run lint:fix working-directory: frontend diff --git a/.github/workflows/deployment-pipeline.yml b/.github/workflows/deployment-pipeline.yml deleted file mode 100644 index 70fe64088..000000000 --- a/.github/workflows/deployment-pipeline.yml +++ /dev/null @@ -1,212 +0,0 @@ -name: Deployment pipeline -on: [workflow_dispatch] - -permissions: - id-token: write - contents: read - -jobs: - infisical-tests: - name: Integration tests - # https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview - uses: ./.github/workflows/run-backend-tests.yml - - infisical-image: - name: Build - runs-on: ubuntu-latest - needs: [infisical-tests] - steps: - - name: ☁️ Checkout source - uses: actions/checkout@v3 - - name: 📦 Install dependencies to test all dependencies - run: npm ci --only-production - working-directory: backend - - name: Save commit hashes for tag - id: commit - uses: pr-mpt/actions-commit-hash@v2 - - name: 🔧 Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - name: 🐋 Login to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up Depot CLI - uses: depot/setup-action@v1 - - name: 🏗️ Build backend and push to docker hub - uses: depot/build-push-action@v1 - with: - project: 64mmf0n610 - token: ${{ secrets.DEPOT_PROJECT_TOKEN }} - push: true - context: . - file: Dockerfile.standalone-infisical - tags: | - infisical/staging_infisical:${{ steps.commit.outputs.short }} - infisical/staging_infisical:latest - platforms: linux/amd64,linux/arm64 - build-args: | - POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} - INFISICAL_PLATFORM_VERSION=${{ steps.commit.outputs.short }} - - gamma-deployment: - name: Deploy to gamma - runs-on: ubuntu-latest - needs: [infisical-image] - environment: - name: Gamma - steps: - - uses: twingate/github-action@v1 - with: - # The Twingate Service Key used to connect Twingate to the proper service - # Learn more about [Twingate Services](https://docs.twingate.com/docs/services) - # - # Required - service-key: ${{ secrets.TWINGATE_SERVICE_KEY }} - - name: Checkout code - uses: actions/checkout@v2 - - name: Setup Node.js environment - uses: actions/setup-node@v2 - with: - node-version: "20" - - name: Change directory to backend and install dependencies - env: - DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }} - run: | - cd backend - npm install - npm run migration:latest - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - audience: sts.amazonaws.com - aws-region: us-east-1 - role-to-assume: arn:aws:iam::905418227878:role/deploy-new-ecs-img - - name: Save commit hashes for tag - id: commit - uses: pr-mpt/actions-commit-hash@v2 - - name: Download task definition - run: | - aws ecs describe-task-definition --task-definition infisical-core-gamma-stage --query taskDefinition > task-definition.json - - name: Render Amazon ECS task definition - id: render-web-container - uses: aws-actions/amazon-ecs-render-task-definition@v1 - with: - task-definition: task-definition.json - container-name: infisical-core - image: infisical/staging_infisical:${{ steps.commit.outputs.short }} - environment-variables: "LOG_LEVEL=info" - - name: Deploy to Amazon ECS service - uses: aws-actions/amazon-ecs-deploy-task-definition@v1 - with: - task-definition: ${{ steps.render-web-container.outputs.task-definition }} - service: infisical-core-gamma-stage - cluster: infisical-gamma-stage - wait-for-service-stability: true - - production-us: - name: US production deploy - runs-on: ubuntu-latest - needs: [gamma-deployment] - environment: - name: Production - steps: - - uses: twingate/github-action@v1 - with: - # The Twingate Service Key used to connect Twingate to the proper service - # Learn more about [Twingate Services](https://docs.twingate.com/docs/services) - # - # Required - service-key: ${{ secrets.TWINGATE_SERVICE_KEY }} - - name: Checkout code - uses: actions/checkout@v2 - - name: Setup Node.js environment - uses: actions/setup-node@v2 - with: - node-version: "20" - - name: Change directory to backend and install dependencies - env: - DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }} - AUDIT_LOGS_DB_CONNECTION_URI: ${{ secrets.AUDIT_LOGS_DB_CONNECTION_URI }} - run: | - cd backend - npm install - npm run migration:latest - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - audience: sts.amazonaws.com - aws-region: us-east-1 - role-to-assume: arn:aws:iam::381492033652:role/gha-make-prod-deployment - - name: Save commit hashes for tag - id: commit - uses: pr-mpt/actions-commit-hash@v2 - - name: Download task definition - run: | - aws ecs describe-task-definition --task-definition infisical-core-platform --query taskDefinition > task-definition.json - - name: Render Amazon ECS task definition - id: render-web-container - uses: aws-actions/amazon-ecs-render-task-definition@v1 - with: - task-definition: task-definition.json - container-name: infisical-core-platform - image: infisical/staging_infisical:${{ steps.commit.outputs.short }} - environment-variables: "LOG_LEVEL=info" - - name: Deploy to Amazon ECS service - uses: aws-actions/amazon-ecs-deploy-task-definition@v1 - with: - task-definition: ${{ steps.render-web-container.outputs.task-definition }} - service: infisical-core-platform - cluster: infisical-core-platform - wait-for-service-stability: true - - production-eu: - name: EU production deploy - runs-on: ubuntu-latest - needs: [production-us] - environment: - name: production-eu - steps: - - uses: twingate/github-action@v1 - with: - service-key: ${{ secrets.TWINGATE_SERVICE_KEY }} - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - audience: sts.amazonaws.com - aws-region: eu-central-1 - role-to-assume: arn:aws:iam::345594589636:role/gha-make-prod-deployment - - name: Checkout code - uses: actions/checkout@v2 - - name: Setup Node.js environment - uses: actions/setup-node@v2 - with: - node-version: "20" - - name: Change directory to backend and install dependencies - env: - DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }} - run: | - cd backend - npm install - npm run migration:latest - - name: Save commit hashes for tag - id: commit - uses: pr-mpt/actions-commit-hash@v2 - - name: Download task definition - run: | - aws ecs describe-task-definition --task-definition infisical-core-platform --query taskDefinition > task-definition.json - - name: Render Amazon ECS task definition - id: render-web-container - uses: aws-actions/amazon-ecs-render-task-definition@v1 - with: - task-definition: task-definition.json - container-name: infisical-core-platform - image: infisical/staging_infisical:${{ steps.commit.outputs.short }} - environment-variables: "LOG_LEVEL=info" - - name: Deploy to Amazon ECS service - uses: aws-actions/amazon-ecs-deploy-task-definition@v1 - with: - task-definition: ${{ steps.render-web-container.outputs.task-definition }} - service: infisical-core-platform - cluster: infisical-core-platform - wait-for-service-stability: true diff --git a/.github/workflows/helm-release-infisical-core.yml b/.github/workflows/helm-release-infisical-core.yml new file mode 100644 index 000000000..0588d5d0d --- /dev/null +++ b/.github/workflows/helm-release-infisical-core.yml @@ -0,0 +1,22 @@ +name: Release Infisical Core Helm chart + +on: [workflow_dispatch] + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install Helm + uses: azure/setup-helm@v3 + with: + version: v3.10.0 + - name: Install python + uses: actions/setup-python@v4 + - name: Install Cloudsmith CLI + run: pip install --upgrade cloudsmith-cli + - name: Build and push helm package to Cloudsmith + run: cd helm-charts && sh upload-infisical-core-helm-cloudsmith.sh + env: + CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }} \ No newline at end of file diff --git a/.github/workflows/helm_chart_release.yml b/.github/workflows/helm_chart_release.yml deleted file mode 100644 index 8f47da69d..000000000 --- a/.github/workflows/helm_chart_release.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Release Helm Charts - -on: [workflow_dispatch] - -jobs: - release: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Install Helm - uses: azure/setup-helm@v3 - with: - version: v3.10.0 - - name: Install python - uses: actions/setup-python@v4 - - name: Install Cloudsmith CLI - run: pip install --upgrade cloudsmith-cli - - name: Build and push helm package to Cloudsmith - run: cd helm-charts && sh upload-to-cloudsmith.sh - env: - CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }} \ No newline at end of file diff --git a/.github/workflows/release-k8-operator-helm.yml b/.github/workflows/release-k8-operator-helm.yml new file mode 100644 index 000000000..f3731fb46 --- /dev/null +++ b/.github/workflows/release-k8-operator-helm.yml @@ -0,0 +1,27 @@ +name: Release K8 Operator Helm Chart +on: + workflow_dispatch: + +jobs: + release-helm: + name: Release Helm Chart + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install Helm + uses: azure/setup-helm@v3 + with: + version: v3.10.0 + + - name: Install python + uses: actions/setup-python@v4 + + - name: Install Cloudsmith CLI + run: pip install --upgrade cloudsmith-cli + + - name: Build and push helm package to CloudSmith + run: cd helm-charts && sh upload-k8s-operator-cloudsmith.sh + env: + CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }} diff --git a/.github/workflows/release-standalone-docker-img-postgres-offical.yml b/.github/workflows/release-standalone-docker-img-postgres-offical.yml index f08e882aa..7a73288cb 100644 --- a/.github/workflows/release-standalone-docker-img-postgres-offical.yml +++ b/.github/workflows/release-standalone-docker-img-postgres-offical.yml @@ -1,62 +1,115 @@ name: Release standalone docker image on: - push: - tags: - - "infisical/v*.*.*-postgres" + push: + tags: + - "infisical/v*.*.*-postgres" jobs: - infisical-tests: - name: Run tests before deployment - # https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview - uses: ./.github/workflows/run-backend-tests.yml - infisical-standalone: - name: Build infisical standalone image postgres - runs-on: ubuntu-latest - needs: [infisical-tests] - steps: - - name: Extract version from tag - id: extract_version - run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}" - - name: ☁️ Checkout source - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: 📦 Install dependencies to test all dependencies - run: npm ci --only-production - working-directory: backend - - name: version output - run: | - echo "Output Value: ${{ steps.version.outputs.major }}" - echo "Output Value: ${{ steps.version.outputs.minor }}" - echo "Output Value: ${{ steps.version.outputs.patch }}" - echo "Output Value: ${{ steps.version.outputs.version }}" - echo "Output Value: ${{ steps.version.outputs.version_type }}" - echo "Output Value: ${{ steps.version.outputs.increment }}" - - name: Save commit hashes for tag - id: commit - uses: pr-mpt/actions-commit-hash@v2 - - name: 🔧 Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - name: 🐋 Login to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up Depot CLI - uses: depot/setup-action@v1 - - name: 📦 Build backend and export to Docker - uses: depot/build-push-action@v1 - with: - project: 64mmf0n610 - token: ${{ secrets.DEPOT_PROJECT_TOKEN }} - push: true - context: . - tags: | - infisical/infisical:latest-postgres - infisical/infisical:${{ steps.commit.outputs.short }} - infisical/infisical:${{ steps.extract_version.outputs.version }} - platforms: linux/amd64,linux/arm64 - file: Dockerfile.standalone-infisical - build-args: | - POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} - INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }} + infisical-tests: + name: Run tests before deployment + # https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview + uses: ./.github/workflows/run-backend-tests.yml + + infisical-standalone: + name: Build infisical standalone image postgres + runs-on: ubuntu-latest + needs: [infisical-tests] + steps: + - name: Extract version from tag + id: extract_version + run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}" + - name: ☁️ Checkout source + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: 📦 Install dependencies to test all dependencies + run: npm ci --only-production + working-directory: backend + - name: version output + run: | + echo "Output Value: ${{ steps.version.outputs.major }}" + echo "Output Value: ${{ steps.version.outputs.minor }}" + echo "Output Value: ${{ steps.version.outputs.patch }}" + echo "Output Value: ${{ steps.version.outputs.version }}" + echo "Output Value: ${{ steps.version.outputs.version_type }}" + echo "Output Value: ${{ steps.version.outputs.increment }}" + - name: Save commit hashes for tag + id: commit + uses: pr-mpt/actions-commit-hash@v2 + - name: 🔧 Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: 🐋 Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set up Depot CLI + uses: depot/setup-action@v1 + - name: 📦 Build backend and export to Docker + uses: depot/build-push-action@v1 + with: + project: 64mmf0n610 + token: ${{ secrets.DEPOT_PROJECT_TOKEN }} + push: true + context: . + tags: | + infisical/infisical:latest-postgres + infisical/infisical:${{ steps.commit.outputs.short }} + infisical/infisical:${{ steps.extract_version.outputs.version }} + platforms: linux/amd64,linux/arm64 + file: Dockerfile.standalone-infisical + build-args: | + POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} + INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }} + + infisical-fips-standalone: + name: Build infisical standalone image postgres + runs-on: ubuntu-latest + needs: [infisical-tests] + steps: + - name: Extract version from tag + id: extract_version + run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}" + - name: ☁️ Checkout source + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: 📦 Install dependencies to test all dependencies + run: npm ci --only-production + working-directory: backend + - name: version output + run: | + echo "Output Value: ${{ steps.version.outputs.major }}" + echo "Output Value: ${{ steps.version.outputs.minor }}" + echo "Output Value: ${{ steps.version.outputs.patch }}" + echo "Output Value: ${{ steps.version.outputs.version }}" + echo "Output Value: ${{ steps.version.outputs.version_type }}" + echo "Output Value: ${{ steps.version.outputs.increment }}" + - name: Save commit hashes for tag + id: commit + uses: pr-mpt/actions-commit-hash@v2 + - name: 🔧 Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: 🐋 Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set up Depot CLI + uses: depot/setup-action@v1 + - name: 📦 Build backend and export to Docker + uses: depot/build-push-action@v1 + with: + project: 64mmf0n610 + token: ${{ secrets.DEPOT_PROJECT_TOKEN }} + push: true + context: . + tags: | + infisical/infisical-fips:latest-postgres + infisical/infisical-fips:${{ steps.commit.outputs.short }} + infisical/infisical-fips:${{ steps.extract_version.outputs.version }} + platforms: linux/amd64,linux/arm64 + file: Dockerfile.fips.standalone-infisical + build-args: | + POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} + INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }} diff --git a/.github/workflows/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml index 02c349237..3fe0fbe21 100644 --- a/.github/workflows/release_build_infisical_cli.yml +++ b/.github/workflows/release_build_infisical_cli.yml @@ -1,75 +1,153 @@ name: Build and release CLI on: - workflow_dispatch: + workflow_dispatch: - push: - # run only against tags - tags: - - "infisical-cli/v*.*.*" + push: + # run only against tags + tags: + - "infisical-cli/v*.*.*" permissions: - contents: write - # packages: write - # issues: write -jobs: - cli-integration-tests: - name: Run tests before deployment - uses: ./.github/workflows/run-cli-tests.yml - secrets: - CLI_TESTS_UA_CLIENT_ID: ${{ secrets.CLI_TESTS_UA_CLIENT_ID }} - CLI_TESTS_UA_CLIENT_SECRET: ${{ secrets.CLI_TESTS_UA_CLIENT_SECRET }} - CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }} - CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }} - CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }} - CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }} - CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }} - CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }} + contents: write - goreleaser: - runs-on: ubuntu-20.04 - needs: [cli-integration-tests] - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: 🐋 Login to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: 🔧 Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - run: git fetch --force --tags - - run: echo "Ref name ${{github.ref_name}}" - - uses: actions/setup-go@v3 - with: - go-version: ">=1.19.3" - cache: true - cache-dependency-path: cli/go.sum - - name: libssl1.1 => libssl1.0-dev for OSXCross - run: | - echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list - sudo apt update && apt-cache policy libssl1.0-dev - sudo apt-get install libssl1.0-dev - - name: OSXCross for CGO Support - run: | - mkdir ../../osxcross - git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target - - uses: goreleaser/goreleaser-action@v4 - with: - distribution: goreleaser-pro - version: v1.26.2-pro - args: release --clean - env: - GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }} - POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }} - FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }} - AUR_KEY: ${{ secrets.AUR_KEY }} - GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} - - uses: actions/setup-python@v4 - - run: pip install --upgrade cloudsmith-cli - - name: Publish to CloudSmith - run: sh cli/upload_to_cloudsmith.sh - env: - CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }} +jobs: + cli-integration-tests: + name: Run tests before deployment + uses: ./.github/workflows/run-cli-tests.yml + secrets: + CLI_TESTS_UA_CLIENT_ID: ${{ secrets.CLI_TESTS_UA_CLIENT_ID }} + CLI_TESTS_UA_CLIENT_SECRET: ${{ secrets.CLI_TESTS_UA_CLIENT_SECRET }} + CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }} + CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }} + CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }} + CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }} + CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }} + CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }} + + npm-release: + runs-on: ubuntu-latest + env: + working-directory: ./npm + needs: + - cli-integration-tests + - goreleaser + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Extract version + run: | + VERSION=$(echo ${{ github.ref_name }} | sed 's/infisical-cli\/v//') + echo "Version extracted: $VERSION" + echo "CLI_VERSION=$VERSION" >> $GITHUB_ENV + + - name: Print version + run: echo ${{ env.CLI_VERSION }} + + - name: Setup Node + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 + with: + node-version: 20 + cache: "npm" + cache-dependency-path: ./npm/package-lock.json + - name: Install dependencies + working-directory: ${{ env.working-directory }} + run: npm install --ignore-scripts + + - name: Set NPM version + working-directory: ${{ env.working-directory }} + run: npm version ${{ env.CLI_VERSION }} --allow-same-version --no-git-tag-version + + - name: Setup NPM + working-directory: ${{ env.working-directory }} + run: | + echo 'registry="https://registry.npmjs.org/"' > ./.npmrc + echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ./.npmrc + + echo 'registry="https://registry.npmjs.org/"' > ~/.npmrc + echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Pack NPM + working-directory: ${{ env.working-directory }} + run: npm pack + + - name: Publish NPM + working-directory: ${{ env.working-directory }} + run: npm publish --tarball=./infisical-sdk-${{github.ref_name}} --access public --registry=https://registry.npmjs.org/ + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + goreleaser: + runs-on: ubuntu-latest + needs: [cli-integration-tests] + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: 🐋 Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: 🔧 Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - run: git fetch --force --tags + - run: echo "Ref name ${{github.ref_name}}" + - uses: actions/setup-go@v3 + with: + go-version: ">=1.19.3" + cache: true + cache-dependency-path: cli/go.sum + - name: Setup for libssl1.0-dev + run: | + echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list + sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32 + sudo apt update + sudo apt-get install -y libssl1.0-dev + - name: OSXCross for CGO Support + run: | + mkdir ../../osxcross + git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target + - uses: goreleaser/goreleaser-action@v4 + with: + distribution: goreleaser-pro + version: v1.26.2-pro + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }} + POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }} + FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }} + AUR_KEY: ${{ secrets.AUR_KEY }} + GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} + - uses: actions/setup-python@v4 + - run: pip install --upgrade cloudsmith-cli + - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 + with: + ruby-version: "3.3" # Not needed with a .ruby-version, .tool-versions or mise.toml + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - name: Install deb-s3 + run: gem install deb-s3 + - name: Configure GPG Key + run: echo -n "$GPG_SIGNING_KEY" | base64 --decode | gpg --batch --import + env: + GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} + GPG_SIGNING_KEY_PASSPHRASE: ${{ secrets.GPG_SIGNING_KEY_PASSPHRASE }} + - name: Publish to CloudSmith + run: sh cli/upload_to_cloudsmith.sh + env: + CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }} + INFISICAL_CLI_S3_BUCKET: ${{ secrets.INFISICAL_CLI_S3_BUCKET }} + INFISICAL_CLI_REPO_SIGNING_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_SIGNING_KEY_ID }} + AWS_ACCESS_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }} + - name: Invalidate Cloudfront cache + run: aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_DISTRIBUTION_ID --paths '/deb/dists/stable/*' + env: + AWS_ACCESS_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }} + CLOUDFRONT_DISTRIBUTION_ID: ${{ secrets.INFISICAL_CLI_REPO_CLOUDFRONT_DISTRIBUTION_ID }} diff --git a/.github/workflows/release_docker_k8_operator.yaml b/.github/workflows/release_docker_k8_operator.yaml index 517549ea8..1f894df47 100644 --- a/.github/workflows/release_docker_k8_operator.yaml +++ b/.github/workflows/release_docker_k8_operator.yaml @@ -1,37 +1,107 @@ -name: Release Docker image for K8 operator +name: Release K8 Operator Docker Image on: - push: - tags: - - "infisical-k8-operator/v*.*.*" + push: + tags: + - "infisical-k8-operator/v*.*.*" + +permissions: + contents: write + pull-requests: write jobs: - release: - runs-on: ubuntu-latest - steps: - - name: Extract version from tag - id: extract_version - run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical-k8-operator/}" - - uses: actions/checkout@v2 + release-image: + name: Generate Helm Chart PR + runs-on: ubuntu-latest + outputs: + pr_number: ${{ steps.create-pr.outputs.pull-request-number }} + steps: + - name: Extract version from tag + id: extract_version + run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical-k8-operator/}" - - name: 🔧 Set up QEMU - uses: docker/setup-qemu-action@v1 + - name: Checkout code + uses: actions/checkout@v2 - - name: 🔧 Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + # Dependency for helm generation + - name: Install Helm + uses: azure/setup-helm@v3 + with: + version: v3.10.0 - - name: 🐋 Login to Docker Hub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + # Dependency for helm generation + - name: Install Go + uses: actions/setup-go@v4 + with: + go-version: 1.21 - - name: Build and push - id: docker_build - uses: docker/build-push-action@v2 - with: - context: k8-operator - push: true - platforms: linux/amd64,linux/arm64 - tags: | - infisical/kubernetes-operator:latest - infisical/kubernetes-operator:${{ steps.extract_version.outputs.version }} + # Install binaries for helm generation + - name: Install dependencies + working-directory: k8-operator + run: | + make helmify + make kustomize + make controller-gen + + - name: Generate Helm Chart + working-directory: k8-operator + run: make helm + + - name: Update Helm Chart Version + run: ./k8-operator/scripts/update-version.sh ${{ steps.extract_version.outputs.version }} + + - name: Debug - Check file changes + run: | + echo "Current git status:" + git status + echo "" + echo "Modified files:" + git diff --name-only + + # If there is no diff, exit with error. Version should always be changed, so if there is no diff, something is wrong and we should exit. + if [ -z "$(git diff --name-only)" ]; then + echo "No helm changes or version changes. Invalid release detected, Exiting." + exit 1 + fi + + - name: Create Helm Chart PR + id: create-pr + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "Update Helm chart to version ${{ steps.extract_version.outputs.version }}" + committer: GitHub + author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com> + branch: helm-update-${{ steps.extract_version.outputs.version }} + delete-branch: true + title: "Update Helm chart to version ${{ steps.extract_version.outputs.version }}" + body: | + This PR updates the Helm chart to version `${{ steps.extract_version.outputs.version }}`. + Additionally the helm chart has been updated to match the latest operator code changes. + + Associated Release Workflow: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + + Once you have approved this PR, you can trigger the helm release workflow manually. + base: main + + - name: 🔧 Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: 🔧 Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: 🐋 Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + id: docker_build + uses: docker/build-push-action@v2 + with: + context: k8-operator + push: true + platforms: linux/amd64,linux/arm64 + tags: | + infisical/kubernetes-operator:latest + infisical/kubernetes-operator:${{ steps.extract_version.outputs.version }} diff --git a/.github/workflows/run-backend-tests.yml b/.github/workflows/run-backend-tests.yml index 1fc9deff6..f2ba04e76 100644 --- a/.github/workflows/run-backend-tests.yml +++ b/.github/workflows/run-backend-tests.yml @@ -34,7 +34,10 @@ jobs: working-directory: backend - name: Start postgres and redis run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis - - name: Start integration test + - name: Run unit test + run: npm run test:unit + working-directory: backend + - name: Run integration test run: npm run test:e2e working-directory: backend env: @@ -44,4 +47,5 @@ jobs: ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218 - name: cleanup run: | - docker compose -f "docker-compose.dev.yml" down \ No newline at end of file + docker compose -f "docker-compose.dev.yml" down + diff --git a/.gitignore b/.gitignore index e76fd0c11..f2a23324b 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,5 @@ frontend-build cli/infisical-merge cli/test/infisical-merge /backend/binary + +/npm/bin diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 8f608c40c..e3147d650 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -162,6 +162,24 @@ scoop: description: "The official Infisical CLI" license: MIT +winget: + - name: infisical + publisher: infisical + license: MIT + homepage: https://infisical.com + short_description: "The official Infisical CLI" + repository: + owner: infisical + name: winget-pkgs + branch: "infisical-{{.Version}}" + pull_request: + enabled: true + draft: false + base: + owner: microsoft + name: winget-pkgs + branch: master + aurs: - name: infisical-bin homepage: "https://infisical.com" diff --git a/.husky/pre-commit b/.husky/pre-commit index 4f18d2521..9a9f7b9e4 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,6 +1,12 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" +# Check if infisical is installed +if ! command -v infisical >/dev/null 2>&1; then + echo "\nError: Infisical CLI is not installed. Please install the Infisical CLI before comitting.\n You can refer to the documentation at https://infisical.com/docs/cli/overview\n\n" + exit 1 +fi + npx lint-staged infisical scan git-changes --staged -v diff --git a/.infisicalignore b/.infisicalignore index b7fc38b35..4ccf734b6 100644 --- a/.infisicalignore +++ b/.infisicalignore @@ -6,3 +6,21 @@ frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/S docs/self-hosting/configuration/envars.mdx:generic-api-key:106 frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:451 docs/mint.json:generic-api-key:651 +backend/src/ee/services/hsm/hsm-service.ts:generic-api-key:134 +docs/documentation/platform/audit-log-streams/audit-log-streams.mdx:generic-api-key:104 +docs/cli/commands/bootstrap.mdx:jwt:86 +docs/documentation/platform/audit-log-streams/audit-log-streams.mdx:generic-api-key:102 +docs/self-hosting/guides/automated-bootstrapping.mdx:jwt:74 +frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx:generic-api-key:72 +k8-operator/config/samples/crd/pushsecret/source-secret-with-templating.yaml:private-key:11 +k8-operator/config/samples/crd/pushsecret/push-secret-with-template.yaml:private-key:52 +backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts:generic-api-key:125 +frontend/src/components/permissions/AccessTree/nodes/RoleNode.tsx:generic-api-key:67 +frontend/src/components/secret-rotations-v2/RotateSecretRotationV2Modal.tsx:generic-api-key:14 +frontend/src/components/secret-rotations-v2/SecretRotationV2StatusBadge.tsx:generic-api-key:11 +frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx:generic-api-key:23 +frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:28 +frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:65 +frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationItem.tsx:generic-api-key:26 +docs/documentation/platform/kms/overview.mdx:generic-api-key:281 +docs/documentation/platform/kms/overview.mdx:generic-api-key:344 diff --git a/Dockerfile.fips.standalone-infisical b/Dockerfile.fips.standalone-infisical new file mode 100644 index 000000000..33360bf45 --- /dev/null +++ b/Dockerfile.fips.standalone-infisical @@ -0,0 +1,184 @@ +ARG POSTHOG_HOST=https://app.posthog.com +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 base AS frontend-dependencies +WORKDIR /app + +COPY frontend/package.json frontend/package-lock.json ./ + +# Install dependencies +RUN npm ci --only-production --ignore-scripts + +# Rebuild the source code only when needed +FROM base AS frontend-builder +WORKDIR /app + +# Copy dependencies +COPY --from=frontend-dependencies /app/node_modules ./node_modules +# Copy all files +COPY /frontend . + +ENV NODE_ENV production +ARG POSTHOG_HOST +ENV VITE_POSTHOG_HOST $POSTHOG_HOST +ARG POSTHOG_API_KEY +ENV VITE_POSTHOG_API_KEY $POSTHOG_API_KEY +ARG INTERCOM_ID +ENV VITE_INTERCOM_ID $INTERCOM_ID +ARG INFISICAL_PLATFORM_VERSION +ENV VITE_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION +ARG CAPTCHA_SITE_KEY +ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY + +# Build +RUN npm run build + +# Production image +FROM base AS frontend-runner +WORKDIR /app + +RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user + +COPY --from=frontend-builder --chown=non-root-user:nodejs /app/dist ./ + +USER non-root-user + +## +## BACKEND +## +FROM base AS backend-build + +ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/ + +RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user + +WORKDIR /app + +# Required for pkcs11js and ODBC +RUN apt-get update && apt-get install -y \ + python3 \ + make \ + g++ \ + unixodbc \ + unixodbc-dev \ + freetds-dev \ + freetds-bin \ + tdsodbc \ + && rm -rf /var/lib/apt/lists/* + +# Configure ODBC +RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini + +COPY backend/package*.json ./ +RUN npm ci --only-production + +COPY /backend . +COPY --chown=non-root-user:nodejs standalone-entrypoint.sh standalone-entrypoint.sh +RUN npm i -D tsconfig-paths +RUN npm run build + +# Production stage +FROM base AS backend-runner + +ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/ + +WORKDIR /app + +# Required for pkcs11js and ODBC +RUN apt-get update && apt-get install -y \ + python3 \ + make \ + g++ \ + unixodbc \ + unixodbc-dev \ + freetds-dev \ + freetds-bin \ + tdsodbc \ + && rm -rf /var/lib/apt/lists/* + +# Configure ODBC +RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini + +COPY backend/package*.json ./ +RUN npm ci --only-production + +COPY --from=backend-build /app . + +RUN mkdir frontend-build + +# Production stage +FROM base AS production + +# Install necessary packages including ODBC +RUN apt-get update && apt-get install -y \ + ca-certificates \ + curl \ + git \ + python3 \ + make \ + g++ \ + unixodbc \ + unixodbc-dev \ + freetds-dev \ + freetds-bin \ + tdsodbc \ + openssh-client \ + && rm -rf /var/lib/apt/lists/* + +# Configure ODBC in production +RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini + +# Install Infisical CLI +RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash \ + && apt-get update && apt-get install -y infisical=0.31.1 \ + && rm -rf /var/lib/apt/lists/* + +RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user + +# Give non-root-user permission to update SSL certs +RUN chown -R non-root-user /etc/ssl/certs +RUN chown non-root-user /etc/ssl/certs/ca-certificates.crt +RUN chmod -R u+rwx /etc/ssl/certs +RUN chmod u+rw /etc/ssl/certs/ca-certificates.crt +RUN chown non-root-user /usr/sbin/update-ca-certificates +RUN chmod u+rx /usr/sbin/update-ca-certificates + +## set pre baked keys +ARG POSTHOG_API_KEY +ENV POSTHOG_API_KEY=$POSTHOG_API_KEY +ARG INTERCOM_ID=intercom-id +ENV INTERCOM_ID=$INTERCOM_ID +ARG CAPTCHA_SITE_KEY +ENV CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY + +WORKDIR / + +COPY --from=backend-runner /app /backend + +COPY --from=frontend-runner /app ./backend/frontend-build + +ARG INFISICAL_PLATFORM_VERSION +ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION + +ENV PORT 8080 +ENV HOST=0.0.0.0 +ENV HTTPS_ENABLED false +ENV NODE_ENV production +ENV STANDALONE_BUILD true +ENV STANDALONE_MODE true +ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/ + +WORKDIR /backend + +ENV TELEMETRY_ENABLED true + +EXPOSE 8080 +EXPOSE 443 + +USER non-root-user + +CMD ["./standalone-entrypoint.sh"] diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index 269cbfcf9..6d582ce76 100644 --- a/Dockerfile.standalone-infisical +++ b/Dockerfile.standalone-infisical @@ -3,16 +3,13 @@ ARG POSTHOG_API_KEY=posthog-api-key ARG INTERCOM_ID=intercom-id ARG CAPTCHA_SITE_KEY=captcha-site-key -FROM node:20-alpine AS base +FROM node:20-slim AS base FROM base AS frontend-dependencies -# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. -RUN apk add --no-cache libc6-compat - WORKDIR /app -COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./ +COPY frontend/package.json frontend/package-lock.json ./ # Install dependencies RUN npm ci --only-production --ignore-scripts @@ -27,17 +24,16 @@ COPY --from=frontend-dependencies /app/node_modules ./node_modules COPY /frontend . ENV NODE_ENV production -ENV NEXT_PUBLIC_ENV production ARG POSTHOG_HOST -ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST +ENV VITE_POSTHOG_HOST $POSTHOG_HOST ARG POSTHOG_API_KEY -ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY +ENV VITE_POSTHOG_API_KEY $POSTHOG_API_KEY ARG INTERCOM_ID -ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID +ENV VITE_INTERCOM_ID $INTERCOM_ID ARG INFISICAL_PLATFORM_VERSION -ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION +ENV VITE_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION ARG CAPTCHA_SITE_KEY -ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY +ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY # Build RUN npm run build @@ -46,32 +42,35 @@ RUN npm run build FROM base AS frontend-runner WORKDIR /app -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 non-root-user +RUN groupadd --system --gid 1001 nodejs +RUN useradd --system --uid 1001 --gid nodejs non-root-user -RUN mkdir -p /app/.next/cache/images && chown non-root-user:nodejs /app/.next/cache/images -VOLUME /app/.next/cache/images - -COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts -COPY --from=frontend-builder /app/public ./public -RUN chown non-root-user:nodejs ./public/data - -COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./ -COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static +COPY --from=frontend-builder --chown=non-root-user:nodejs /app/dist ./ USER non-root-user -ENV NEXT_TELEMETRY_DISABLED 1 - ## ## BACKEND ## FROM base AS backend-build -RUN addgroup --system --gid 1001 nodejs \ - && adduser --system --uid 1001 non-root-user WORKDIR /app +# Install all required dependencies for build +RUN apt-get update && apt-get install -y \ + python3 \ + make \ + g++ \ + unixodbc \ + freetds-bin \ + unixodbc-dev \ + libc-dev \ + freetds-dev \ + && rm -rf /var/lib/apt/lists/* + +RUN groupadd --system --gid 1001 nodejs +RUN useradd --system --uid 1001 --gid nodejs non-root-user + COPY backend/package*.json ./ RUN npm ci --only-production @@ -85,6 +84,21 @@ FROM base AS backend-runner WORKDIR /app +# Install all required dependencies for runtime +RUN apt-get update && apt-get install -y \ + python3 \ + make \ + g++ \ + unixodbc \ + freetds-bin \ + unixodbc-dev \ + libc-dev \ + freetds-dev \ + && rm -rf /var/lib/apt/lists/* + +# Configure ODBC +RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini + COPY backend/package*.json ./ RUN npm ci --only-production @@ -94,13 +108,37 @@ RUN mkdir frontend-build # Production stage FROM base AS production -RUN apk add --upgrade --no-cache ca-certificates -RUN apk add --no-cache bash curl && curl -1sLf \ - 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \ - && apk add infisical=0.31.1 && apk add --no-cache git -RUN addgroup --system --gid 1001 nodejs \ - && adduser --system --uid 1001 non-root-user +RUN apt-get update && apt-get install -y \ + ca-certificates \ + bash \ + curl \ + git \ + python3 \ + make \ + g++ \ + unixodbc \ + freetds-bin \ + unixodbc-dev \ + libc-dev \ + freetds-dev \ + wget \ + openssh-client \ + && rm -rf /var/lib/apt/lists/* + +# Install Infisical CLI +RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash \ + && apt-get update && apt-get install -y infisical=0.31.1 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR / + +# Configure ODBC in production +RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini + +# Setup user permissions +RUN groupadd --system --gid 1001 nodejs \ + && useradd --system --uid 1001 --gid nodejs non-root-user # Give non-root-user permission to update SSL certs RUN chown -R non-root-user /etc/ssl/certs @@ -112,21 +150,17 @@ RUN chmod u+rx /usr/sbin/update-ca-certificates ## set pre baked keys ARG POSTHOG_API_KEY -ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \ - BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY +ENV POSTHOG_API_KEY=$POSTHOG_API_KEY ARG INTERCOM_ID=intercom-id -ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \ - BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID +ENV INTERCOM_ID=$INTERCOM_ID ARG CAPTCHA_SITE_KEY -ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \ - BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY - -WORKDIR / +ENV CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY COPY --from=backend-runner /app /backend - COPY --from=frontend-runner /app ./backend/frontend-build +ARG INFISICAL_PLATFORM_VERSION +ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION ENV PORT 8080 ENV HOST=0.0.0.0 @@ -134,6 +168,7 @@ ENV HTTPS_ENABLED false ENV NODE_ENV production ENV STANDALONE_BUILD true ENV STANDALONE_MODE true + WORKDIR /backend ENV TELEMETRY_ENABLED true diff --git a/Makefile b/Makefile index aec2dad74..2352e5134 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,9 @@ up-dev: up-dev-ldap: docker compose -f docker-compose.dev.yml --profile ldap up --build +up-dev-metrics: + docker compose -f docker-compose.dev.yml --profile metrics up --build + up-prod: docker-compose -f docker-compose.prod.yml up --build @@ -28,3 +31,5 @@ reviewable-api: reviewable: reviewable-ui reviewable-api +up-dev-sso: + docker compose -f docker-compose.dev.yml --profile sso up --build diff --git a/README.md b/README.md index d68481428..f1393495d 100644 --- a/README.md +++ b/README.md @@ -14,15 +14,6 @@ Hiring (Remote/SF) -

- - - - - Deploy to DO - -

-

Infisical is released under the MIT license. @@ -59,13 +50,13 @@ We're on a mission to make security tooling more accessible to everyone, not jus - **[Dashboard](https://infisical.com/docs/documentation/platform/project)**: Manage secrets across projects and environments (e.g. development, production, etc.) through a user-friendly interface. - **[Native Integrations](https://infisical.com/docs/integrations/overview)**: Sync secrets to platforms like [GitHub](https://infisical.com/docs/integrations/cicd/githubactions), [Vercel](https://infisical.com/docs/integrations/cloud/vercel), [AWS](https://infisical.com/docs/integrations/cloud/aws-secret-manager), and use tools like [Terraform](https://infisical.com/docs/integrations/frameworks/terraform), [Ansible](https://infisical.com/docs/integrations/platforms/ansible), and more. - **[Secret versioning](https://infisical.com/docs/documentation/platform/secret-versioning)** and **[Point-in-Time Recovery](https://infisical.com/docs/documentation/platform/pit-recovery)**: Keep track of every secret and project state; roll back when needed. -- **[Secret Rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview)**: Rotate secrets at regular intervals for services like [PostgreSQL](https://infisical.com/docs/documentation/platform/secret-rotation/postgres), [MySQL](https://infisical.com/docs/documentation/platform/secret-rotation/mysql), [AWS IAM](https://infisical.com/docs/documentation/platform/secret-rotation/aws-iam), and more. +- **[Secret Rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview)**: Rotate secrets at regular intervals for services like [PostgreSQL](https://infisical.com/docs/documentation/platform/secret-rotation/postgres-credentials), [MySQL](https://infisical.com/docs/documentation/platform/secret-rotation/mysql), [AWS IAM](https://infisical.com/docs/documentation/platform/secret-rotation/aws-iam), and more. - **[Dynamic Secrets](https://infisical.com/docs/documentation/platform/dynamic-secrets/overview)**: Generate ephemeral secrets on-demand for services like [PostgreSQL](https://infisical.com/docs/documentation/platform/dynamic-secrets/postgresql), [MySQL](https://infisical.com/docs/documentation/platform/dynamic-secrets/mysql), [RabbitMQ](https://infisical.com/docs/documentation/platform/dynamic-secrets/rabbit-mq), and more. - **[Secret Scanning and Leak Prevention](https://infisical.com/docs/cli/scanning-overview)**: Prevent secrets from leaking to git. - **[Infisical Kubernetes Operator](https://infisical.com/docs/documentation/getting-started/kubernetes)**: Deliver secrets to your Kubernetes workloads and automatically reload deployments. - **[Infisical Agent](https://infisical.com/docs/infisical-agent/overview)**: Inject secrets into applications without modifying any code logic. -### Internal PKI: +### Infisical (Internal) PKI: - **[Private Certificate Authority](https://infisical.com/docs/documentation/platform/pki/private-ca)**: Create CA hierarchies, configure [certificate templates](https://infisical.com/docs/documentation/platform/pki/certificates#guide-to-issuing-certificates) for policy enforcement, and start issuing X.509 certificates. - **[Certificate Management](https://infisical.com/docs/documentation/platform/pki/certificates)**: Manage the certificate lifecycle from [issuance](https://infisical.com/docs/documentation/platform/pki/certificates#guide-to-issuing-certificates) to [revocation](https://infisical.com/docs/documentation/platform/pki/certificates#guide-to-revoking-certificates) with support for CRL. @@ -73,12 +64,17 @@ We're on a mission to make security tooling more accessible to everyone, not jus - **[Infisical PKI Issuer for Kubernetes](https://infisical.com/docs/documentation/platform/pki/pki-issuer)**: Deliver TLS certificates to your Kubernetes workloads with automatic renewal. - **[Enrollment over Secure Transport](https://infisical.com/docs/documentation/platform/pki/est)**: Enroll and manage certificates via EST protocol. -### Key Management (KMS): +### Infisical Key Management System (KMS): -- **[Cryptograhic Keys](https://infisical.com/docs/documentation/platform/kms)**: Centrally manage keys across projects through a user-friendly interface or via the API. +- **[Cryptographic Keys](https://infisical.com/docs/documentation/platform/kms)**: Centrally manage keys across projects through a user-friendly interface or via the API. - **[Encrypt and Decrypt Data](https://infisical.com/docs/documentation/platform/kms#guide-to-encrypting-data)**: Use symmetric keys to encrypt and decrypt data. +### Infisical SSH + +- **[Signed SSH Certificates](https://infisical.com/docs/documentation/platform/ssh)**: Issue ephemeral SSH credentials for secure, short-lived, and centralized access to infrastructure. + ### General Platform: + - **Authentication Methods**: Authenticate machine identities with Infisical using a cloud-native or platform agnostic authentication method ([Kubernetes Auth](https://infisical.com/docs/documentation/platform/identities/kubernetes-auth), [GCP Auth](https://infisical.com/docs/documentation/platform/identities/gcp-auth), [Azure Auth](https://infisical.com/docs/documentation/platform/identities/azure-auth), [AWS Auth](https://infisical.com/docs/documentation/platform/identities/aws-auth), [OIDC Auth](https://infisical.com/docs/documentation/platform/identities/oidc-auth/general), [Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth)). - **[Access Controls](https://infisical.com/docs/documentation/platform/access-controls/overview)**: Define advanced authorization controls for users and machine identities with [RBAC](https://infisical.com/docs/documentation/platform/access-controls/role-based-access-controls), [additional privileges](https://infisical.com/docs/documentation/platform/access-controls/additional-privileges), [temporary access](https://infisical.com/docs/documentation/platform/access-controls/temporary-access), [access requests](https://infisical.com/docs/documentation/platform/access-controls/access-requests), [approval workflows](https://infisical.com/docs/documentation/platform/pr-workflows), and more. - **[Audit logs](https://infisical.com/docs/documentation/platform/audit-logs)**: Track every action taken on the platform. @@ -129,7 +125,7 @@ Install pre commit hook to scan each commit before you push to your repository infisical scan install --pre-commit-hook ``` -Lean about Infisical's code scanning feature [here](https://infisical.com/docs/cli/scanning-overview) +Learn about Infisical's code scanning feature [here](https://infisical.com/docs/cli/scanning-overview) ## Open-source vs. paid diff --git a/backend/Dockerfile b/backend/Dockerfile index 2153ba33a..b9edf8b98 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,8 +1,24 @@ # Build stage -FROM node:20-alpine AS build +FROM node:20-slim AS build WORKDIR /app +# Required for pkcs11js +RUN apt-get update && apt-get install -y \ + python3 \ + make \ + g++ \ + openssh-client \ + openssl + +# Install dependencies for TDS driver (required for SAP ASE dynamic secrets) +RUN apt-get install -y \ + unixodbc \ + freetds-bin \ + freetds-dev \ + unixodbc-dev \ + libc-dev + COPY package*.json ./ RUN npm ci --only-production @@ -10,20 +26,36 @@ COPY . . RUN npm run build # Production stage -FROM node:20-alpine - +FROM node:20-slim WORKDIR /app ENV npm_config_cache /home/node/.npm COPY package*.json ./ + +RUN apt-get update && apt-get install -y \ + python3 \ + make \ + g++ + +# Install dependencies for TDS driver (required for SAP ASE dynamic secrets) +RUN apt-get install -y \ + unixodbc \ + freetds-bin \ + freetds-dev \ + unixodbc-dev \ + libc-dev + +RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini + RUN npm ci --only-production && npm cache clean --force COPY --from=build /app . -RUN apk add --no-cache bash curl && curl -1sLf \ - 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \ - && apk add infisical=0.8.1 && apk add --no-cache git +# Install Infisical CLI +RUN apt-get install -y curl bash && \ + curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash && \ + apt-get update && apt-get install -y infisical=0.8.1 git HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \ CMD node healthcheck.js diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev index ec34f63d7..3435672e7 100644 --- a/backend/Dockerfile.dev +++ b/backend/Dockerfile.dev @@ -1,8 +1,63 @@ -FROM node:20-alpine +FROM node:20-slim -RUN apk add --no-cache bash curl && curl -1sLf \ - 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \ - && apk add infisical=0.8.1 && apk add --no-cache git +# ? Setup a test SoftHSM module. In production a real HSM is used. + +ARG SOFTHSM2_VERSION=2.5.0 + +ENV SOFTHSM2_VERSION=${SOFTHSM2_VERSION} \ + SOFTHSM2_SOURCES=/tmp/softhsm2 + +# Install build dependencies including python3 (required for pkcs11js and partially TDS driver) +RUN apt-get update && apt-get install -y \ + build-essential \ + autoconf \ + automake \ + git \ + libtool \ + libssl-dev \ + python3 \ + make \ + g++ \ + openssh-client \ + openssl \ + curl \ + pkg-config + +# Install dependencies for TDS driver (required for SAP ASE dynamic secrets) +RUN apt-get install -y \ + unixodbc \ + unixodbc-dev \ + freetds-dev \ + freetds-bin \ + tdsodbc + +RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini + +# Build and install SoftHSM2 +RUN git clone https://github.com/opendnssec/SoftHSMv2.git ${SOFTHSM2_SOURCES} +WORKDIR ${SOFTHSM2_SOURCES} + +RUN git checkout ${SOFTHSM2_VERSION} -b ${SOFTHSM2_VERSION} \ + && sh autogen.sh \ + && ./configure --prefix=/usr/local --disable-gost \ + && make \ + && make install + +WORKDIR /root +RUN rm -fr ${SOFTHSM2_SOURCES} + +# Install pkcs11-tool +RUN apt-get install -y opensc + +RUN mkdir -p /etc/softhsm2/tokens && \ + softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000 + +# ? App setup + +# Install Infisical CLI +RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash && \ + apt-get update && \ + apt-get install -y infisical=0.8.1 WORKDIR /app diff --git a/backend/Dockerfile.dev.fips b/backend/Dockerfile.dev.fips new file mode 100644 index 000000000..8c40404dc --- /dev/null +++ b/backend/Dockerfile.dev.fips @@ -0,0 +1,85 @@ +FROM node:20-slim + +# ? Setup a test SoftHSM module. In production a real HSM is used. + +ARG SOFTHSM2_VERSION=2.5.0 + +ENV SOFTHSM2_VERSION=${SOFTHSM2_VERSION} \ + SOFTHSM2_SOURCES=/tmp/softhsm2 + +# Install build dependencies including python3 (required for pkcs11js and partially TDS driver) +RUN apt-get update && apt-get install -y \ + build-essential \ + autoconf \ + automake \ + git \ + libtool \ + libssl-dev \ + python3 \ + make \ + g++ \ + openssh-client \ + curl \ + pkg-config \ + perl \ + wget + +# Install dependencies for TDS driver (required for SAP ASE dynamic secrets) +RUN apt-get install -y \ + unixodbc \ + unixodbc-dev \ + freetds-dev \ + freetds-bin \ + tdsodbc + +RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini + +# Build and install SoftHSM2 +RUN git clone https://github.com/opendnssec/SoftHSMv2.git ${SOFTHSM2_SOURCES} +WORKDIR ${SOFTHSM2_SOURCES} + +RUN git checkout ${SOFTHSM2_VERSION} -b ${SOFTHSM2_VERSION} \ + && sh autogen.sh \ + && ./configure --prefix=/usr/local --disable-gost \ + && make \ + && make install + +WORKDIR /root +RUN rm -fr ${SOFTHSM2_SOURCES} + +# Install pkcs11-tool +RUN apt-get install -y opensc + +RUN mkdir -p /etc/softhsm2/tokens && \ + softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000 + +WORKDIR /openssl-build +RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \ + && tar -xf openssl-3.1.2.tar.gz \ + && cd openssl-3.1.2 \ + && ./Configure enable-fips \ + && make \ + && make install_fips + +# ? App setup + +# Install Infisical CLI +RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash && \ + apt-get update && \ + apt-get install -y infisical=0.8.1 + +WORKDIR /app + +COPY package.json package.json +COPY package-lock.json package-lock.json + +RUN npm install + +COPY . . + +ENV HOST=0.0.0.0 +ENV OPENSSL_CONF=/app/nodejs.cnf +ENV OPENSSL_MODULES=/usr/local/lib/ossl-modules +ENV NODE_OPTIONS=--force-fips + +CMD ["npm", "run", "dev:docker"] diff --git a/backend/e2e-test/mocks/keystore.ts b/backend/e2e-test/mocks/keystore.ts index 05753995c..48f52f9e7 100644 --- a/backend/e2e-test/mocks/keystore.ts +++ b/backend/e2e-test/mocks/keystore.ts @@ -9,6 +9,7 @@ export const mockKeyStore = (): TKeyStoreFactory => { store[key] = value; return "OK"; }, + setExpiry: async () => 0, setItemWithExpiry: async (key, value) => { store[key] = value; return "OK"; diff --git a/backend/e2e-test/mocks/queue.ts b/backend/e2e-test/mocks/queue.ts index c694979db..3f49bcfea 100644 --- a/backend/e2e-test/mocks/queue.ts +++ b/backend/e2e-test/mocks/queue.ts @@ -10,17 +10,23 @@ export const mockQueue = (): TQueueServiceFactory => { queue: async (name, jobData) => { job[name] = jobData; }, + queuePg: async () => {}, + schedulePg: async () => {}, + initialize: async () => {}, shutdown: async () => undefined, stopRepeatableJob: async () => true, start: (name, jobFn) => { queues[name] = jobFn; workers[name] = jobFn; }, + startPg: async () => {}, listen: (name, event) => { events[name] = event; }, + getRepeatableJobs: async () => [], clearQueue: async () => {}, stopJobById: async () => {}, - stopRepeatableJobByJobId: async () => true + stopRepeatableJobByJobId: async () => true, + stopRepeatableJobByKey: async () => true }; }; diff --git a/backend/e2e-test/mocks/smtp.ts b/backend/e2e-test/mocks/smtp.ts index 9f83f7134..4ba42e838 100644 --- a/backend/e2e-test/mocks/smtp.ts +++ b/backend/e2e-test/mocks/smtp.ts @@ -5,6 +5,9 @@ export const mockSmtpServer = (): TSmtpService => { return { sendMail: async (data) => { storage.push(data); + }, + verify: async () => { + return true; } }; }; diff --git a/backend/e2e-test/routes/v3/secret-recursive.spec.ts b/backend/e2e-test/routes/v3/secret-recursive.spec.ts new file mode 100644 index 000000000..b28dff985 --- /dev/null +++ b/backend/e2e-test/routes/v3/secret-recursive.spec.ts @@ -0,0 +1,86 @@ +import { createFolder, deleteFolder } from "e2e-test/testUtils/folders"; +import { createSecretV2, deleteSecretV2, getSecretsV2 } from "e2e-test/testUtils/secrets"; + +import { seedData1 } from "@app/db/seed-data"; + +describe("Secret Recursive Testing", async () => { + const projectId = seedData1.projectV3.id; + const folderAndSecretNames = [ + { name: "deep1", path: "/", expectedSecretCount: 4 }, + { name: "deep21", path: "/deep1", expectedSecretCount: 2 }, + { name: "deep3", path: "/deep1/deep2", expectedSecretCount: 1 }, + { name: "deep22", path: "/deep2", expectedSecretCount: 1 } + ]; + + beforeAll(async () => { + const rootFolderIds: string[] = []; + for (const folder of folderAndSecretNames) { + // eslint-disable-next-line no-await-in-loop + const createdFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: "prod", + workspaceId: projectId, + secretPath: folder.path, + name: folder.name + }); + + if (folder.path === "/") { + rootFolderIds.push(createdFolder.id); + } + // eslint-disable-next-line no-await-in-loop + await createSecretV2({ + secretPath: folder.path, + authToken: jwtAuthToken, + environmentSlug: "prod", + workspaceId: projectId, + key: folder.name, + value: folder.name + }); + } + + return async () => { + await Promise.all( + rootFolderIds.map((id) => + deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id, + workspaceId: projectId, + environmentSlug: "prod" + }) + ) + ); + + await deleteSecretV2({ + authToken: jwtAuthToken, + secretPath: "/", + workspaceId: projectId, + environmentSlug: "prod", + key: folderAndSecretNames[0].name + }); + }; + }); + + test.each(folderAndSecretNames)("$path recursive secret fetching", async ({ path, expectedSecretCount }) => { + const secrets = await getSecretsV2({ + authToken: jwtAuthToken, + secretPath: path, + workspaceId: projectId, + environmentSlug: "prod", + recursive: true + }); + + expect(secrets.secrets.length).toEqual(expectedSecretCount); + expect(secrets.secrets.sort((a, b) => a.secretKey.localeCompare(b.secretKey))).toEqual( + folderAndSecretNames + .filter((el) => el.path.startsWith(path)) + .sort((a, b) => a.name.localeCompare(b.name)) + .map((el) => + expect.objectContaining({ + secretKey: el.name, + secretValue: el.name + }) + ) + ); + }); +}); diff --git a/backend/e2e-test/routes/v3/secrets-v2.spec.ts b/backend/e2e-test/routes/v3/secrets-v2.spec.ts index dc02587cd..a6f4475e0 100644 --- a/backend/e2e-test/routes/v3/secrets-v2.spec.ts +++ b/backend/e2e-test/routes/v3/secrets-v2.spec.ts @@ -535,6 +535,107 @@ describe.each([{ auth: AuthMode.JWT }, { auth: AuthMode.IDENTITY_ACCESS_TOKEN }] ); }); + test.each(secretTestCases)("Bulk upsert secrets in path $path", async ({ secret, path }) => { + const updateSharedSecRes = await testServer.inject({ + method: "PATCH", + url: `/api/v3/secrets/batch/raw`, + headers: { + authorization: `Bearer ${authToken}` + }, + body: { + workspaceId: seedData1.projectV3.id, + environment: seedData1.environment.slug, + secretPath: path, + mode: "upsert", + secrets: Array.from(Array(5)).map((_e, i) => ({ + secretKey: `BULK-${secret.key}-${i + 1}`, + secretValue: "update-value", + secretComment: secret.comment + })) + } + }); + expect(updateSharedSecRes.statusCode).toBe(200); + const updateSharedSecPayload = JSON.parse(updateSharedSecRes.payload); + expect(updateSharedSecPayload).toHaveProperty("secrets"); + + // bulk ones should exist + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.arrayContaining( + Array.from(Array(5)).map((_e, i) => + expect.objectContaining({ + secretKey: `BULK-${secret.key}-${i + 1}`, + secretValue: "update-value", + type: SecretType.Shared + }) + ) + ) + ); + await Promise.all( + Array.from(Array(5)).map((_e, i) => deleteSecret({ path, key: `BULK-${secret.key}-${i + 1}` })) + ); + }); + + test("Bulk upsert secrets in path multiple paths", async () => { + const firstBatchSecrets = Array.from(Array(5)).map((_e, i) => ({ + secretKey: `BULK-KEY-${secretTestCases[0].secret.key}-${i + 1}`, + secretValue: "update-value", + secretComment: "comment", + secretPath: secretTestCases[0].path + })); + const secondBatchSecrets = Array.from(Array(5)).map((_e, i) => ({ + secretKey: `BULK-KEY-${secretTestCases[1].secret.key}-${i + 1}`, + secretValue: "update-value", + secretComment: "comment", + secretPath: secretTestCases[1].path + })); + const testSecrets = [...firstBatchSecrets, ...secondBatchSecrets]; + + const updateSharedSecRes = await testServer.inject({ + method: "PATCH", + url: `/api/v3/secrets/batch/raw`, + headers: { + authorization: `Bearer ${authToken}` + }, + body: { + workspaceId: seedData1.projectV3.id, + environment: seedData1.environment.slug, + mode: "upsert", + secrets: testSecrets + } + }); + expect(updateSharedSecRes.statusCode).toBe(200); + const updateSharedSecPayload = JSON.parse(updateSharedSecRes.payload); + expect(updateSharedSecPayload).toHaveProperty("secrets"); + + // bulk ones should exist + const firstBatchSecretsOnInfisical = await getSecrets(seedData1.environment.slug, secretTestCases[0].path); + expect(firstBatchSecretsOnInfisical).toEqual( + expect.arrayContaining( + firstBatchSecrets.map((el) => + expect.objectContaining({ + secretKey: el.secretKey, + secretValue: "update-value", + type: SecretType.Shared + }) + ) + ) + ); + const secondBatchSecretsOnInfisical = await getSecrets(seedData1.environment.slug, secretTestCases[1].path); + expect(secondBatchSecretsOnInfisical).toEqual( + expect.arrayContaining( + secondBatchSecrets.map((el) => + expect.objectContaining({ + secretKey: el.secretKey, + secretValue: "update-value", + type: SecretType.Shared + }) + ) + ) + ); + await Promise.all(testSecrets.map((el) => deleteSecret({ path: el.secretPath, key: el.secretKey }))); + }); + test.each(secretTestCases)("Bulk delete secrets in path $path", async ({ secret, path }) => { await Promise.all( Array.from(Array(5)).map((_e, i) => createSecret({ ...secret, key: `BULK-${secret.key}-${i + 1}`, path })) diff --git a/backend/e2e-test/testUtils/secrets.ts b/backend/e2e-test/testUtils/secrets.ts index 96ecc91c6..8b9e47f2f 100644 --- a/backend/e2e-test/testUtils/secrets.ts +++ b/backend/e2e-test/testUtils/secrets.ts @@ -97,6 +97,7 @@ export const getSecretsV2 = async (dto: { environmentSlug: string; secretPath: string; authToken: string; + recursive?: boolean; }) => { const getSecretsResponse = await testServer.inject({ method: "GET", @@ -109,7 +110,8 @@ export const getSecretsV2 = async (dto: { environment: dto.environmentSlug, secretPath: dto.secretPath, expandSecretReferences: "true", - include_imports: "true" + include_imports: "true", + recursive: String(dto.recursive || false) } }); expect(getSecretsResponse.statusCode).toBe(200); diff --git a/backend/e2e-test/vitest-environment-knex.ts b/backend/e2e-test/vitest-environment-knex.ts index 7be0b860f..46b322349 100644 --- a/backend/e2e-test/vitest-environment-knex.ts +++ b/backend/e2e-test/vitest-environment-knex.ts @@ -16,20 +16,21 @@ import { initDbConnection } from "@app/db"; import { queueServiceFactory } from "@app/queue"; import { keyStoreFactory } from "@app/keystore/keystore"; import { Redis } from "ioredis"; +import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns"; dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true }); export default { name: "knex-env", transformMode: "ssr", async setup() { - const logger = await initLogger(); - const cfg = initEnvConfig(logger); + const logger = initLogger(); + const envConfig = initEnvConfig(logger); const db = initDbConnection({ - dbConnectionUri: cfg.DB_CONNECTION_URI, - dbRootCert: cfg.DB_ROOT_CERT + dbConnectionUri: envConfig.DB_CONNECTION_URI, + dbRootCert: envConfig.DB_ROOT_CERT }); - const redis = new Redis(cfg.REDIS_URL); + const redis = new Redis(envConfig.REDIS_URL); await redis.flushdb("SYNC"); try { @@ -41,6 +42,7 @@ export default { }, true ); + await db.migrate.latest({ directory: path.join(__dirname, "../src/db/migrations"), extension: "ts", @@ -51,10 +53,25 @@ export default { directory: path.join(__dirname, "../src/db/seeds"), extension: "ts" }); + const smtp = mockSmtpServer(); - const queue = queueServiceFactory(cfg.REDIS_URL); - const keyStore = keyStoreFactory(cfg.REDIS_URL); - const server = await main({ db, smtp, logger, queue, keyStore }); + const queue = queueServiceFactory(envConfig.REDIS_URL, { dbConnectionUrl: envConfig.DB_CONNECTION_URI }); + const keyStore = keyStoreFactory(envConfig.REDIS_URL); + + const hsmModule = initializeHsmModule(envConfig); + hsmModule.initialize(); + + const server = await main({ + db, + smtp, + logger, + queue, + keyStore, + hsmModule: hsmModule.getModule(), + redis, + envConfig + }); + // @ts-expect-error type globalThis.testServer = server; // @ts-expect-error type @@ -67,8 +84,8 @@ export default { organizationId: seedData1.organization.id, accessVersion: 1 }, - cfg.AUTH_SECRET, - { expiresIn: cfg.JWT_AUTH_LIFETIME } + envConfig.AUTH_SECRET, + { expiresIn: envConfig.JWT_AUTH_LIFETIME } ); } catch (error) { // eslint-disable-next-line diff --git a/backend/nodejs.cnf b/backend/nodejs.cnf new file mode 100644 index 000000000..47d4a3fe3 --- /dev/null +++ b/backend/nodejs.cnf @@ -0,0 +1,16 @@ +nodejs_conf = nodejs_init + +.include /usr/local/ssl/fipsmodule.cnf + +[nodejs_init] +providers = provider_sect + +[provider_sect] +default = default_sect +fips = fips_sect + +[default_sect] +activate = 1 + +[algorithm_sect] +default_properties = fips=yes diff --git a/backend/package-lock.json b/backend/package-lock.json index 6829ebbc8..be6137424 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -21,24 +21,36 @@ "@fastify/etag": "^5.1.0", "@fastify/formbody": "^7.4.0", "@fastify/helmet": "^11.1.1", - "@fastify/multipart": "8.3.0", + "@fastify/multipart": "8.3.1", "@fastify/passport": "^2.4.0", "@fastify/rate-limit": "^9.0.0", + "@fastify/request-context": "^5.1.0", "@fastify/session": "^10.7.0", + "@fastify/static": "^7.0.4", "@fastify/swagger": "^8.14.0", "@fastify/swagger-ui": "^2.1.0", - "@node-saml/passport-saml": "^4.0.4", + "@google-cloud/kms": "^4.5.0", + "@infisical/quic": "^1.0.8", + "@node-saml/passport-saml": "^5.0.1", "@octokit/auth-app": "^7.1.1", "@octokit/plugin-retry": "^5.0.5", "@octokit/rest": "^20.0.2", "@octokit/webhooks-types": "^7.3.1", + "@octopusdeploy/api-client": "^3.4.1", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.55.0", + "@opentelemetry/exporter-prometheus": "^0.55.0", + "@opentelemetry/instrumentation": "^0.55.0", + "@opentelemetry/instrumentation-http": "^0.57.2", + "@opentelemetry/resources": "^1.28.0", + "@opentelemetry/sdk-metrics": "^1.28.0", + "@opentelemetry/semantic-conventions": "^1.27.0", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/x509": "^1.12.1", "@serdnam/pino-cloudwatch-transport": "^1.0.4", "@sindresorhus/slugify": "1.1.0", - "@slack/oauth": "^3.0.1", - "@slack/web-api": "^7.3.4", - "@team-plain/typescript-sdk": "^4.6.1", + "@slack/oauth": "^3.0.2", + "@slack/web-api": "^7.8.0", "@ucast/mongo2js": "^1.3.4", "ajv": "^8.12.0", "argon2": "^0.31.2", @@ -50,6 +62,7 @@ "cassandra-driver": "^4.7.2", "connect-redis": "^7.1.1", "cron": "^3.1.7", + "dd-trace": "^5.40.0", "dotenv": "^16.4.1", "fastify": "^4.28.1", "fastify-plugin": "^4.5.1", @@ -58,6 +71,7 @@ "handlebars": "^4.7.8", "hdb": "^0.19.10", "ioredis": "^5.3.2", + "isomorphic-dompurify": "^2.22.0", "jmespath": "^0.16.0", "jsonwebtoken": "^9.0.2", "jsrp": "^0.2.4", @@ -70,22 +84,27 @@ "mongodb": "^6.8.1", "ms": "^2.1.3", "mysql2": "^3.9.8", - "nanoid": "^3.3.4", + "nanoid": "^3.3.8", "nodemailer": "^6.9.9", + "odbc": "^2.4.9", "openid-client": "^5.6.5", "ora": "^7.0.1", "oracledb": "^6.4.0", + "otplib": "^12.0.1", "passport-github": "^1.1.0", "passport-gitlab2": "^5.0.0", "passport-google-oauth20": "^2.0.0", "passport-ldapauth": "^3.0.1", "pg": "^8.11.3", + "pg-boss": "^10.1.5", "pg-query-stream": "^4.5.3", "picomatch": "^3.0.1", "pino": "^8.16.2", + "pkcs11js": "^2.1.6", "pkijs": "^3.2.4", "posthog-node": "^3.6.2", "probot": "^13.3.8", + "re2": "^1.21.4", "safe-regex": "^2.1.1", "scim-patch": "^0.8.3", "scim2-parse-filter": "^0.2.10", @@ -114,12 +133,13 @@ "@types/jsrp": "^0.2.6", "@types/libsodium-wrappers": "^0.7.13", "@types/lodash.isequal": "^4.5.8", - "@types/node": "^20.9.5", + "@types/node": "^20.17.30", "@types/nodemailer": "^6.4.14", "@types/passport-github": "^1.1.12", "@types/passport-google-oauth20": "^2.0.14", "@types/pg": "^8.10.9", "@types/picomatch": "^2.3.3", + "@types/pkcs11js": "^1.0.4", "@types/prompt-sync": "^4.2.3", "@types/resolve": "^1.20.6", "@types/safe-regex": "^1.1.6", @@ -182,6 +202,23 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-2.8.3.tgz", + "integrity": "sha512-GIc76d9UI1hCvOATjZPyHFmE5qhRccp3/zGfMPapK3jBi+yocEzp6BBB0UnfRYP9NP4FANqUZYb0hnfs3TM3hw==", + "dependencies": { + "@csstools/css-calc": "^2.1.1", + "@csstools/css-color-parser": "^3.0.7", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", @@ -4852,6 +4889,111 @@ "node": ">=12" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.2.tgz", + "integrity": "sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.8.tgz", + "integrity": "sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", + "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", + "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@dabh/diagnostics": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", @@ -4863,6 +5005,109 @@ "kuler": "^2.0.0" } }, + "node_modules/@datadog/libdatadog": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@datadog/libdatadog/-/libdatadog-0.4.0.tgz", + "integrity": "sha512-kGZfFVmQInzt6J4FFGrqMbrDvOxqwk3WqhAreS6n9b/De+iMVy/NMu3V7uKsY5zAvz+uQw0liDJm3ZDVH/MVVw==" + }, + "node_modules/@datadog/native-appsec": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@datadog/native-appsec/-/native-appsec-8.4.0.tgz", + "integrity": "sha512-LC47AnpVLpQFEUOP/nIIs+i0wLb8XYO+et3ACaJlHa2YJM3asR4KZTqQjDQNy08PTAUbVvYWKwfSR1qVsU/BeA==", + "hasInstallScript": true, + "dependencies": { + "node-gyp-build": "^3.9.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@datadog/native-iast-rewriter": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@datadog/native-iast-rewriter/-/native-iast-rewriter-2.8.0.tgz", + "integrity": "sha512-DKmtvlmCld9RIJwDcPKWNkKYWYQyiuOrOtynmBppJiUv/yfCOuZtsQV4Zepj40H33sLiQyi5ct6dbWl53vxqkA==", + "dependencies": { + "lru-cache": "^7.14.0", + "node-gyp-build": "^4.5.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@datadog/native-iast-rewriter/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/@datadog/native-iast-rewriter/node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/@datadog/native-iast-taint-tracking": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@datadog/native-iast-taint-tracking/-/native-iast-taint-tracking-3.3.0.tgz", + "integrity": "sha512-OzmjOncer199ATSYeCAwSACCRyQimo77LKadSHDUcxa/n9FYU+2U/bYQTYsK3vquSA2E47EbSVq9rytrlTdvnA==", + "hasInstallScript": true, + "dependencies": { + "node-gyp-build": "^3.9.0" + } + }, + "node_modules/@datadog/native-metrics": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@datadog/native-metrics/-/native-metrics-3.1.0.tgz", + "integrity": "sha512-yOBi4x0OQRaGNPZ2bx9TGvDIgEdQ8fkudLTFAe7gEM1nAlvFmbE5YfpH8WenEtTSEBwojSau06m2q7axtEEmCg==", + "hasInstallScript": true, + "dependencies": { + "node-addon-api": "^6.1.0", + "node-gyp-build": "^3.9.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@datadog/native-metrics/node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" + }, + "node_modules/@datadog/pprof": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@datadog/pprof/-/pprof-5.5.1.tgz", + "integrity": "sha512-3pZVYqc5YkZJOj9Rc8kQ/wG4qlygcnnwFU/w0QKX6dEdJh+1+dWniuUu+GSEjy/H0jc14yhdT2eJJf/F2AnHNw==", + "hasInstallScript": true, + "dependencies": { + "delay": "^5.0.0", + "node-gyp-build": "<4.0", + "p-limit": "^3.1.0", + "pprof-format": "^2.1.0", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@datadog/pprof/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@datadog/sketches-js": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@datadog/sketches-js/-/sketches-js-2.1.1.tgz", + "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", @@ -5391,6 +5636,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz", "integrity": "sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==", + "license": "MIT", "engines": { "node": ">=14" } @@ -5406,13 +5652,10 @@ } }, "node_modules/@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", - "license": "MIT", - "engines": { - "node": ">=14" - } + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.1.1.tgz", + "integrity": "sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==", + "license": "MIT" }, "node_modules/@fastify/cookie": { "version": "9.3.1", @@ -5485,19 +5728,41 @@ } }, "node_modules/@fastify/multipart": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@fastify/multipart/-/multipart-8.3.0.tgz", - "integrity": "sha512-A8h80TTyqUzaMVH0Cr9Qcm6RxSkVqmhK/MVBYHYeRRSUbUYv08WecjWKSlG2aSnD4aGI841pVxAjC+G1GafUeQ==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@fastify/multipart/-/multipart-8.3.1.tgz", + "integrity": "sha512-pncbnG28S6MIskFSVRtzTKE9dK+GrKAJl0NbaQ/CG8ded80okWFsYKzSlP9haaLNQhNRDOoHqmGQNvgbiPVpWQ==", "license": "MIT", "dependencies": { - "@fastify/busboy": "^2.1.0", - "@fastify/deepmerge": "^1.0.0", - "@fastify/error": "^3.0.0", + "@fastify/busboy": "^3.0.0", + "@fastify/deepmerge": "^2.0.0", + "@fastify/error": "^4.0.0", "fastify-plugin": "^4.0.0", "secure-json-parse": "^2.4.0", "stream-wormhole": "^1.1.0" } }, + "node_modules/@fastify/multipart/node_modules/@fastify/deepmerge": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-2.0.1.tgz", + "integrity": "sha512-hx+wJQr9Ph1hY/dyzY0SxqjumMyqZDlIF6oe71dpRKDHUg7dFQfjG94qqwQ274XRjmUrwKiYadex8XplNHx3CA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/multipart/node_modules/@fastify/error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.0.0.tgz", + "integrity": "sha512-OO/SA8As24JtT1usTUTKgGH7uLvhfwZPwlptRi2Dp5P4KKmJI3gvsZ8MIHnNwDs4sLf/aai5LzTyl66xr7qMxA==", + "license": "MIT" + }, "node_modules/@fastify/passport": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@fastify/passport/-/passport-2.4.0.tgz", @@ -5517,10 +5782,20 @@ "toad-cache": "^3.3.0" } }, + "node_modules/@fastify/request-context": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/request-context/-/request-context-5.1.0.tgz", + "integrity": "sha512-PM7wrLJOEylVDpxabOFLaYsdAiaa0lpDUcP2HMFJ1JzgiWuC6k4r3duf6Pm9YLnzlGmT+Yp4tkQjqsu7V/pSOA==", + "license": "MIT", + "dependencies": { + "fastify-plugin": "^4.0.0" + } + }, "node_modules/@fastify/send": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.1.0.tgz", "integrity": "sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==", + "license": "MIT", "dependencies": { "@lukeed/ms": "^2.0.1", "escape-html": "~1.0.3", @@ -5539,16 +5814,85 @@ } }, "node_modules/@fastify/static": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@fastify/static/-/static-6.12.0.tgz", - "integrity": "sha512-KK1B84E6QD/FcQWxDI2aiUCwHxMJBI1KeCUzm1BwYpPY1b742+jeKruGHP2uOluuM6OkBPI8CIANrXcCRtC2oQ==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-7.0.4.tgz", + "integrity": "sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q==", + "license": "MIT", "dependencies": { "@fastify/accept-negotiator": "^1.0.0", "@fastify/send": "^2.0.0", "content-disposition": "^0.5.3", "fastify-plugin": "^4.0.0", - "glob": "^8.0.1", - "p-limit": "^3.1.0" + "fastq": "^1.17.0", + "glob": "^10.3.4" + } + }, + "node_modules/@fastify/static/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==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@fastify/static/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@fastify/static/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/@fastify/static/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@fastify/static/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" } }, "node_modules/@fastify/swagger": { @@ -5575,6 +5919,32 @@ "yaml": "^2.2.2" } }, + "node_modules/@fastify/swagger-ui/node_modules/@fastify/static": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-6.12.0.tgz", + "integrity": "sha512-KK1B84E6QD/FcQWxDI2aiUCwHxMJBI1KeCUzm1BwYpPY1b742+jeKruGHP2uOluuM6OkBPI8CIANrXcCRtC2oQ==", + "license": "MIT", + "dependencies": { + "@fastify/accept-negotiator": "^1.0.0", + "@fastify/send": "^2.0.0", + "content-disposition": "^0.5.3", + "fastify-plugin": "^4.0.0", + "glob": "^8.0.1", + "p-limit": "^3.1.0" + } + }, + "node_modules/@google-cloud/kms": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@google-cloud/kms/-/kms-4.5.0.tgz", + "integrity": "sha512-i2vC0DI7bdfEhQszqASTw0KVvbB7HsO2CwTBod423NawAu7FWi+gVVa7NLfXVNGJaZZayFfci2Hu+om/HmyEjQ==", + "license": "Apache-2.0", + "dependencies": { + "google-gax": "^4.0.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@google-cloud/paginator": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", @@ -5641,12 +6011,124 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/@graphql-typed-document-node/core": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", - "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "node_modules/@grpc/grpc-js": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.2.tgz", + "integrity": "sha512-bgxdZmgTrJZX50OjyVwz3+mNEnCTNkh3cIqGPWVNeW9jX6bn1ZkU80uPd+67/ZpIJIjRQ9qaHCjhavyoWYxumg==", + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@grpc/proto-loader/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/@grpc/proto-loader/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@grpc/proto-loader/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==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@grpc/proto-loader/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" } }, "node_modules/@hapi/bourne": { @@ -5710,6 +6192,112 @@ "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, + "node_modules/@infisical/quic": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@infisical/quic/-/quic-1.0.8.tgz", + "integrity": "sha512-ozgGkVdLP+ST41rPYmtaUBnBzisEzoI1l9yIds2l3hbd6OX6I836G0djTWLZbisn2ZO8CEbr8buLUclxqiY/IA==", + "license": "Apache-2.0", + "dependencies": { + "@matrixai/async-cancellable": "^1.1.1", + "@matrixai/async-init": "^1.10.0", + "@matrixai/async-locks": "^4.0.0", + "@matrixai/contexts": "^1.2.0", + "@matrixai/errors": "^1.2.0", + "@matrixai/events": "^3.2.3", + "@matrixai/logger": "^3.1.2", + "@matrixai/resources": "^1.1.5", + "@matrixai/timer": "^1.1.3", + "ip-num": "^1.5.0" + }, + "optionalDependencies": { + "@infisical/quic-darwin-arm64": "1.0.8", + "@infisical/quic-darwin-universal": "1.0.8", + "@infisical/quic-darwin-x64": "1.0.8", + "@infisical/quic-linux-arm": "1.0.8", + "@infisical/quic-linux-arm64": "1.0.8", + "@infisical/quic-linux-x64": "1.0.8", + "@infisical/quic-win32-x64": "1.0.8" + } + }, + "node_modules/@infisical/quic-darwin-arm64": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@infisical/quic-darwin-arm64/-/quic-darwin-arm64-1.0.8.tgz", + "integrity": "sha512-gJ1g2magCRQ+v02q6Jx50t85xIWnXgihYz/vak0YDYUIIOZFrcc7BaxNdfAo6lcxxDuGnK9RDTcLOpfMWt9EtQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@infisical/quic-darwin-universal": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@infisical/quic-darwin-universal/-/quic-darwin-universal-1.0.8.tgz", + "integrity": "sha512-BsOVIKexP+FysAyddnHHNKADUKz12ZzX7Eyl3fx4ZBGYdjzk3QTkG989HhSvYYQ+p3GyxB7p+Qn1cX3W5q3SHw==", + "cpu": [ + "x64", + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@infisical/quic-darwin-x64": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@infisical/quic-darwin-x64/-/quic-darwin-x64-1.0.8.tgz", + "integrity": "sha512-xvWuAB8plgGFxK9trO1MYq5xhRFgKRySpWxOuPDkNrGQZJNBZTiQfCM91dWfpMip1GhmhO7K0fcp5GYTphMXCA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@infisical/quic-linux-arm64": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@infisical/quic-linux-arm64/-/quic-linux-arm64-1.0.8.tgz", + "integrity": "sha512-vOEr/gIskr+eXN0rofpX1+hCJeKjUnHpt97LVsob9Jh7cXVGa9ywM0NbbYurz7FOdWH8+fBtppxkto4UdLSrZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@infisical/quic-linux-x64": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@infisical/quic-linux-x64/-/quic-linux-x64-1.0.8.tgz", + "integrity": "sha512-YE4xk5xv2oAiqZwVqNhC9AUnuFj1Q0cUiZmxih0RqfO51sr1BCgqmSBRWRykrdL8oyPmQduSNWKN7FcckPhYVQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@infisical/quic-win32-x64": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@infisical/quic-win32-x64/-/quic-win32-x64-1.0.8.tgz", + "integrity": "sha512-Ihh+NxKI6ujJTvOoRwQ8PUmoxU05iiAzkq0r0tP2xtTdXfg57R7sOGUyJCjg1AwpS40T+xgGTIjIGGQS50qYvg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@ioredis/commands": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", @@ -5752,6 +6340,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@isaacs/ttlcache": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", + "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==", + "engines": { + "node": ">=12" + } + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -5827,6 +6423,15 @@ "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.6.3.tgz", "integrity": "sha512-T1rRxzdqkEXcou0ZprN1q9yDRlvzCPLqmlNt5IIsGBzoEVgLCCYrKEwc84+TvsXuAc95VAZwtWD2zVsKPY4bcA==" }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@ldapjs/asn1": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@ldapjs/asn1/-/asn1-2.0.0.tgz", @@ -5905,9 +6510,10 @@ "integrity": "sha512-O89xFDLW2gBoZWNXuXpBSM32/KealKCTb3JGtJdtUQc7RjAk8XzrRgyz02cPAwGKwKPxy0ivuC7UP9bmN87egQ==" }, "node_modules/@lukeed/ms": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.1.tgz", - "integrity": "sha512-Xs/4RZltsAL7pkvaNStUQt7netTkyxrS0K+RILcVr3TRMS/ToOg4I6uNfhB9SlGsnWBym4U+EaXq0f0cEMNkHA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "license": "MIT", "engines": { "node": ">=8" } @@ -5964,6 +6570,85 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@matrixai/async-cancellable": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@matrixai/async-cancellable/-/async-cancellable-1.1.1.tgz", + "integrity": "sha512-f0yxu7dHwvffZ++7aCm2WIcCJn18uLcOTdCCwEA3R3KVHYE3TG/JNoTWD9/mqBkAV1AI5vBfJzg27WnF9rOUXQ==", + "license": "Apache-2.0" + }, + "node_modules/@matrixai/async-init": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@matrixai/async-init/-/async-init-1.10.0.tgz", + "integrity": "sha512-JjUFu6rqd+dtTHFJ6z8bjbceuFGBj/APWfJByVsfbEH1DJsOgWERFcW3DBUrS0mgTph4Vl518tsNcsSwKT5Y+g==", + "license": "Apache-2.0", + "dependencies": { + "@matrixai/async-locks": "^4.0.0", + "@matrixai/errors": "^1.2.0", + "@matrixai/events": "^3.2.0" + } + }, + "node_modules/@matrixai/async-locks": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@matrixai/async-locks/-/async-locks-4.0.0.tgz", + "integrity": "sha512-u/3fOdtjOKcDYF8dDoPR1/+7nmOkhxo42eBpXTEgfI0hLPGI37PoW7tjLvwy+O51Quy1HGOwhsR/Dgr4x+euug==", + "license": "Apache-2.0", + "dependencies": { + "@matrixai/async-cancellable": "^1.1.1", + "@matrixai/errors": "^1.1.7", + "@matrixai/resources": "^1.1.5", + "@matrixai/timer": "^1.1.1" + } + }, + "node_modules/@matrixai/contexts": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@matrixai/contexts/-/contexts-1.2.0.tgz", + "integrity": "sha512-MR/B02Kf4UoliP9b/gMMKsvWV6QM4JSPKTIqrhQP2tbOl3FwLI+AIhL3vgYEj1Xw+PP8bY5cr8ontJ8x6AJyMg==", + "license": "Apache-2.0", + "dependencies": { + "@matrixai/async-cancellable": "^1.1.1", + "@matrixai/async-locks": "^4.0.0", + "@matrixai/errors": "^1.1.7", + "@matrixai/resources": "^1.1.5", + "@matrixai/timer": "^1.1.1" + } + }, + "node_modules/@matrixai/errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@matrixai/errors/-/errors-1.2.0.tgz", + "integrity": "sha512-eZHPHFla5GFmi0O0yGgbtkca+ZjwpDbMz+60NC3y+DzQq6BMoe4gHmPjDalAHTxyxv0+Q+AWJTuV8Ows+IqBfQ==", + "license": "Apache-2.0", + "dependencies": { + "ts-custom-error": "3.2.2" + } + }, + "node_modules/@matrixai/events": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@matrixai/events/-/events-3.2.3.tgz", + "integrity": "sha512-bZrNCwzYeFalGQpn8qa/jgD10mUAwLRbv6xGMI7gGz1f+vE65d3GPoJ6JoFOJSg9iCmRSayQJ+IipH3LMATvDA==", + "license": "Apache-2.0" + }, + "node_modules/@matrixai/logger": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@matrixai/logger/-/logger-3.1.3.tgz", + "integrity": "sha512-wIHyiAzkrlZ/qlss4HLtXFzdlk1hxmAb0FNCQMd7ZwiHJABiKbvQtFSpBUvUakZYwYomiFmHwTYkHloiIVjrsg==", + "license": "Apache-2.0" + }, + "node_modules/@matrixai/resources": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@matrixai/resources/-/resources-1.1.5.tgz", + "integrity": "sha512-m/DEZEe3wHqWEPTyoBtzFF6U9vWYhEnQtGgwvqiAlTxTM0rk96UBpWjDZCTF/vYG11ZlmlQFtg5H+zGgbjaB3Q==", + "license": "Apache-2.0" + }, + "node_modules/@matrixai/timer": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@matrixai/timer/-/timer-1.1.3.tgz", + "integrity": "sha512-BG5bAZMIt7qxc9iqAOCk2zm7V0+yNQLwp+WhsWVkP25Nvd1klqKpScE1lGwoLA27ygxEi+8IRU3wa8PLrhs0DQ==", + "license": "Apache-2.0", + "dependencies": { + "@matrixai/async-cancellable": "^1.1.1", + "@matrixai/errors": "^1.1.7" + } + }, "node_modules/@mongodb-js/saslprep": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", @@ -6063,32 +6748,35 @@ } }, "node_modules/@node-saml/node-saml": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-4.0.5.tgz", - "integrity": "sha512-J5DglElbY1tjOuaR1NPtjOXkXY5bpUhDoKVoeucYN98A3w4fwgjIOPqIGcb6cQsqFq2zZ6vTCeKn5C/hvefSaw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-5.0.1.tgz", + "integrity": "sha512-YQzFPEC+CnsfO9AFYnwfYZKIzOLx3kITaC1HrjHVLTo6hxcQhc+LgHODOMvW4VCV95Gwrz1MshRUWCPzkDqmnA==", + "license": "MIT", "dependencies": { - "@types/debug": "^4.1.7", - "@types/passport": "^1.0.11", - "@types/xml-crypto": "^1.4.2", - "@types/xml-encryption": "^1.2.1", - "@types/xml2js": "^0.4.11", - "@xmldom/xmldom": "^0.8.6", + "@types/debug": "^4.1.12", + "@types/qs": "^6.9.11", + "@types/xml-encryption": "^1.2.4", + "@types/xml2js": "^0.4.14", + "@xmldom/is-dom-node": "^1.0.1", + "@xmldom/xmldom": "^0.8.10", "debug": "^4.3.4", - "xml-crypto": "^3.0.1", + "xml-crypto": "^6.0.1", "xml-encryption": "^3.0.2", - "xml2js": "^0.5.0", - "xmlbuilder": "^15.1.1" + "xml2js": "^0.6.2", + "xmlbuilder": "^15.1.1", + "xpath": "^0.0.34" }, "engines": { - "node": ">= 14" + "node": ">= 18" } }, "node_modules/@node-saml/node-saml/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -6099,25 +6787,43 @@ } } }, - "node_modules/@node-saml/node-saml/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==" + "node_modules/@node-saml/node-saml/node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/@node-saml/node-saml/node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } }, "node_modules/@node-saml/passport-saml": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@node-saml/passport-saml/-/passport-saml-4.0.4.tgz", - "integrity": "sha512-xFw3gw0yo+K1mzlkW15NeBF7cVpRHN/4vpjmBKzov5YFImCWh/G0LcTZ8krH3yk2/eRPc3Or8LRPudVJBjmYaw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@node-saml/passport-saml/-/passport-saml-5.0.1.tgz", + "integrity": "sha512-fMztg3zfSnjLEgxvpl6HaDMNeh0xeQX4QHiF9e2Lsie2dc4qFE37XYbQZhVmn8XJ2awPpSWLQ736UskYgGU8lQ==", + "license": "MIT", "dependencies": { - "@node-saml/node-saml": "^4.0.4", - "@types/express": "^4.17.14", - "@types/passport": "^1.0.11", - "@types/passport-strategy": "^0.2.35", - "passport": "^0.6.0", + "@node-saml/node-saml": "^5.0.1", + "@types/express": "^4.17.21", + "@types/passport": "^1.0.16", + "@types/passport-strategy": "^0.2.38", + "passport": "^0.7.0", "passport-strategy": "^1.0.0" }, "engines": { - "node": ">= 14" + "node": ">= 18" } }, "node_modules/@nodelib/fs.scandir": { @@ -6155,6 +6861,79 @@ "node": ">= 8" } }, + "node_modules/@npmcli/agent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", + "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@npmcli/agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@npmcli/agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/@octokit/auth-app": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-7.1.1.tgz", @@ -6804,15 +7583,343 @@ "resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.1.0.tgz", "integrity": "sha512-y92CpG4kFFtBBjni8LHoV12IegJ+KFxLgKRengrVjKmGE5XMeCuGvlfRe75lTRrgXaG6XIWJlFpIDTlkoJsU8w==" }, + "node_modules/@octopusdeploy/api-client": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@octopusdeploy/api-client/-/api-client-3.4.1.tgz", + "integrity": "sha512-j6FRgDNzc6AQoT3CAguYLWxoMR4W5TKCT1BCPpqjEN9mknmdMSKfYORs3djn/Yj/BhqtITTydDpBoREbzKY5+g==", + "license": "Apache-2.0", + "dependencies": { + "adm-zip": "^0.5.9", + "axios": "^1.2.1", + "form-data": "^4.0.0", + "glob": "^8.0.3", + "lodash": "^4.17.21", + "semver": "^7.3.8", + "urijs": "^1.19.11" + } + }, "node_modules/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", - "license": "Apache-2.0", "engines": { "node": ">=8.0.0" } }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.55.0.tgz", + "integrity": "sha512-3cpa+qI45VHYcA5c0bHM6VHo9gicv3p5mlLHNG3rLyjQU8b7e0st1rWtrUn3JbZ3DwwCfhKop4eQ9UuYlC6Pkg==", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/core": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.28.0.tgz", + "integrity": "sha512-ZLwRMV+fNDpVmF2WYUdBHlq0eOWtEaUJSusrzjGnBt7iSRvfjFE3RXYUZJrqou/wIDWV0DwQ5KIfYe9WXg9Xqw==", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.55.0.tgz", + "integrity": "sha512-3MqDNZzgXmLaiVo9gs9kCw/zPEaZYKIT0+jeMWscWHL/jrA9BNArTOYWUHEPabAQmWQ2BbvgNC7yzlqjoynQwA==", + "dependencies": { + "@opentelemetry/core": "1.28.0", + "@opentelemetry/otlp-exporter-base": "0.55.0", + "@opentelemetry/otlp-transformer": "0.55.0", + "@opentelemetry/resources": "1.28.0", + "@opentelemetry/sdk-metrics": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.55.0.tgz", + "integrity": "sha512-ECybJ4Lh/k+6Dhpq5PTRwF5dgwk241mXvMuLJZRiYN7CNJDWK7wGasQUqT0qhi3IFKJiKVq8WZKWSBmR6MSBfg==", + "dependencies": { + "@opentelemetry/core": "1.28.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.55.0", + "@opentelemetry/otlp-exporter-base": "0.55.0", + "@opentelemetry/otlp-transformer": "0.55.0", + "@opentelemetry/resources": "1.28.0", + "@opentelemetry/sdk-metrics": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.55.0.tgz", + "integrity": "sha512-huHo4Fw9W2jlMu67EKXTY1DMSzQepmEDTTElPBTJ/2qcdlrFFhuz+neJW9cQ7M7Db8qd7I5bpNdxObCn4ZEjnA==", + "dependencies": { + "@opentelemetry/core": "1.28.0", + "@opentelemetry/resources": "1.28.0", + "@opentelemetry/sdk-metrics": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.55.0.tgz", + "integrity": "sha512-YDCMlaQRZkziLL3t6TONRgmmGxDx6MyQDXRD0dknkkgUZtOK5+8MWft1OXzmNu6XfBOdT12MKN5rz+jHUkafKQ==", + "dependencies": { + "@opentelemetry/api-logs": "0.55.0", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http": { + "version": "0.57.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.57.2.tgz", + "integrity": "sha512-1Uz5iJ9ZAlFOiPuwYg29Bf7bJJc/GeoeJIFKJYQf67nTVKFe8RHbEtxgkOmK4UGZNHKXcpW4P8cWBYzBn1USpg==", + "dependencies": { + "@opentelemetry/core": "1.30.1", + "@opentelemetry/instrumentation": "0.57.2", + "@opentelemetry/semantic-conventions": "1.28.0", + "forwarded-parse": "2.1.2", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/api-logs": { + "version": "0.57.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.57.2.tgz", + "integrity": "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/core": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz", + "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/instrumentation": { + "version": "0.57.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz", + "integrity": "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==", + "dependencies": { + "@opentelemetry/api-logs": "0.57.2", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.55.0.tgz", + "integrity": "sha512-iHQI0Zzq3h1T6xUJTVFwmFl5Dt5y1es+fl4kM+k5T/3YvmVyeYkSiF+wHCg6oKrlUAJfk+t55kaAu3sYmt7ZYA==", + "dependencies": { + "@opentelemetry/core": "1.28.0", + "@opentelemetry/otlp-transformer": "0.55.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.55.0.tgz", + "integrity": "sha512-kVqEfxtp6mSN2Dhpy0REo1ghP4PYhC1kMHQJ2qVlO99Pc+aigELjZDfg7/YKmL71gR6wVGIeJfiql/eXL7sQPA==", + "dependencies": { + "@opentelemetry/api-logs": "0.55.0", + "@opentelemetry/core": "1.28.0", + "@opentelemetry/resources": "1.28.0", + "@opentelemetry/sdk-logs": "0.55.0", + "@opentelemetry/sdk-metrics": "1.28.0", + "@opentelemetry/sdk-trace-base": "1.28.0", + "protobufjs": "^7.3.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.28.0.tgz", + "integrity": "sha512-cIyXSVJjGeTICENN40YSvLDAq4Y2502hGK3iN7tfdynQLKWb3XWZQEkPc+eSx47kiy11YeFAlYkEfXwR1w8kfw==", + "dependencies": { + "@opentelemetry/core": "1.28.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.55.0.tgz", + "integrity": "sha512-TSx+Yg/d48uWW6HtjS1AD5x6WPfLhDWLl/WxC7I2fMevaiBuKCuraxTB8MDXieCNnBI24bw9ytyXrDCswFfWgA==", + "dependencies": { + "@opentelemetry/api-logs": "0.55.0", + "@opentelemetry/core": "1.28.0", + "@opentelemetry/resources": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.28.0.tgz", + "integrity": "sha512-43tqMK/0BcKTyOvm15/WQ3HLr0Vu/ucAl/D84NO7iSlv6O4eOprxSHa3sUtmYkaZWHqdDJV0AHVz/R6u4JALVQ==", + "dependencies": { + "@opentelemetry/core": "1.28.0", + "@opentelemetry/resources": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.28.0.tgz", + "integrity": "sha512-ceUVWuCpIao7Y5xE02Xs3nQi0tOGmMea17ecBdwtCvdo9ekmO+ijc9RFDgfifMl7XCBf41zne/1POM3LqSTZDA==", + "dependencies": { + "@opentelemetry/core": "1.28.0", + "@opentelemetry/resources": "1.28.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@otplib/core": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/core/-/core-12.0.1.tgz", + "integrity": "sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==" + }, + "node_modules/@otplib/plugin-crypto": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/plugin-crypto/-/plugin-crypto-12.0.1.tgz", + "integrity": "sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==", + "dependencies": { + "@otplib/core": "^12.0.1" + } + }, + "node_modules/@otplib/plugin-thirty-two": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/plugin-thirty-two/-/plugin-thirty-two-12.0.1.tgz", + "integrity": "sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==", + "dependencies": { + "@otplib/core": "^12.0.1", + "thirty-two": "^1.0.2" + } + }, + "node_modules/@otplib/preset-default": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/preset-default/-/preset-default-12.0.1.tgz", + "integrity": "sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==", + "dependencies": { + "@otplib/core": "^12.0.1", + "@otplib/plugin-crypto": "^12.0.1", + "@otplib/plugin-thirty-two": "^12.0.1" + } + }, + "node_modules/@otplib/preset-v11": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/preset-v11/-/preset-v11-12.0.1.tgz", + "integrity": "sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==", + "dependencies": { + "@otplib/core": "^12.0.1", + "@otplib/plugin-crypto": "^12.0.1", + "@otplib/plugin-thirty-two": "^12.0.1" + } + }, "node_modules/@peculiar/asn1-cms": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.3.8.tgz", @@ -7076,6 +8183,60 @@ "node": ">= 6" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.24.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", @@ -7449,6 +8610,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-4.0.0.tgz", "integrity": "sha512-Wz7QYfPAlG/DR+DfABddUZeNgoeY7d1J39OCR2jR+v7VBsB8ezulDK5szTnDDPDwLH5IWhLvXIHlCFZV7MSKgA==", + "license": "MIT", "dependencies": { "@types/node": ">=18.0.0" }, @@ -7458,12 +8620,13 @@ } }, "node_modules/@slack/oauth": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@slack/oauth/-/oauth-3.0.1.tgz", - "integrity": "sha512-TuR9PI6bYKX6qHC7FQI4keMnhj45TNfSNQtTU3mtnHUX4XLM2dYLvRkUNADyiLTle2qu2rsOQtCIsZJw6H0sDA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@slack/oauth/-/oauth-3.0.2.tgz", + "integrity": "sha512-MdPS8AP9n3u/hBeqRFu+waArJLD/q+wOSZ48ktMTwxQLc6HJyaWPf8soqAyS/b0D6IlvI5TxAdyRyyv3wQ5IVw==", + "license": "MIT", "dependencies": { "@slack/logger": "^4", - "@slack/web-api": "^7.3.4", + "@slack/web-api": "^7.8.0", "@types/jsonwebtoken": "^9", "@types/node": ">=18", "jsonwebtoken": "^9", @@ -7475,24 +8638,26 @@ } }, "node_modules/@slack/types": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/@slack/types/-/types-2.12.0.tgz", - "integrity": "sha512-yFewzUomYZ2BYaGJidPuIgjoYj5wqPDmi7DLSaGIkf+rCi4YZ2Z3DaiYIbz7qb/PL2NmamWjCvB7e9ArI5HkKg==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@slack/types/-/types-2.14.0.tgz", + "integrity": "sha512-n0EGm7ENQRxlXbgKSrQZL69grzg1gHLAVd+GlRVQJ1NSORo0FrApR7wql/gaKdu2n4TO83Sq/AmeUOqD60aXUA==", + "license": "MIT", "engines": { "node": ">= 12.13.0", "npm": ">= 6.12.0" } }, "node_modules/@slack/web-api": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.3.4.tgz", - "integrity": "sha512-KwLK8dlz2lhr3NO7kbYQ7zgPTXPKrhq1JfQc0etJ0K8LSJhYYnf8GbVznvgDT/Uz1/pBXfFQnoXjrQIOKAdSuw==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.8.0.tgz", + "integrity": "sha512-d4SdG+6UmGdzWw38a4sN3lF/nTEzsDxhzU13wm10ejOpPehtmRoqBKnPztQUfFiWbNvSb4czkWYJD4kt+5+Fuw==", + "license": "MIT", "dependencies": { "@slack/logger": "^4.0.0", "@slack/types": "^2.9.0", "@types/node": ">=18.0.0", "@types/retry": "0.12.0", - "axios": "^1.7.4", + "axios": "^1.7.8", "eventemitter3": "^5.0.1", "form-data": "^4.0.0", "is-electron": "2.2.2", @@ -7510,6 +8675,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", "engines": { "node": ">=8" }, @@ -8448,18 +9614,6 @@ "optional": true, "peer": true }, - "node_modules/@team-plain/typescript-sdk": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@team-plain/typescript-sdk/-/typescript-sdk-4.6.1.tgz", - "integrity": "sha512-Uy9QJXu9U7bJb6WXL9sArGk7FXPpzdqBd6q8tAF1vexTm8fbTJRqcikTKxGtZmNADt+C2SapH3cApM4oHpO4lQ==", - "dependencies": { - "@graphql-typed-document-node/core": "^3.2.0", - "ajv": "^8.12.0", - "ajv-formats": "^2.1.1", - "graphql": "^16.6.0", - "zod": "3.22.4" - } - }, "node_modules/@techteamer/ocsp": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@techteamer/ocsp/-/ocsp-1.0.1.tgz", @@ -8547,6 +9701,7 @@ "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", "dependencies": { "@types/ms": "*" } @@ -8666,16 +9821,18 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, "node_modules/@types/ms": { - "version": "0.7.34", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", - "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" }, "node_modules/@types/node": { - "version": "20.9.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.5.tgz", - "integrity": "sha512-Uq2xbNq0chGg+/WQEU0LJTSs/1nKxz6u1iemLcGomkSnKokbW1fbLqc3HOqCf2JP7KjlL4QkS7oZZTrOQHQYgQ==", + "version": "20.17.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz", + "integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==", + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/node-fetch": { @@ -8830,6 +9987,17 @@ "integrity": "sha512-Yll76ZHikRFCyz/pffKGjrCwe/le2CDwOP5F210KQo27kpRE46U2rDnzikNlVn6/ezH3Mhn46bJMTfeVTtcYMg==", "dev": true }, + "node_modules/@types/pkcs11js": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/pkcs11js/-/pkcs11js-1.0.4.tgz", + "integrity": "sha512-Pkq8VbwZZv7o/6ODFOhxw0s0M8J4ucg4/I4V1dSCn8tUwWgIKIYzuV4Pp2fYuir81DgQXAF5TpGyhBMjJ3FjFw==", + "deprecated": "This is a stub types definition for pkcs11js (https://github.com/PeculiarVentures/pkcs11js). pkcs11js provides its own type definitions, so you don't need @types/pkcs11js installed!", + "dev": true, + "license": "MIT", + "dependencies": { + "pkcs11js": "*" + } + }, "node_modules/@types/prompt-sync": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/@types/prompt-sync/-/prompt-sync-4.2.3.tgz", @@ -8837,9 +10005,10 @@ "dev": true }, "node_modules/@types/qs": { - "version": "6.9.10", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", - "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==" + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", @@ -8896,7 +10065,8 @@ "node_modules/@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" }, "node_modules/@types/safe-regex": { "version": "1.1.6", @@ -8929,6 +10099,11 @@ "@types/node": "*" } }, + "node_modules/@types/shimmer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz", + "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==" + }, "node_modules/@types/sjcl": { "version": "1.0.34", "resolved": "https://registry.npmjs.org/@types/sjcl/-/sjcl-1.0.34.tgz", @@ -8948,6 +10123,12 @@ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "optional": true + }, "node_modules/@types/tunnel": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz", @@ -8976,19 +10157,11 @@ "@types/webidl-conversions": "*" } }, - "node_modules/@types/xml-crypto": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@types/xml-crypto/-/xml-crypto-1.4.6.tgz", - "integrity": "sha512-A6jEW2FxLZo1CXsRWnZHUX2wzR3uDju2Bozt6rDbSmU/W8gkilaVbwFEVN0/NhnUdMVzwYobWtM6bU1QJJFb7Q==", - "dependencies": { - "@types/node": "*", - "xpath": "0.0.27" - } - }, "node_modules/@types/xml-encryption": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@types/xml-encryption/-/xml-encryption-1.2.4.tgz", "integrity": "sha512-I69K/WW1Dv7j6O3jh13z0X8sLWJRXbu5xnHDl9yHzUNDUBtUoBY058eb5s+x/WG6yZC1h8aKdI2EoyEPjyEh+Q==", + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -8997,6 +10170,7 @@ "version": "0.4.14", "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -9440,10 +10614,20 @@ "url": "https://opencollective.com/vitest" } }, + "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", + "integrity": "sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q==", + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "license": "MIT", "engines": { "node": ">=10.0.0" } @@ -9692,7 +10876,6 @@ "version": "8.11.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -9700,6 +10883,14 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "peerDependencies": { + "acorn": "^8" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -10319,9 +11510,10 @@ } }, "node_modules/axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -10689,14 +11881,6 @@ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" }, - "node_modules/buffer-writer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", - "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", - "engines": { - "node": ">=4" - } - }, "node_modules/bullmq": { "version": "5.4.2", "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.4.2.tgz", @@ -10753,6 +11937,115 @@ "node": ">=8" } }, + "node_modules/cacache": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", + "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/cacache/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==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -10772,6 +12065,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "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==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -10908,6 +12213,11 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/cjs-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==" + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -11239,9 +12549,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -11251,6 +12561,66 @@ "node": ">= 8" } }, + "node_modules/crypto-randomuuid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-randomuuid/-/crypto-randomuuid-1.0.0.tgz", + "integrity": "sha512-/RC5F4l1SCqD/jazwUF6+t34Cd8zTSAGZ7rvvZu1whZUhD2a5MOGKjSGowoGcpj/cbVZk1ZODIooJEQQq3nNAA==" + }, + "node_modules/cssstyle": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.2.1.tgz", + "integrity": "sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==", + "dependencies": { + "@asamuzakjp/css-color": "^2.8.2", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.1.tgz", + "integrity": "sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==", + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/dateformat": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", @@ -11259,6 +12629,81 @@ "node": "*" } }, + "node_modules/dc-polyfill": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/dc-polyfill/-/dc-polyfill-0.1.6.tgz", + "integrity": "sha512-UV33cugmCC49a5uWAApM+6Ev9ZdvIUMTrtCO9fj96TPGOQiea54oeO3tiEVdVeo3J9N2UdJEmbS4zOkkEA35uQ==", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/dd-trace": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/dd-trace/-/dd-trace-5.40.0.tgz", + "integrity": "sha512-/UYVCcgpZ9LnnUvIJcNfd1Hj51i8HhqLOn9PCj5gK3wJUn6MY/ie/5da2ZaFtoK2DKQ9OZmFBITLV3+KDl4pjA==", + "hasInstallScript": true, + "dependencies": { + "@datadog/libdatadog": "^0.4.0", + "@datadog/native-appsec": "8.4.0", + "@datadog/native-iast-rewriter": "2.8.0", + "@datadog/native-iast-taint-tracking": "3.3.0", + "@datadog/native-metrics": "^3.1.0", + "@datadog/pprof": "5.5.1", + "@datadog/sketches-js": "^2.1.0", + "@isaacs/ttlcache": "^1.4.1", + "@opentelemetry/api": ">=1.0.0 <1.9.0", + "@opentelemetry/core": "^1.14.0", + "crypto-randomuuid": "^1.0.0", + "dc-polyfill": "^0.1.4", + "ignore": "^5.2.4", + "import-in-the-middle": "1.11.2", + "istanbul-lib-coverage": "3.2.0", + "jest-docblock": "^29.7.0", + "koalas": "^1.0.2", + "limiter": "1.1.5", + "lodash.sortby": "^4.7.0", + "lru-cache": "^7.14.0", + "module-details-from-path": "^1.0.3", + "opentracing": ">=0.12.1", + "path-to-regexp": "^0.1.12", + "pprof-format": "^2.1.0", + "protobufjs": "^7.2.5", + "retry": "^0.13.1", + "rfdc": "^1.3.1", + "semifies": "^1.0.0", + "shell-quote": "^1.8.1", + "source-map": "^0.7.4", + "tlhunter-sorted-set": "^0.1.0", + "ttl-set": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/dd-trace/node_modules/@opentelemetry/api": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz", + "integrity": "sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/dd-trace/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/dd-trace/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", @@ -11268,6 +12713,11 @@ "ms": "^2.1.1" } }, + "node_modules/decimal.js": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==" + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -11360,6 +12810,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -11412,6 +12873,14 @@ "node": ">=8" } }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "engines": { + "node": ">=8" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -11454,6 +12923,14 @@ "node": ">=6.0.0" } }, + "node_modules/dompurify": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz", + "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dotenv": { "version": "16.4.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", @@ -11465,6 +12942,19 @@ "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexify": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", @@ -11535,6 +13025,16 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -11556,6 +13056,32 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT" + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -11618,13 +13144,9 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "engines": { "node": ">= 0.4" } @@ -11638,15 +13160,26 @@ "node": ">= 0.4" } }, - "node_modules/es-set-tostringtag": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", - "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", - "dev": true, + "node_modules/es-object-atoms": { + "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==", "dependencies": { - "get-intrinsic": "^1.2.2", - "has-tostringtag": "^1.0.0", - "hasown": "^2.0.0" + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -12279,7 +13812,8 @@ "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" }, "node_modules/events": { "version": "3.3.0", @@ -12318,10 +13852,16 @@ "node": ">=0.10.0" } }, + "node_modules/exponential-backoff": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", + "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==", + "license": "Apache-2.0" + }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -12343,7 +13883,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -12358,6 +13898,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express-session": { @@ -12471,6 +14015,11 @@ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -12892,12 +14441,13 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" }, "engines": { @@ -12912,6 +14462,11 @@ "node": ">= 0.6" } }, + "node_modules/forwarded-parse": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", + "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==" + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -13283,16 +14838,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "license": "MIT", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -13309,6 +14868,18 @@ "node": ">=8.0.0" } }, + "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==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -13506,6 +15077,44 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/google-gax": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.4.1.tgz", + "integrity": "sha512-Phyp9fMfA00J3sZbJxbbB4jC55b7DBjE3F6poyL3wKMEBVKA79q6BGuHcTiM28yOzVql0NDbRL8MLLh8Iwk9Dg==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.10.9", + "@grpc/proto-loader": "^0.7.13", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "google-auth-library": "^9.3.0", + "node-fetch": "^2.7.0", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^2.0.2", + "protobufjs": "^7.3.2", + "retry-request": "^7.0.0", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT" + }, + "node_modules/google-gax/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==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/googleapis": { "version": "137.1.0", "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-137.1.0.tgz", @@ -13535,11 +15144,11 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -13556,14 +15165,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/graphql": { - "version": "16.9.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", - "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==", - "engines": { - "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" - } - }, "node_modules/gtoken": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", @@ -13657,6 +15258,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -13665,9 +15267,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "engines": { "node": ">= 0.4" }, @@ -13676,11 +15278,11 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -13721,9 +15323,9 @@ } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { "function-bind": "^1.1.2" }, @@ -13806,6 +15408,17 @@ "node": ">=14" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/html-entities": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", @@ -13822,6 +15435,12 @@ ], "license": "MIT" }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "license": "BSD-2-Clause" + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -13957,7 +15576,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", - "dev": true, "engines": { "node": ">= 4" } @@ -13990,11 +15608,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-in-the-middle": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.2.tgz", + "integrity": "sha512-gK6Rr6EykBcc6cVWRSBR5TWf8nn6hZMYSRYqCcHa0l0d1fPK7JSYo6+Mlmck76jIX9aL/IZ71c06U2VpFwl1zA==", + "dependencies": { + "acorn": "^8.8.2", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "engines": { "node": ">=0.8.19" } @@ -14027,6 +15655,16 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, + "node_modules/install-artifact-from-github": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/install-artifact-from-github/-/install-artifact-from-github-1.3.5.tgz", + "integrity": "sha512-gZHC7f/cJgXz7MXlHFBxPVMsvIbev1OQN1uKQYKVJDydGNm9oYf9JstbU4Atnh/eSvk41WtEovoRm+8IF686xg==", + "license": "BSD-3-Clause", + "bin": { + "install-from-cache": "bin/install-from-cache.js", + "save-to-github-cache": "bin/save-to-github-cache.js" + } + }, "node_modules/internal-slot": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", @@ -14109,6 +15747,25 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-num": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ip-num/-/ip-num-1.5.1.tgz", + "integrity": "sha512-QziFxgxq3mjIf5CuwlzXFYscHxgLqdEdJKRo2UJ5GurL5zrSRMzT/O+nK0ABimoFH8MWF8YwIiwECYsHc1LpUQ==", + "license": "MIT" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -14245,7 +15902,8 @@ "node_modules/is-electron": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz", - "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==" + "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==", + "license": "MIT" }, "node_modules/is-extglob": { "version": "2.1.1", @@ -14301,6 +15959,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT" + }, "node_modules/is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -14346,6 +16010,11 @@ "node": ">=8" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" + }, "node_modules/is-property": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", @@ -14491,6 +16160,26 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/isomorphic-dompurify": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.22.0.tgz", + "integrity": "sha512-A2xsDNST1yB94rErEnwqlzSvGllCJ4e8lDMe1OWBH2hvpfc/2qzgMEiDshTO1HwO+PIDTiYeOc7ZDB7Ds49BOg==", + "dependencies": { + "dompurify": "^3.2.4", + "jsdom": "^26.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "engines": { + "node": ">=8" + } + }, "node_modules/jackspeak": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", @@ -14509,6 +16198,17 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/jmespath": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", @@ -14560,6 +16260,112 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" }, + "node_modules/jsdom": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.0.0.tgz", + "integrity": "sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.1", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/jsdom/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.1.tgz", + "integrity": "sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==", + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -14857,6 +16663,14 @@ "node": ">=8" } }, + "node_modules/koalas": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/koalas/-/koalas-1.0.2.tgz", + "integrity": "sha512-RYhBbYaTTTHId3l6fnMZc3eGQNW6FVCqMG6AMwA5I1Mafr6AflaXeoi6x3xQuATRotGYRLk6+1ELZH4dstFNOA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", @@ -15098,6 +16912,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -15173,8 +16992,7 @@ "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", - "dev": true + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" }, "node_modules/log-symbols": { "version": "5.1.0", @@ -15290,6 +17108,46 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/make-fetch-happen": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", + "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -15455,6 +17313,125 @@ "node": ">=8" } }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-fetch/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/minizlib": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", @@ -15515,6 +17492,11 @@ "obliterator": "^2.0.1" } }, + "node_modules/module-details-from-path": { + "version": "1.0.3", + "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/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -15771,16 +17753,23 @@ "node": ">=12" } }, + "node_modules/nan": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", + "integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==", + "license": "MIT" + }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -15860,6 +17849,40 @@ } } }, + "node_modules/node-gyp": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.3.1.tgz", + "integrity": "sha512-Pp3nFHBThHzVtNY7U6JfPjvT/DTE8+o/4xKsLQtBoU+j2HLsGlhcfzflAoUreaJbNmYnX+LlLi0qjV8kpyO6xQ==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^13.0.0", + "nopt": "^7.0.0", + "proc-log": "^4.1.0", + "semver": "^7.3.5", + "tar": "^6.2.1", + "which": "^4.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/node-gyp-build": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.9.0.tgz", + "integrity": "sha512-zLcTg6P4AbcHPq465ZMFNXx7XpKKJh+7kkN699NiQWisR2uWYOWNWqRHAmbnmKiL4e9aLSlmy5U7rEMUXV59+A==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-gyp-build-optional-packages": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz", @@ -15871,6 +17894,122 @@ "node-gyp-build-optional-packages-test": "build-test.js" } }, + "node_modules/node-gyp/node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/node-gyp/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==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/node-gyp/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/node-gyp/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/node-gyp/node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -15997,6 +18136,11 @@ "set-blocking": "^2.0.0" } }, + "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/oauth": { "version": "0.9.15", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", @@ -16289,6 +18433,27 @@ "jsonwebtoken": "^9.0.2" } }, + "node_modules/odbc": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/odbc/-/odbc-2.4.9.tgz", + "integrity": "sha512-sHFWOKfyj4oFYds7YBlN+fq9ZjC2J6CsCN5CNMABpKLp+NZdb8bnanb57OaoDy1VFXEOTE91S+F900J/aIPu6w==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.5", + "async": "^3.0.1", + "node-addon-api": "^3.0.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/odbc/node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "license": "MIT" + }, "node_modules/oidc-token-hash": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", @@ -16392,6 +18557,14 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/opentracing": { + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/opentracing/-/opentracing-0.14.7.tgz", + "integrity": "sha512-vz9iS7MJ5+Bp1URw8Khvdyw1H/hGvzHWlKQ7eRrQojSCDL1/SrWfrY9QebLw97n2deyRtzHRC3MkQfVNUCo91Q==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -16440,10 +18613,21 @@ "node": ">=14.6" } }, + "node_modules/otplib": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/otplib/-/otplib-12.0.1.tgz", + "integrity": "sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==", + "dependencies": { + "@otplib/core": "^12.0.1", + "@otplib/preset-default": "^12.0.1", + "@otplib/preset-v11": "^12.0.1" + } + }, "node_modules/p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", "engines": { "node": ">=4" } @@ -16486,10 +18670,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-queue": { "version": "6.6.2", "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" @@ -16504,12 +18704,14 @@ "node_modules/p-queue/node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" }, "node_modules/p-retry": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" @@ -16533,6 +18735,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "license": "MIT", "dependencies": { "p-finally": "^1.0.0" }, @@ -16554,11 +18757,6 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, - "node_modules/packet-reader": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", - "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -16592,6 +18790,17 @@ "node": ">=0.10.0" } }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -16601,9 +18810,10 @@ } }, "node_modules/passport": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", - "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", @@ -16742,9 +18952,9 @@ "license": "ISC" }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, "node_modules/path-type": { @@ -16777,15 +18987,13 @@ "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, "node_modules/pg": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", - "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "version": "8.13.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz", + "integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==", "dependencies": { - "buffer-writer": "2.0.0", - "packet-reader": "1.0.0", - "pg-connection-string": "^2.6.2", - "pg-pool": "^3.6.1", - "pg-protocol": "^1.6.0", + "pg-connection-string": "^2.7.0", + "pg-pool": "^3.7.0", + "pg-protocol": "^1.7.0", "pg-types": "^2.1.0", "pgpass": "1.x" }, @@ -16804,6 +19012,19 @@ } } }, + "node_modules/pg-boss": { + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/pg-boss/-/pg-boss-10.1.5.tgz", + "integrity": "sha512-H87NL6c7N6nTCSCePh16EaSQVSFevNXWdJuzY6PZz4rw+W/nuMKPfI/vYyXS0AdT1g1Q3S3EgeOYOHcB7ZVToQ==", + "dependencies": { + "cron-parser": "^4.9.0", + "pg": "^8.13.0", + "serialize-error": "^8.1.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/pg-cloudflare": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", @@ -16841,17 +19062,17 @@ } }, "node_modules/pg-pool": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", - "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz", + "integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==", "peerDependencies": { "pg": ">=8.0" } }, "node_modules/pg-protocol": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", - "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz", + "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==" }, "node_modules/pg-query-stream": { "version": "4.5.3", @@ -16880,9 +19101,9 @@ } }, "node_modules/pg/node_modules/pg-connection-string": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", - "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==" }, "node_modules/pgpass": { "version": "1.0.5", @@ -17066,6 +19287,20 @@ "node": ">= 6" } }, + "node_modules/pkcs11js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/pkcs11js/-/pkcs11js-2.1.6.tgz", + "integrity": "sha512-+t5jxzB749q8GaEd1yNx3l98xYuaVK6WW/Vjg1Mk1Iy5bMu/A5W4O/9wZGrpOknWF6lFQSb12FXX+eSNxdriwA==", + "hasInstallScript": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/PeculiarVentures" + } + }, "node_modules/pkg-conf": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-3.1.0.tgz", @@ -17290,6 +19525,11 @@ "node": ">=15.0.0" } }, + "node_modules/pprof-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pprof-format/-/pprof-format-2.1.0.tgz", + "integrity": "sha512-0+G5bHH0RNr8E5hoZo/zJYsL92MhkZjwrHp3O2IxmY8RJL9ooKeuZ8Tm0ZNBw5sGZ9TiM71sthTjWoR2Vf5/xw==" + }, "node_modules/prebuild-install": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", @@ -17522,6 +19762,15 @@ "real-require": "^0.2.0" } }, + "node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -17550,6 +19799,28 @@ "node": ">=0.4.0" } }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promise-retry/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/prompt-sync": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/prompt-sync/-/prompt-sync-4.2.0.tgz", @@ -17580,6 +19851,41 @@ "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", + "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", + "license": "Apache-2.0", + "dependencies": { + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -17786,6 +20092,18 @@ "node": ">=0.10.0" } }, + "node_modules/re2": { + "version": "1.21.4", + "resolved": "https://registry.npmjs.org/re2/-/re2-1.21.4.tgz", + "integrity": "sha512-MVIfXWJmsP28mRsSt8HeL750ifb8H5+oF2UDIxGaiJCr8fkMqhLZ7kcX9ADRk2dC8qeGKedB7UVYRfBVpEiLfA==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "install-artifact-from-github": "^1.3.5", + "nan": "^2.20.0", + "node-gyp": "^10.2.0" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -17975,7 +20293,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -17988,6 +20305,35 @@ "node": ">=0.10.0" } }, + "node_modules/require-in-the-middle": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.4.0.tgz", + "integrity": "sha512-X34iHADNbNDfr6OTStIAHWSAvvKQRYgLO6duASaVf7J2VA3lvmNYboAHOuLC2huav1IwgZJtyEcJCKVzFxOSMQ==", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/require-in-the-middle/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -18078,9 +20424,9 @@ } }, "node_modules/rfdc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" }, "node_modules/rimraf": { "version": "5.0.5", @@ -18191,6 +20537,11 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -18305,6 +20656,17 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scim-patch": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/scim-patch/-/scim-patch-0.8.3.tgz", @@ -18323,11 +20685,6 @@ "undici-types": "~6.19.2" } }, - "node_modules/scim-patch/node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" - }, "node_modules/scim2-parse-filter": { "version": "0.2.10", "resolved": "https://registry.npmjs.org/scim2-parse-filter/-/scim2-parse-filter-0.2.10.tgz", @@ -18338,6 +20695,11 @@ "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" }, + "node_modules/semifies": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semifies/-/semifies-1.0.0.tgz", + "integrity": "sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==" + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -18417,6 +20779,20 @@ "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" }, + "node_modules/serialize-error": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", @@ -18509,6 +20885,22 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", + "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" + }, "node_modules/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", @@ -18633,6 +21025,16 @@ "node": ">=8" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/smee-client": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/smee-client/-/smee-client-2.0.0.tgz", @@ -18836,6 +21238,60 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/socks": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/sonic-boom": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.7.0.tgz", @@ -18891,6 +21347,27 @@ "node": ">= 0.6" } }, + "node_modules/ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ssri/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -19284,6 +21761,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, "node_modules/synckit": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", @@ -19526,6 +22008,14 @@ "node": ">=0.8" } }, + "node_modules/thirty-two": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/thirty-two/-/thirty-two-1.0.2.tgz", + "integrity": "sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==", + "engines": { + "node": ">=0.2.6" + } + }, "node_modules/thread-stream": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz", @@ -19566,6 +22056,27 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "6.1.82", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.82.tgz", + "integrity": "sha512-KCTjNL9F7j8MzxgfTgjT+v21oYH38OidFty7dH00maWANAI2IsLw2AnThtTJi9HKALHZKQQWnNebYheadacD+g==", + "dependencies": { + "tldts-core": "^6.1.82" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.82", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.82.tgz", + "integrity": "sha512-Jabl32m21tt/d/PbDO88R43F8aY98Piiz6BVH9ShUlOAiiAELhEqwrAmBocjAqnCfoUeIsRU+h3IEzZd318F3w==" + }, + "node_modules/tlhunter-sorted-set": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/tlhunter-sorted-set/-/tlhunter-sorted-set-0.1.0.tgz", + "integrity": "sha512-eGYW4bjf1DtrHzUYxYfAcSytpOkA44zsr7G2n3PV7yOUR23vmkGe3LL4R+1jL9OsXtbsFOwe8XtbCrabeaEFnw==" + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -19636,6 +22147,17 @@ "node": "*" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -19671,6 +22193,15 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-custom-error": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.2.2.tgz", + "integrity": "sha512-u0YCNf2lf6T/vHm+POKZK1yFKWpSpJitcUN3HxqyEcFuNnHIDbyuIQC7QDy/PsBX3giFyk9rt6BFqBAh2lsDZQ==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -19761,9 +22292,10 @@ } }, "node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", + "license": "0BSD" }, "node_modules/tsup": { "version": "8.0.1", @@ -20364,6 +22896,14 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, + "node_modules/ttl-set": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ttl-set/-/ttl-set-1.0.0.tgz", + "integrity": "sha512-2fuHn/UR+8Z9HK49r97+p2Ru1b5Eewg2QqPrU14BVCQ9QoyU3+vLLZk2WEiyZ9sgJh6W8G1cZr9I2NBLywAHrA==", + "dependencies": { + "fast-fifo": "^1.3.2" + } + }, "node_modules/tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", @@ -20422,7 +22962,6 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, "engines": { "node": ">=10" }, @@ -20587,9 +23126,9 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", @@ -20631,6 +23170,30 @@ "node": ">=4" } }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "license": "ISC", + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/universal-github-app-jwt": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.0.tgz", @@ -20705,6 +23268,12 @@ "punycode": "^2.1.0" } }, + "node_modules/urijs": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", + "license": "MIT" + }, "node_modules/url": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", @@ -21511,11 +24080,41 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -21808,47 +24407,77 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "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/xml-crypto": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-3.2.0.tgz", - "integrity": "sha512-qVurBUOQrmvlgmZqIVBqmb06TD2a/PpEUfFPgD7BuBfjmoH4zgkqaWSIJrnymlCvM2GGt9x+XtJFA+ttoAufqg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-6.0.1.tgz", + "integrity": "sha512-v05aU7NS03z4jlZ0iZGRFeZsuKO1UfEbbYiaeRMiATBFs6Jq9+wqKquEMTn4UTrYZ9iGD8yz3KT4L9o2iF682w==", + "license": "MIT", "dependencies": { - "@xmldom/xmldom": "^0.8.8", - "xpath": "0.0.32" + "@xmldom/is-dom-node": "^1.0.1", + "@xmldom/xmldom": "^0.8.10", + "xpath": "^0.0.33" }, "engines": { - "node": ">=4.0.0" + "node": ">=16" } }, "node_modules/xml-crypto/node_modules/xpath": { - "version": "0.0.32", - "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.32.tgz", - "integrity": "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==", + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.33.tgz", + "integrity": "sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA==", + "license": "MIT", "engines": { "node": ">=0.6.0" } }, "node_modules/xml-encryption": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/xml-encryption/-/xml-encryption-3.0.2.tgz", - "integrity": "sha512-VxYXPvsWB01/aqVLd6ZMPWZ+qaj0aIdF+cStrVJMcFj3iymwZeI0ABzB3VqMYv48DkSpRhnrXqTUkR34j+UDyg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/xml-encryption/-/xml-encryption-3.1.0.tgz", + "integrity": "sha512-PV7qnYpoAMXbf1kvQkqMScLeQpjCMixddAKq9PtqVrho8HnYbBOWNfG0kA4R7zxQDo7w9kiYAyzS/ullAyO55Q==", + "license": "MIT", "dependencies": { "@xmldom/xmldom": "^0.8.5", "escape-html": "^1.0.3", "xpath": "0.0.32" - }, - "engines": { - "node": ">=12" } }, "node_modules/xml-encryption/node_modules/xpath": { "version": "0.0.32", "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.32.tgz", "integrity": "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==", + "license": "MIT", "engines": { "node": ">=0.6.0" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "engines": { + "node": ">=18" + } + }, "node_modules/xml2js": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", @@ -21873,14 +24502,21 @@ "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "license": "MIT", "engines": { "node": ">=8.0" } }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, "node_modules/xpath": { - "version": "0.0.27", - "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz", - "integrity": "sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ==", + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.34.tgz", + "integrity": "sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA==", + "license": "MIT", "engines": { "node": ">=0.6.0" } @@ -21897,7 +24533,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "engines": { "node": ">=10" } diff --git a/backend/package.json b/backend/package.json index 1336478a1..b2c0d751a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -40,27 +40,38 @@ "type:check": "tsc --noEmit", "lint:fix": "eslint --fix --ext js,ts ./src", "lint": "eslint 'src/**/*.ts'", + "test:unit": "vitest run -c vitest.unit.config.ts", "test:e2e": "vitest run -c vitest.e2e.config.ts --bail=1", "test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1", "test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts", "generate:component": "tsx ./scripts/create-backend-file.ts", "generate:schema": "tsx ./scripts/generate-schema-types.ts && eslint --fix --ext ts ./src/db/schemas", - "auditlog-migration:latest": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:latest", - "auditlog-migration:up": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:up", - "auditlog-migration:down": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:down", - "auditlog-migration:list": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:list", - "auditlog-migration:status": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:status", - "auditlog-migration:rollback": "knex --knexfile ./src/db/auditlog-knexfile.ts migrate:rollback", + "auditlog-migration:latest": "node ./dist/db/rename-migrations-to-mjs.mjs && knex --knexfile ./dist/db/auditlog-knexfile.mjs --client pg migrate:latest", + "auditlog-migration:up": "knex --knexfile ./dist/db/auditlog-knexfile.mjs --client pg migrate:up", + "auditlog-migration:down": "knex --knexfile ./dist/db/auditlog-knexfile.mjs --client pg migrate:down", + "auditlog-migration:list": "knex --knexfile ./dist/db/auditlog-knexfile.mjs --client pg migrate:list", + "auditlog-migration:status": "knex --knexfile ./dist/db/auditlog-knexfile.mjs --client pg migrate:status", + "auditlog-migration:unlock": "knex --knexfile ./dist/db/auditlog-knexfile.mjs migrate:unlock", + "auditlog-migration:rollback": "knex --knexfile ./dist/db/auditlog-knexfile.mjs migrate:rollback", "migration:new": "tsx ./scripts/create-migration.ts", - "migration:up": "npm run auditlog-migration:up && knex --knexfile ./src/db/knexfile.ts --client pg migrate:up", - "migration:down": "npm run auditlog-migration:down && knex --knexfile ./src/db/knexfile.ts --client pg migrate:down", - "migration:list": "npm run auditlog-migration:list && knex --knexfile ./src/db/knexfile.ts --client pg migrate:list", - "migration:latest": "npm run auditlog-migration:latest && knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest", - "migration:status": "npm run auditlog-migration:status && knex --knexfile ./src/db/knexfile.ts --client pg migrate:status", - "migration:rollback": "npm run auditlog-migration:rollback && knex --knexfile ./src/db/knexfile.ts migrate:rollback", + "migration:up": "npm run auditlog-migration:up && knex --knexfile ./dist/db/knexfile.mjs --client pg migrate:up", + "migration:down": "npm run auditlog-migration:down && knex --knexfile ./dist/db/knexfile.mjs --client pg migrate:down", + "migration:list": "npm run auditlog-migration:list && knex --knexfile ./dist/db/knexfile.mjs --client pg migrate:list", + "migration:latest": "node ./dist/db/rename-migrations-to-mjs.mjs && npm run auditlog-migration:latest && knex --knexfile ./dist/db/knexfile.mjs --client pg migrate:latest", + "migration:status": "npm run auditlog-migration:status && knex --knexfile ./dist/db/knexfile.mjs --client pg migrate:status", + "migration:rollback": "npm run auditlog-migration:rollback && knex --knexfile ./dist/db/knexfile.mjs migrate:rollback", + "migration:unlock": "npm run auditlog-migration:unlock && knex --knexfile ./dist/db/knexfile.mjs migrate:unlock", + "migration:up-dev": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:up", + "migration:down-dev": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:down", + "migration:list-dev": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:list", + "migration:latest-dev": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest", + "migration:status-dev": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:status", + "migration:rollback-dev": "knex --knexfile ./src/db/knexfile.ts migrate:rollback", + "migration:unlock-dev": "knex --knexfile ./src/db/knexfile.ts migrate:unlock", "migrate:org": "tsx ./scripts/migrate-organization.ts", "seed:new": "tsx ./scripts/create-seed-file.ts", - "seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run", + "seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run", + "seed-dev": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run", "db:reset": "npm run migration:rollback -- --all && npm run migration:latest" }, "keywords": [], @@ -78,12 +89,13 @@ "@types/jsrp": "^0.2.6", "@types/libsodium-wrappers": "^0.7.13", "@types/lodash.isequal": "^4.5.8", - "@types/node": "^20.9.5", + "@types/node": "^20.17.30", "@types/nodemailer": "^6.4.14", "@types/passport-github": "^1.1.12", "@types/passport-google-oauth20": "^2.0.14", "@types/pg": "^8.10.9", "@types/picomatch": "^2.3.3", + "@types/pkcs11js": "^1.0.4", "@types/prompt-sync": "^4.2.3", "@types/resolve": "^1.20.6", "@types/safe-regex": "^1.1.6", @@ -126,24 +138,36 @@ "@fastify/etag": "^5.1.0", "@fastify/formbody": "^7.4.0", "@fastify/helmet": "^11.1.1", - "@fastify/multipart": "8.3.0", + "@fastify/multipart": "8.3.1", "@fastify/passport": "^2.4.0", "@fastify/rate-limit": "^9.0.0", + "@fastify/request-context": "^5.1.0", "@fastify/session": "^10.7.0", + "@fastify/static": "^7.0.4", "@fastify/swagger": "^8.14.0", "@fastify/swagger-ui": "^2.1.0", - "@node-saml/passport-saml": "^4.0.4", + "@google-cloud/kms": "^4.5.0", + "@infisical/quic": "^1.0.8", + "@node-saml/passport-saml": "^5.0.1", "@octokit/auth-app": "^7.1.1", "@octokit/plugin-retry": "^5.0.5", "@octokit/rest": "^20.0.2", "@octokit/webhooks-types": "^7.3.1", + "@octopusdeploy/api-client": "^3.4.1", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.55.0", + "@opentelemetry/exporter-prometheus": "^0.55.0", + "@opentelemetry/instrumentation": "^0.55.0", + "@opentelemetry/instrumentation-http": "^0.57.2", + "@opentelemetry/resources": "^1.28.0", + "@opentelemetry/sdk-metrics": "^1.28.0", + "@opentelemetry/semantic-conventions": "^1.27.0", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/x509": "^1.12.1", "@serdnam/pino-cloudwatch-transport": "^1.0.4", "@sindresorhus/slugify": "1.1.0", - "@slack/oauth": "^3.0.1", - "@slack/web-api": "^7.3.4", - "@team-plain/typescript-sdk": "^4.6.1", + "@slack/oauth": "^3.0.2", + "@slack/web-api": "^7.8.0", "@ucast/mongo2js": "^1.3.4", "ajv": "^8.12.0", "argon2": "^0.31.2", @@ -155,6 +179,7 @@ "cassandra-driver": "^4.7.2", "connect-redis": "^7.1.1", "cron": "^3.1.7", + "dd-trace": "^5.40.0", "dotenv": "^16.4.1", "fastify": "^4.28.1", "fastify-plugin": "^4.5.1", @@ -163,6 +188,7 @@ "handlebars": "^4.7.8", "hdb": "^0.19.10", "ioredis": "^5.3.2", + "isomorphic-dompurify": "^2.22.0", "jmespath": "^0.16.0", "jsonwebtoken": "^9.0.2", "jsrp": "^0.2.4", @@ -175,22 +201,27 @@ "mongodb": "^6.8.1", "ms": "^2.1.3", "mysql2": "^3.9.8", - "nanoid": "^3.3.4", + "nanoid": "^3.3.8", "nodemailer": "^6.9.9", + "odbc": "^2.4.9", "openid-client": "^5.6.5", "ora": "^7.0.1", "oracledb": "^6.4.0", + "otplib": "^12.0.1", "passport-github": "^1.1.0", "passport-gitlab2": "^5.0.0", "passport-google-oauth20": "^2.0.0", "passport-ldapauth": "^3.0.1", "pg": "^8.11.3", + "pg-boss": "^10.1.5", "pg-query-stream": "^4.5.3", "picomatch": "^3.0.1", "pino": "^8.16.2", + "pkcs11js": "^2.1.6", "pkijs": "^3.2.4", "posthog-node": "^3.6.2", "probot": "^13.3.8", + "re2": "^1.21.4", "safe-regex": "^2.1.1", "scim-patch": "^0.8.3", "scim2-parse-filter": "^0.2.10", diff --git a/backend/scripts/migrate-organization.ts b/backend/scripts/migrate-organization.ts index ca6aa904d..2d171de98 100644 --- a/backend/scripts/migrate-organization.ts +++ b/backend/scripts/migrate-organization.ts @@ -8,61 +8,80 @@ const prompt = promptSync({ sigint: true }); +const sanitizeInputParam = (value: string) => { + // Escape double quotes and wrap the entire value in double quotes + if (value) { + return `"${value.replace(/"/g, '\\"')}"`; + } + return '""'; +}; + const exportDb = () => { - const exportHost = prompt("Enter your Postgres Host to migrate from: "); - const exportPort = prompt("Enter your Postgres Port to migrate from [Default = 5432]: ") ?? "5432"; - const exportUser = prompt("Enter your Postgres User to migrate from: [Default = infisical]: ") ?? "infisical"; - const exportPassword = prompt("Enter your Postgres Password to migrate from: "); - const exportDatabase = prompt("Enter your Postgres Database to migrate from [Default = infisical]: ") ?? "infisical"; + const exportHost = sanitizeInputParam(prompt("Enter your Postgres Host to migrate from: ")); + const exportPort = sanitizeInputParam( + prompt("Enter your Postgres Port to migrate from [Default = 5432]: ") ?? "5432" + ); + const exportUser = sanitizeInputParam( + prompt("Enter your Postgres User to migrate from: [Default = infisical]: ") ?? "infisical" + ); + const exportPassword = sanitizeInputParam(prompt("Enter your Postgres Password to migrate from: ")); + const exportDatabase = sanitizeInputParam( + prompt("Enter your Postgres Database to migrate from [Default = infisical]: ") ?? "infisical" + ); // we do not include the audit_log and secret_sharing entries execSync( - `PGDATABASE="${exportDatabase}" PGPASSWORD="${exportPassword}" PGHOST="${exportHost}" PGPORT=${exportPort} PGUSER=${exportUser} pg_dump infisical --exclude-table-data="secret_sharing" --exclude-table-data="audit_log*" > ${path.join( + `PGDATABASE=${exportDatabase} PGPASSWORD=${exportPassword} PGHOST=${exportHost} PGPORT=${exportPort} PGUSER=${exportUser} pg_dump -Fc infisical --exclude-table-data="secret_sharing" --exclude-table-data="audit_log*" > ${path.join( __dirname, - "../src/db/dump.sql" + "../src/db/backup.dump" )}`, { stdio: "inherit" } ); }; const importDbForOrg = () => { - const importHost = prompt("Enter your Postgres Host to migrate to: "); - const importPort = prompt("Enter your Postgres Port to migrate to [Default = 5432]: ") ?? "5432"; - const importUser = prompt("Enter your Postgres User to migrate to: [Default = infisical]: ") ?? "infisical"; - const importPassword = prompt("Enter your Postgres Password to migrate to: "); - const importDatabase = prompt("Enter your Postgres Database to migrate to [Default = infisical]: ") ?? "infisical"; - const orgId = prompt("Enter the organization ID to migrate: "); + const importHost = sanitizeInputParam(prompt("Enter your Postgres Host to migrate to: ")); + const importPort = sanitizeInputParam(prompt("Enter your Postgres Port to migrate to [Default = 5432]: ") ?? "5432"); + const importUser = sanitizeInputParam( + prompt("Enter your Postgres User to migrate to: [Default = infisical]: ") ?? "infisical" + ); + const importPassword = sanitizeInputParam(prompt("Enter your Postgres Password to migrate to: ")); + const importDatabase = sanitizeInputParam( + prompt("Enter your Postgres Database to migrate to [Default = infisical]: ") ?? "infisical" + ); + const orgId = sanitizeInputParam(prompt("Enter the organization ID to migrate: ")); - if (!existsSync(path.join(__dirname, "../src/db/dump.sql"))) { + if (!existsSync(path.join(__dirname, "../src/db/backup.dump"))) { console.log("File not found, please export the database first."); return; } execSync( - `PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -f ${path.join( + `PGDATABASE=${importDatabase} PGPASSWORD=${importPassword} PGHOST=${importHost} PGPORT=${importPort} PGUSER=${importUser} pg_restore -d ${importDatabase} --verbose ${path.join( __dirname, - "../src/db/dump.sql" - )}` + "../src/db/backup.dump" + )}`, + { maxBuffer: 1024 * 1024 * 4096 } ); execSync( - `PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c "DELETE FROM public.organizations WHERE id != '${orgId}'"` + `PGDATABASE=${importDatabase} PGPASSWORD=${importPassword} PGHOST=${importHost} PGPORT=${importPort} PGUSER=${importUser} psql -c "DELETE FROM public.organizations WHERE id != '${orgId}'"` ); // delete global/instance-level resources not relevant to the organization to migrate // users execSync( - `PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c 'DELETE FROM users WHERE users.id NOT IN (SELECT org_memberships."userId" FROM org_memberships)'` + `PGDATABASE=${importDatabase} PGPASSWORD=${importPassword} PGHOST=${importHost} PGPORT=${importPort} PGUSER=${importUser} psql -c 'DELETE FROM users WHERE users.id NOT IN (SELECT org_memberships."userId" FROM org_memberships)'` ); // identities execSync( - `PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c 'DELETE FROM identities WHERE id NOT IN (SELECT "identityId" FROM identity_org_memberships)'` + `PGDATABASE=${importDatabase} PGPASSWORD=${importPassword} PGHOST=${importHost} PGPORT=${importPort} PGUSER=${importUser} psql -c 'DELETE FROM identities WHERE id NOT IN (SELECT "identityId" FROM identity_org_memberships)'` ); // reset slack configuration in superAdmin execSync( - `PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c 'UPDATE super_admin SET "encryptedSlackClientId" = null, "encryptedSlackClientSecret" = null'` + `PGDATABASE=${importDatabase} PGPASSWORD=${importPassword} PGHOST=${importHost} PGPORT=${importPort} PGUSER=${importUser} psql -c 'UPDATE super_admin SET "encryptedSlackClientId" = null, "encryptedSlackClientSecret" = null'` ); console.log("Organization migrated successfully."); diff --git a/backend/src/@types/fastify-zod.d.ts b/backend/src/@types/fastify-zod.d.ts index 393579391..440e3393f 100644 --- a/backend/src/@types/fastify-zod.d.ts +++ b/backend/src/@types/fastify-zod.d.ts @@ -1,6 +1,6 @@ import { FastifyInstance, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault } from "fastify"; -import { Logger } from "pino"; +import { CustomLogger } from "@app/lib/logger/logger"; import { ZodTypeProvider } from "@app/server/plugins/fastify-zod"; declare global { @@ -8,7 +8,7 @@ declare global { RawServerDefault, RawRequestDefaultExpression, RawReplyDefaultExpression, - Readonly, + Readonly, ZodTypeProvider >; diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index d79c224e5..441d3ce4c 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -1,5 +1,7 @@ import "fastify"; +import { Redis } from "ioredis"; + import { TUsers } from "@app/db/schemas"; import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service"; import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service"; @@ -11,9 +13,13 @@ import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/ import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service"; import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service"; import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service"; +import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TGroupServiceFactory } from "@app/ee/services/group/group-service"; import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service"; +import { TKmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal"; +import { TKmipOperationServiceFactory } from "@app/ee/services/kmip/kmip-operation-service"; +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"; @@ -27,11 +33,16 @@ import { TScimServiceFactory } from "@app/ee/services/scim/scim-service"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service"; import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service"; +import { TSecretRotationV2ServiceFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-service"; import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; +import { TSshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh-certificate-authority-service"; +import { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service"; +import { TSshHostServiceFactory } from "@app/ee/services/ssh-host/ssh-host-service"; import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service"; import { TAuthMode } from "@app/server/plugins/auth/inject-identity"; import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service"; +import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; import { TAuthLoginFactory } from "@app/services/auth/auth-login-service"; import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service"; import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service"; @@ -44,11 +55,13 @@ import { TCmekServiceFactory } from "@app/services/cmek/cmek-service"; import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service"; import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service"; import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service"; +import { THsmServiceFactory } from "@app/services/hsm/hsm-service"; import { TIdentityServiceFactory } from "@app/services/identity/identity-service"; import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service"; import { TIdentityAzureAuthServiceFactory } from "@app/services/identity-azure-auth/identity-azure-auth-service"; import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service"; +import { TIdentityJwtAuthServiceFactory } from "@app/services/identity-jwt-auth/identity-jwt-auth-service"; import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service"; import { TIdentityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service"; import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service"; @@ -73,18 +86,37 @@ import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret- import { TSecretImportServiceFactory } from "@app/services/secret-import/secret-import-service"; import { TSecretReplicationServiceFactory } from "@app/services/secret-replication/secret-replication-service"; import { TSecretSharingServiceFactory } from "@app/services/secret-sharing/secret-sharing-service"; +import { TSecretSyncServiceFactory } from "@app/services/secret-sync/secret-sync-service"; import { TSecretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service"; import { TServiceTokenServiceFactory } from "@app/services/service-token/service-token-service"; import { TSlackServiceFactory } from "@app/services/slack/slack-service"; import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service"; import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service"; +import { TTotpServiceFactory } from "@app/services/totp/totp-service"; import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserServiceFactory } from "@app/services/user/user-service"; import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service"; import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service"; import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service"; +declare module "@fastify/request-context" { + interface RequestContextData { + reqId: string; + identityAuthInfo?: { + identityId: string; + oidc?: { + claims: Record; + }; + }; + identityPermissionMetadata?: Record; // filled by permission service + } +} + declare module "fastify" { + interface Session { + callbackPort: string; + } + interface FastifyRequest { realIp: string; // used for mfa session authentication @@ -104,15 +136,21 @@ declare module "fastify" { rateLimits: RateLimitConfiguration; // passport data passportUser: { - isUserCompleted: string; + isUserCompleted: boolean; providerAuthToken: string; }; + kmipUser: { + projectId: string; + clientId: string; + name: string; + }; auditLogInfo: Pick; ssoConfig: Awaited>; ldapConfig: Awaited>; } interface FastifyInstance { + redis: Redis; services: { login: TAuthLoginFactory; password: TAuthPasswordFactory; @@ -153,6 +191,7 @@ declare module "fastify" { identityAwsAuth: TIdentityAwsAuthServiceFactory; identityAzureAuth: TIdentityAzureAuthServiceFactory; identityOidcAuth: TIdentityOidcAuthServiceFactory; + identityJwtAuth: TIdentityJwtAuthServiceFactory; accessApprovalPolicy: TAccessApprovalPolicyServiceFactory; accessApprovalRequest: TAccessApprovalRequestServiceFactory; secretApprovalPolicy: TSecretApprovalPolicyServiceFactory; @@ -166,6 +205,9 @@ declare module "fastify" { auditLogStream: TAuditLogStreamServiceFactory; certificate: TCertificateServiceFactory; certificateTemplate: TCertificateTemplateServiceFactory; + sshCertificateAuthority: TSshCertificateAuthorityServiceFactory; + sshCertificateTemplate: TSshCertificateTemplateServiceFactory; + sshHost: TSshHostServiceFactory; certificateAuthority: TCertificateAuthorityServiceFactory; certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory; certificateEst: TCertificateEstServiceFactory; @@ -184,6 +226,7 @@ declare module "fastify" { rateLimit: TRateLimitServiceFactory; userEngagement: TUserEngagementServiceFactory; externalKms: TExternalKmsServiceFactory; + hsm: THsmServiceFactory; orgAdmin: TOrgAdminServiceFactory; slack: TSlackServiceFactory; workflowIntegration: TWorkflowIntegrationServiceFactory; @@ -191,11 +234,19 @@ declare module "fastify" { migration: TExternalMigrationServiceFactory; externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory; projectTemplate: TProjectTemplateServiceFactory; + totp: TTotpServiceFactory; + appConnection: TAppConnectionServiceFactory; + secretSync: TSecretSyncServiceFactory; + kmip: TKmipServiceFactory; + kmipOperation: TKmipOperationServiceFactory; + gateway: TGatewayServiceFactory; + secretRotationV2: TSecretRotationV2ServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer store: { user: Pick; + kmipClient: Pick; }; } } diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 9cfb78dd7..dc0e5ee67 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -17,6 +17,9 @@ import { TApiKeys, TApiKeysInsert, TApiKeysUpdate, + TAppConnections, + TAppConnectionsInsert, + TAppConnectionsUpdate, TAuditLogs, TAuditLogsInsert, TAuditLogStreams, @@ -65,9 +68,15 @@ import { TDynamicSecrets, TDynamicSecretsInsert, TDynamicSecretsUpdate, + TExternalGroupOrgRoleMappings, + TExternalGroupOrgRoleMappingsInsert, + TExternalGroupOrgRoleMappingsUpdate, TExternalKms, TExternalKmsInsert, TExternalKmsUpdate, + TGateways, + TGatewaysInsert, + TGatewaysUpdate, TGitAppInstallSessions, TGitAppInstallSessionsInsert, TGitAppInstallSessionsUpdate, @@ -98,6 +107,9 @@ import { TIdentityGcpAuths, TIdentityGcpAuthsInsert, TIdentityGcpAuthsUpdate, + TIdentityJwtAuths, + TIdentityJwtAuthsInsert, + TIdentityJwtAuthsUpdate, TIdentityKubernetesAuths, TIdentityKubernetesAuthsInsert, TIdentityKubernetesAuthsUpdate, @@ -140,6 +152,18 @@ import { TInternalKms, TInternalKmsInsert, TInternalKmsUpdate, + TKmipClientCertificates, + TKmipClientCertificatesInsert, + TKmipClientCertificatesUpdate, + TKmipClients, + TKmipClientsInsert, + TKmipClientsUpdate, + TKmipOrgConfigs, + TKmipOrgConfigsInsert, + TKmipOrgConfigsUpdate, + TKmipOrgServerCertificates, + TKmipOrgServerCertificatesInsert, + TKmipOrgServerCertificatesUpdate, TKmsKeys, TKmsKeysInsert, TKmsKeysUpdate, @@ -164,6 +188,9 @@ import { TOrgBots, TOrgBotsInsert, TOrgBotsUpdate, + TOrgGatewayConfig, + TOrgGatewayConfigInsert, + TOrgGatewayConfigUpdate, TOrgMemberships, TOrgMembershipsInsert, TOrgMembershipsUpdate, @@ -185,6 +212,9 @@ import { TProjectEnvironments, TProjectEnvironmentsInsert, TProjectEnvironmentsUpdate, + TProjectGateways, + TProjectGatewaysInsert, + TProjectGatewaysUpdate, TProjectKeys, TProjectKeysInsert, TProjectKeysUpdate, @@ -199,6 +229,12 @@ import { TProjectSlackConfigs, TProjectSlackConfigsInsert, TProjectSlackConfigsUpdate, + TProjectSplitBackfillIds, + TProjectSplitBackfillIdsInsert, + TProjectSplitBackfillIdsUpdate, + TProjectSshConfigs, + TProjectSshConfigsInsert, + TProjectSshConfigsUpdate, TProjectsUpdate, TProjectTemplates, TProjectTemplatesInsert, @@ -212,6 +248,9 @@ import { TRateLimit, TRateLimitInsert, TRateLimitUpdate, + TResourceMetadata, + TResourceMetadataInsert, + TResourceMetadataUpdate, TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate, @@ -269,6 +308,12 @@ import { TSecretRotations, TSecretRotationsInsert, TSecretRotationsUpdate, + TSecretRotationsV2, + TSecretRotationsV2Insert, + TSecretRotationsV2Update, + TSecretRotationV2SecretMappings, + TSecretRotationV2SecretMappingsInsert, + TSecretRotationV2SecretMappingsUpdate, TSecrets, TSecretScanningGitRisks, TSecretScanningGitRisksInsert, @@ -290,15 +335,27 @@ import { TSecretSnapshotsInsert, TSecretSnapshotsUpdate, TSecretsUpdate, + TSecretsV2, + TSecretsV2Insert, + TSecretsV2Update, + TSecretSyncs, + TSecretSyncsInsert, + TSecretSyncsUpdate, TSecretTagJunction, TSecretTagJunctionInsert, TSecretTagJunctionUpdate, TSecretTags, TSecretTagsInsert, TSecretTagsUpdate, + TSecretV2TagJunction, + TSecretV2TagJunctionInsert, + TSecretV2TagJunctionUpdate, TSecretVersions, TSecretVersionsInsert, TSecretVersionsUpdate, + TSecretVersionsV2, + TSecretVersionsV2Insert, + TSecretVersionsV2Update, TSecretVersionTagJunction, TSecretVersionTagJunctionInsert, TSecretVersionTagJunctionUpdate, @@ -311,9 +368,36 @@ import { TSlackIntegrations, TSlackIntegrationsInsert, TSlackIntegrationsUpdate, + TSshCertificateAuthorities, + TSshCertificateAuthoritiesInsert, + TSshCertificateAuthoritiesUpdate, + TSshCertificateAuthoritySecrets, + TSshCertificateAuthoritySecretsInsert, + TSshCertificateAuthoritySecretsUpdate, + TSshCertificateBodies, + TSshCertificateBodiesInsert, + TSshCertificateBodiesUpdate, + TSshCertificates, + TSshCertificatesInsert, + TSshCertificatesUpdate, + TSshCertificateTemplates, + TSshCertificateTemplatesInsert, + TSshCertificateTemplatesUpdate, + TSshHostLoginUserMappings, + TSshHostLoginUserMappingsInsert, + TSshHostLoginUserMappingsUpdate, + TSshHostLoginUsers, + TSshHostLoginUsersInsert, + TSshHostLoginUsersUpdate, + TSshHosts, + TSshHostsInsert, + TSshHostsUpdate, TSuperAdmin, TSuperAdminInsert, TSuperAdminUpdate, + TTotpConfigs, + TTotpConfigsInsert, + TTotpConfigsUpdate, TTrustedIps, TTrustedIpsInsert, TTrustedIpsUpdate, @@ -339,22 +423,6 @@ import { TWorkflowIntegrationsInsert, TWorkflowIntegrationsUpdate } from "@app/db/schemas"; -import { - TExternalGroupOrgRoleMappings, - TExternalGroupOrgRoleMappingsInsert, - TExternalGroupOrgRoleMappingsUpdate -} from "@app/db/schemas/external-group-org-role-mappings"; -import { - TSecretV2TagJunction, - TSecretV2TagJunctionInsert, - TSecretV2TagJunctionUpdate -} from "@app/db/schemas/secret-v2-tag-junction"; -import { - TSecretVersionsV2, - TSecretVersionsV2Insert, - TSecretVersionsV2Update -} from "@app/db/schemas/secret-versions-v2"; -import { TSecretsV2, TSecretsV2Insert, TSecretsV2Update } from "@app/db/schemas/secrets-v2"; declare module "knex" { namespace Knex { @@ -369,6 +437,42 @@ declare module "knex/types/tables" { interface Tables { [TableName.Users]: KnexOriginal.CompositeTableType; [TableName.Groups]: KnexOriginal.CompositeTableType; + [TableName.SshHost]: KnexOriginal.CompositeTableType; + [TableName.SshCertificateAuthority]: KnexOriginal.CompositeTableType< + TSshCertificateAuthorities, + TSshCertificateAuthoritiesInsert, + TSshCertificateAuthoritiesUpdate + >; + [TableName.SshCertificateAuthoritySecret]: KnexOriginal.CompositeTableType< + TSshCertificateAuthoritySecrets, + TSshCertificateAuthoritySecretsInsert, + TSshCertificateAuthoritySecretsUpdate + >; + [TableName.SshCertificateTemplate]: KnexOriginal.CompositeTableType< + TSshCertificateTemplates, + TSshCertificateTemplatesInsert, + TSshCertificateTemplatesUpdate + >; + [TableName.SshCertificate]: KnexOriginal.CompositeTableType< + TSshCertificates, + TSshCertificatesInsert, + TSshCertificatesUpdate + >; + [TableName.SshCertificateBody]: KnexOriginal.CompositeTableType< + TSshCertificateBodies, + TSshCertificateBodiesInsert, + TSshCertificateBodiesUpdate + >; + [TableName.SshHostLoginUser]: KnexOriginal.CompositeTableType< + TSshHostLoginUsers, + TSshHostLoginUsersInsert, + TSshHostLoginUsersUpdate + >; + [TableName.SshHostLoginUserMapping]: KnexOriginal.CompositeTableType< + TSshHostLoginUserMappings, + TSshHostLoginUserMappingsInsert, + TSshHostLoginUserMappingsUpdate + >; [TableName.CertificateAuthority]: KnexOriginal.CompositeTableType< TCertificateAuthorities, TCertificateAuthoritiesInsert, @@ -473,6 +577,11 @@ declare module "knex/types/tables" { [TableName.SuperAdmin]: KnexOriginal.CompositeTableType; [TableName.ApiKey]: KnexOriginal.CompositeTableType; [TableName.Project]: KnexOriginal.CompositeTableType; + [TableName.ProjectSshConfig]: KnexOriginal.CompositeTableType< + TProjectSshConfigs, + TProjectSshConfigsInsert, + TProjectSshConfigsUpdate + >; [TableName.ProjectMembership]: KnexOriginal.CompositeTableType< TProjectMemberships, TProjectMembershipsInsert, @@ -587,6 +696,11 @@ declare module "knex/types/tables" { TIdentityOidcAuthsInsert, TIdentityOidcAuthsUpdate >; + [TableName.IdentityJwtAuth]: KnexOriginal.CompositeTableType< + TIdentityJwtAuths, + TIdentityJwtAuthsInsert, + TIdentityJwtAuthsUpdate + >; [TableName.IdentityUaClientSecret]: KnexOriginal.CompositeTableType< TIdentityUaClientSecrets, TIdentityUaClientSecretsInsert, @@ -826,5 +940,59 @@ declare module "knex/types/tables" { TProjectTemplatesInsert, TProjectTemplatesUpdate >; + [TableName.TotpConfig]: KnexOriginal.CompositeTableType; + [TableName.ProjectSplitBackfillIds]: KnexOriginal.CompositeTableType< + TProjectSplitBackfillIds, + TProjectSplitBackfillIdsInsert, + TProjectSplitBackfillIdsUpdate + >; + [TableName.ResourceMetadata]: KnexOriginal.CompositeTableType< + TResourceMetadata, + TResourceMetadataInsert, + TResourceMetadataUpdate + >; + [TableName.AppConnection]: KnexOriginal.CompositeTableType< + TAppConnections, + TAppConnectionsInsert, + TAppConnectionsUpdate + >; + [TableName.SecretSync]: KnexOriginal.CompositeTableType; + [TableName.KmipClient]: KnexOriginal.CompositeTableType; + [TableName.KmipOrgConfig]: KnexOriginal.CompositeTableType< + TKmipOrgConfigs, + TKmipOrgConfigsInsert, + TKmipOrgConfigsUpdate + >; + [TableName.KmipOrgServerCertificates]: KnexOriginal.CompositeTableType< + TKmipOrgServerCertificates, + TKmipOrgServerCertificatesInsert, + TKmipOrgServerCertificatesUpdate + >; + [TableName.KmipClientCertificates]: KnexOriginal.CompositeTableType< + TKmipClientCertificates, + TKmipClientCertificatesInsert, + TKmipClientCertificatesUpdate + >; + [TableName.Gateway]: KnexOriginal.CompositeTableType; + [TableName.ProjectGateway]: KnexOriginal.CompositeTableType< + TProjectGateways, + TProjectGatewaysInsert, + TProjectGatewaysUpdate + >; + [TableName.OrgGatewayConfig]: KnexOriginal.CompositeTableType< + TOrgGatewayConfig, + TOrgGatewayConfigInsert, + TOrgGatewayConfigUpdate + >; + [TableName.SecretRotationV2]: KnexOriginal.CompositeTableType< + TSecretRotationsV2, + TSecretRotationsV2Insert, + TSecretRotationsV2Update + >; + [TableName.SecretRotationV2SecretMapping]: KnexOriginal.CompositeTableType< + TSecretRotationV2SecretMappings, + TSecretRotationV2SecretMappingsInsert, + TSecretRotationV2SecretMappingsUpdate + >; } } diff --git a/backend/src/auto-start-migrations.ts b/backend/src/auto-start-migrations.ts new file mode 100644 index 000000000..88f6dea69 --- /dev/null +++ b/backend/src/auto-start-migrations.ts @@ -0,0 +1,105 @@ +import path from "node:path"; + +import dotenv from "dotenv"; +import { Knex } from "knex"; +import { Logger } from "pino"; + +import { PgSqlLock } from "./keystore/keystore"; + +dotenv.config(); + +type TArgs = { + auditLogDb?: Knex; + applicationDb: Knex; + logger: Logger; +}; + +const isProduction = process.env.NODE_ENV === "production"; +const migrationConfig = { + directory: path.join(__dirname, "./db/migrations"), + loadExtensions: [".mjs", ".ts"], + tableName: "infisical_migrations" +}; + +const migrationStatusCheckErrorHandler = (err: Error) => { + // happens for first time in which the migration table itself is not created yet + // error: select * from "infisical_migrations" - relation "infisical_migrations" does not exist + if (err?.message?.includes("does not exist")) { + return true; + } + throw err; +}; + +export const runMigrations = async ({ applicationDb, auditLogDb, logger }: TArgs) => { + try { + // akhilmhdh(Feb 10 2025): 2 years from now remove this + if (isProduction) { + const migrationTable = migrationConfig.tableName; + const hasMigrationTable = await applicationDb.schema.hasTable(migrationTable); + if (hasMigrationTable) { + const firstFile = (await applicationDb(migrationTable).where({}).first()) as { name: string }; + if (firstFile?.name?.includes(".ts")) { + await applicationDb(migrationTable).update({ + name: applicationDb.raw("REPLACE(name, '.ts', '.mjs')") + }); + } + } + if (auditLogDb) { + const hasMigrationTableInAuditLog = await auditLogDb.schema.hasTable(migrationTable); + if (hasMigrationTableInAuditLog) { + const firstFile = (await auditLogDb(migrationTable).where({}).first()) as { name: string }; + if (firstFile?.name?.includes(".ts")) { + await auditLogDb(migrationTable).update({ + name: auditLogDb.raw("REPLACE(name, '.ts', '.mjs')") + }); + } + } + } + } + + const shouldRunMigration = Boolean( + await applicationDb.migrate.status(migrationConfig).catch(migrationStatusCheckErrorHandler) + ); // db.length - code.length + if (!shouldRunMigration) { + logger.info("No migrations pending: Skipping migration process."); + return; + } + + if (auditLogDb) { + await auditLogDb.transaction(async (tx) => { + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.BootUpMigration]); + logger.info("Running audit log migrations."); + + const didPreviousInstanceRunMigration = !(await auditLogDb.migrate + .status(migrationConfig) + .catch(migrationStatusCheckErrorHandler)); + if (didPreviousInstanceRunMigration) { + logger.info("No audit log migrations pending: Applied by previous instance. Skipping migration process."); + return; + } + + await auditLogDb.migrate.latest(migrationConfig); + logger.info("Finished audit log migrations."); + }); + } + + await applicationDb.transaction(async (tx) => { + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.BootUpMigration]); + logger.info("Running application migrations."); + + const didPreviousInstanceRunMigration = !(await applicationDb.migrate + .status(migrationConfig) + .catch(migrationStatusCheckErrorHandler)); + if (didPreviousInstanceRunMigration) { + logger.info("No application migrations pending: Applied by previous instance. Skipping migration process."); + return; + } + + await applicationDb.migrate.latest(migrationConfig); + logger.info("Finished application migrations."); + }); + } catch (err) { + logger.error(err, "Boot up migration failed"); + process.exit(1); + } +}; diff --git a/backend/src/db/instance.ts b/backend/src/db/instance.ts index d4a2a5b2c..5a8dd3d05 100644 --- a/backend/src/db/instance.ts +++ b/backend/src/db/instance.ts @@ -49,6 +49,9 @@ export const initDbConnection = ({ ca: Buffer.from(dbRootCert, "base64").toString("ascii") } : false + }, + migrations: { + tableName: "infisical_migrations" } }); @@ -64,6 +67,9 @@ export const initDbConnection = ({ ca: Buffer.from(replicaDbCertificate, "base64").toString("ascii") } : false + }, + migrations: { + tableName: "infisical_migrations" } }); }); @@ -98,6 +104,9 @@ export const initAuditLogDbConnection = ({ ca: Buffer.from(dbRootCert, "base64").toString("ascii") } : false + }, + migrations: { + tableName: "infisical_migrations" } }); diff --git a/backend/src/db/knexfile.ts b/backend/src/db/knexfile.ts index 8af2b59ab..0e51e94e2 100644 --- a/backend/src/db/knexfile.ts +++ b/backend/src/db/knexfile.ts @@ -38,7 +38,8 @@ export default { directory: "./seeds" }, migrations: { - tableName: "infisical_migrations" + tableName: "infisical_migrations", + loadExtensions: [".mjs", ".ts"] } }, production: { @@ -62,7 +63,8 @@ export default { max: 10 }, migrations: { - tableName: "infisical_migrations" + tableName: "infisical_migrations", + loadExtensions: [".mjs", ".ts"] } } } as Knex.Config; diff --git a/backend/src/db/manual-migrations/partition-audit-logs.ts b/backend/src/db/manual-migrations/partition-audit-logs.ts index 382ef0dbf..fbead3a24 100644 --- a/backend/src/db/manual-migrations/partition-audit-logs.ts +++ b/backend/src/db/manual-migrations/partition-audit-logs.ts @@ -16,7 +16,7 @@ const createAuditLogPartition = async (knex: Knex, startDate: Date, endDate: Dat const startDateStr = formatPartitionDate(startDate); const endDateStr = formatPartitionDate(endDate); - const partitionName = `${TableName.AuditLog}_${startDateStr.replace(/-/g, "")}_${endDateStr.replace(/-/g, "")}`; + const partitionName = `${TableName.AuditLog}_${startDateStr.replaceAll("-", "")}_${endDateStr.replaceAll("-", "")}`; await knex.schema.raw( `CREATE TABLE ${partitionName} PARTITION OF ${TableName.AuditLog} FOR VALUES FROM ('${startDateStr}') TO ('${endDateStr}')` diff --git a/backend/src/db/migrations/20240802181855_ca-cert-version.ts b/backend/src/db/migrations/20240802181855_ca-cert-version.ts index 24eca185d..c38a2d42a 100644 --- a/backend/src/db/migrations/20240802181855_ca-cert-version.ts +++ b/backend/src/db/migrations/20240802181855_ca-cert-version.ts @@ -64,23 +64,25 @@ export async function up(knex: Knex): Promise { } if (await knex.schema.hasTable(TableName.Certificate)) { - await knex.schema.alterTable(TableName.Certificate, (t) => { - t.uuid("caCertId").nullable(); - t.foreign("caCertId").references("id").inTable(TableName.CertificateAuthorityCert); - }); + const hasCaCertIdColumn = await knex.schema.hasColumn(TableName.Certificate, "caCertId"); + if (!hasCaCertIdColumn) { + await knex.schema.alterTable(TableName.Certificate, (t) => { + t.uuid("caCertId").nullable(); + t.foreign("caCertId").references("id").inTable(TableName.CertificateAuthorityCert); + }); - await knex.raw(` + await knex.raw(` UPDATE "${TableName.Certificate}" cert SET "caCertId" = ( SELECT caCert.id FROM "${TableName.CertificateAuthorityCert}" caCert WHERE caCert."caId" = cert."caId" - ) - `); + )`); - await knex.schema.alterTable(TableName.Certificate, (t) => { - t.uuid("caCertId").notNullable().alter(); - }); + await knex.schema.alterTable(TableName.Certificate, (t) => { + t.uuid("caCertId").notNullable().alter(); + }); + } } } diff --git a/backend/src/db/migrations/20241014084900_identity-multiple-auth-methods.ts b/backend/src/db/migrations/20241014084900_identity-multiple-auth-methods.ts index 821132e1e..7a6bd3e1f 100644 --- a/backend/src/db/migrations/20241014084900_identity-multiple-auth-methods.ts +++ b/backend/src/db/migrations/20241014084900_identity-multiple-auth-methods.ts @@ -2,7 +2,7 @@ import { Knex } from "knex"; import { TableName } from "../schemas"; -const BATCH_SIZE = 30_000; +const BATCH_SIZE = 10_000; export async function up(knex: Knex): Promise { const hasAuthMethodColumnAccessToken = await knex.schema.hasColumn(TableName.IdentityAccessToken, "authMethod"); @@ -12,7 +12,18 @@ export async function up(knex: Knex): Promise { t.string("authMethod").nullable(); }); - let nullableAccessTokens = await knex(TableName.IdentityAccessToken).whereNull("authMethod").limit(BATCH_SIZE); + // first we remove identities without auth method that is unused + // ! We delete all access tokens where the identity has no auth method set! + // ! Which means un-configured identities that for some reason have access tokens, will have their access tokens deleted. + await knex(TableName.IdentityAccessToken) + .leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityAccessToken}.identityId`) + .whereNull(`${TableName.Identity}.authMethod`) + .delete(); + + let nullableAccessTokens = await knex(TableName.IdentityAccessToken) + .whereNull("authMethod") + .limit(BATCH_SIZE) + .select("id"); let totalUpdated = 0; do { @@ -33,24 +44,15 @@ export async function up(knex: Knex): Promise { }); // eslint-disable-next-line no-await-in-loop - nullableAccessTokens = await knex(TableName.IdentityAccessToken).whereNull("authMethod").limit(BATCH_SIZE); + nullableAccessTokens = await knex(TableName.IdentityAccessToken) + .whereNull("authMethod") + .limit(BATCH_SIZE) + .select("id"); totalUpdated += batchIds.length; console.log(`Updated ${batchIds.length} access tokens in batch <> Total updated: ${totalUpdated}`); } while (nullableAccessTokens.length > 0); - // ! We delete all access tokens where the identity has no auth method set! - // ! Which means un-configured identities that for some reason have access tokens, will have their access tokens deleted. - await knex(TableName.IdentityAccessToken) - .whereNotExists((queryBuilder) => { - void queryBuilder - .select("id") - .from(TableName.Identity) - .whereRaw(`${TableName.IdentityAccessToken}."identityId" = ${TableName.Identity}.id`) - .whereNotNull("authMethod"); - }) - .delete(); - // Finally we set the authMethod to notNullable after populating the column. // This will fail if the data is not populated correctly, so it's safe. await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => { diff --git a/backend/src/db/migrations/20241107112632_skip-bootstrap-cert-validation-est.ts b/backend/src/db/migrations/20241107112632_skip-bootstrap-cert-validation-est.ts new file mode 100644 index 000000000..fbee179b3 --- /dev/null +++ b/backend/src/db/migrations/20241107112632_skip-bootstrap-cert-validation-est.ts @@ -0,0 +1,35 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasDisableBootstrapCertValidationCol = await knex.schema.hasColumn( + TableName.CertificateTemplateEstConfig, + "disableBootstrapCertValidation" + ); + + const hasCaChainCol = await knex.schema.hasColumn(TableName.CertificateTemplateEstConfig, "encryptedCaChain"); + + await knex.schema.alterTable(TableName.CertificateTemplateEstConfig, (t) => { + if (!hasDisableBootstrapCertValidationCol) { + t.boolean("disableBootstrapCertValidation").defaultTo(false).notNullable(); + } + + if (hasCaChainCol) { + t.binary("encryptedCaChain").nullable().alter(); + } + }); +} + +export async function down(knex: Knex): Promise { + const hasDisableBootstrapCertValidationCol = await knex.schema.hasColumn( + TableName.CertificateTemplateEstConfig, + "disableBootstrapCertValidation" + ); + + await knex.schema.alterTable(TableName.CertificateTemplateEstConfig, (t) => { + if (hasDisableBootstrapCertValidationCol) { + t.dropColumn("disableBootstrapCertValidation"); + } + }); +} diff --git a/backend/src/db/migrations/20241110032223_add-missing-oidc-org-cascade-reference.ts b/backend/src/db/migrations/20241110032223_add-missing-oidc-org-cascade-reference.ts new file mode 100644 index 000000000..335917553 --- /dev/null +++ b/backend/src/db/migrations/20241110032223_add-missing-oidc-org-cascade-reference.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.OidcConfig, "orgId")) { + await knex.schema.alterTable(TableName.OidcConfig, (t) => { + t.dropForeign("orgId"); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.OidcConfig, "orgId")) { + await knex.schema.alterTable(TableName.OidcConfig, (t) => { + t.dropForeign("orgId"); + t.foreign("orgId").references("id").inTable(TableName.Organization); + }); + } +} diff --git a/backend/src/db/migrations/20241111175154_kms-root-cfg-hsm.ts b/backend/src/db/migrations/20241111175154_kms-root-cfg-hsm.ts new file mode 100644 index 000000000..501eccb8b --- /dev/null +++ b/backend/src/db/migrations/20241111175154_kms-root-cfg-hsm.ts @@ -0,0 +1,23 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasEncryptionStrategy = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "encryptionStrategy"); + const hasTimestampsCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "createdAt"); + + await knex.schema.alterTable(TableName.KmsServerRootConfig, (t) => { + if (!hasEncryptionStrategy) t.string("encryptionStrategy").defaultTo("SOFTWARE"); + if (!hasTimestampsCol) t.timestamps(true, true, true); + }); +} + +export async function down(knex: Knex): Promise { + const hasEncryptionStrategy = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "encryptionStrategy"); + const hasTimestampsCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "createdAt"); + + await knex.schema.alterTable(TableName.KmsServerRootConfig, (t) => { + if (hasEncryptionStrategy) t.dropColumn("encryptionStrategy"); + if (hasTimestampsCol) t.dropTimestamps(true); + }); +} diff --git a/backend/src/db/migrations/20241112082701_add-totp-support.ts b/backend/src/db/migrations/20241112082701_add-totp-support.ts new file mode 100644 index 000000000..9aefc444c --- /dev/null +++ b/backend/src/db/migrations/20241112082701_add-totp-support.ts @@ -0,0 +1,54 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.TotpConfig))) { + await knex.schema.createTable(TableName.TotpConfig, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.uuid("userId").notNullable(); + t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + t.boolean("isVerified").defaultTo(false).notNullable(); + t.binary("encryptedRecoveryCodes").notNullable(); + t.binary("encryptedSecret").notNullable(); + t.timestamps(true, true, true); + t.unique("userId"); + }); + + await createOnUpdateTrigger(knex, TableName.TotpConfig); + } + + const doesOrgMfaMethodColExist = await knex.schema.hasColumn(TableName.Organization, "selectedMfaMethod"); + await knex.schema.alterTable(TableName.Organization, (t) => { + if (!doesOrgMfaMethodColExist) { + t.string("selectedMfaMethod"); + } + }); + + const doesUserSelectedMfaMethodColExist = await knex.schema.hasColumn(TableName.Users, "selectedMfaMethod"); + await knex.schema.alterTable(TableName.Users, (t) => { + if (!doesUserSelectedMfaMethodColExist) { + t.string("selectedMfaMethod"); + } + }); +} + +export async function down(knex: Knex): Promise { + await dropOnUpdateTrigger(knex, TableName.TotpConfig); + await knex.schema.dropTableIfExists(TableName.TotpConfig); + + const doesOrgMfaMethodColExist = await knex.schema.hasColumn(TableName.Organization, "selectedMfaMethod"); + await knex.schema.alterTable(TableName.Organization, (t) => { + if (doesOrgMfaMethodColExist) { + t.dropColumn("selectedMfaMethod"); + } + }); + + const doesUserSelectedMfaMethodColExist = await knex.schema.hasColumn(TableName.Users, "selectedMfaMethod"); + await knex.schema.alterTable(TableName.Users, (t) => { + if (doesUserSelectedMfaMethodColExist) { + t.dropColumn("selectedMfaMethod"); + } + }); +} diff --git a/backend/src/db/migrations/20241119143026_add-project-descripton.ts b/backend/src/db/migrations/20241119143026_add-project-descripton.ts new file mode 100644 index 000000000..3c78c99e2 --- /dev/null +++ b/backend/src/db/migrations/20241119143026_add-project-descripton.ts @@ -0,0 +1,23 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasProjectDescription = await knex.schema.hasColumn(TableName.Project, "description"); + + if (!hasProjectDescription) { + await knex.schema.alterTable(TableName.Project, (t) => { + t.string("description"); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasProjectDescription = await knex.schema.hasColumn(TableName.Project, "description"); + + if (hasProjectDescription) { + await knex.schema.alterTable(TableName.Project, (t) => { + t.dropColumn("description"); + }); + } +} diff --git a/backend/src/db/migrations/20241121131344_make-identity-metadata-not-nullable-again.ts b/backend/src/db/migrations/20241121131344_make-identity-metadata-not-nullable-again.ts new file mode 100644 index 000000000..fb58c02df --- /dev/null +++ b/backend/src/db/migrations/20241121131344_make-identity-metadata-not-nullable-again.ts @@ -0,0 +1,20 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.IdentityMetadata, "value")) { + await knex(TableName.IdentityMetadata).whereNull("value").delete(); + await knex.schema.alterTable(TableName.IdentityMetadata, (t) => { + t.string("value", 1020).notNullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.IdentityMetadata, "value")) { + await knex.schema.alterTable(TableName.IdentityMetadata, (t) => { + t.string("value", 1020).alter(); + }); + } +} diff --git a/backend/src/db/migrations/20241203165840_allow-disabling-approval-workflows.ts b/backend/src/db/migrations/20241203165840_allow-disabling-approval-workflows.ts new file mode 100644 index 000000000..c7fb6fe39 --- /dev/null +++ b/backend/src/db/migrations/20241203165840_allow-disabling-approval-workflows.ts @@ -0,0 +1,59 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasAccessApprovalPolicyDeletedAtColumn = await knex.schema.hasColumn( + TableName.AccessApprovalPolicy, + "deletedAt" + ); + const hasSecretApprovalPolicyDeletedAtColumn = await knex.schema.hasColumn( + TableName.SecretApprovalPolicy, + "deletedAt" + ); + + if (!hasAccessApprovalPolicyDeletedAtColumn) { + await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => { + t.timestamp("deletedAt"); + }); + } + if (!hasSecretApprovalPolicyDeletedAtColumn) { + await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => { + t.timestamp("deletedAt"); + }); + } + + await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => { + t.dropForeign(["privilegeId"]); + + // Add the new foreign key constraint with ON DELETE SET NULL + t.foreign("privilegeId").references("id").inTable(TableName.ProjectUserAdditionalPrivilege).onDelete("SET NULL"); + }); +} + +export async function down(knex: Knex): Promise { + const hasAccessApprovalPolicyDeletedAtColumn = await knex.schema.hasColumn( + TableName.AccessApprovalPolicy, + "deletedAt" + ); + const hasSecretApprovalPolicyDeletedAtColumn = await knex.schema.hasColumn( + TableName.SecretApprovalPolicy, + "deletedAt" + ); + + if (hasAccessApprovalPolicyDeletedAtColumn) { + await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => { + t.dropColumn("deletedAt"); + }); + } + if (hasSecretApprovalPolicyDeletedAtColumn) { + await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => { + t.dropColumn("deletedAt"); + }); + } + + await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => { + t.dropForeign(["privilegeId"]); + t.foreign("privilegeId").references("id").inTable(TableName.ProjectUserAdditionalPrivilege).onDelete("CASCADE"); + }); +} diff --git a/backend/src/db/migrations/20241209144123_add-identity-jwt-auth.ts b/backend/src/db/migrations/20241209144123_add-identity-jwt-auth.ts new file mode 100644 index 000000000..03594b77c --- /dev/null +++ b/backend/src/db/migrations/20241209144123_add-identity-jwt-auth.ts @@ -0,0 +1,34 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.IdentityJwtAuth))) { + await knex.schema.createTable(TableName.IdentityJwtAuth, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable(); + t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable(); + t.bigInteger("accessTokenNumUsesLimit").defaultTo(0).notNullable(); + t.jsonb("accessTokenTrustedIps").notNullable(); + t.uuid("identityId").notNullable().unique(); + t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE"); + t.string("configurationType").notNullable(); + t.string("jwksUrl").notNullable(); + t.binary("encryptedJwksCaCert").notNullable(); + t.binary("encryptedPublicKeys").notNullable(); + t.string("boundIssuer").notNullable(); + t.string("boundAudiences").notNullable(); + t.jsonb("boundClaims").notNullable(); + t.string("boundSubject").notNullable(); + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.IdentityJwtAuth); + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.IdentityJwtAuth); + await dropOnUpdateTrigger(knex, TableName.IdentityJwtAuth); +} diff --git a/backend/src/db/migrations/20241213122320_add-index-for-secret-version-v2-folder.ts b/backend/src/db/migrations/20241213122320_add-index-for-secret-version-v2-folder.ts new file mode 100644 index 000000000..96e8f08f6 --- /dev/null +++ b/backend/src/db/migrations/20241213122320_add-index-for-secret-version-v2-folder.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.SecretVersionV2, "folderId")) { + await knex.schema.alterTable(TableName.SecretVersionV2, (t) => { + t.index("folderId"); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.SecretVersionV2, "folderId")) { + await knex.schema.alterTable(TableName.SecretVersionV2, (t) => { + t.dropIndex("folderId"); + }); + } +} diff --git a/backend/src/db/migrations/20241213122350_project-split-to-products.ts b/backend/src/db/migrations/20241213122350_project-split-to-products.ts new file mode 100644 index 000000000..d7a00a801 --- /dev/null +++ b/backend/src/db/migrations/20241213122350_project-split-to-products.ts @@ -0,0 +1,297 @@ +import slugify from "@sindresorhus/slugify"; +import { Knex } from "knex"; +import { v4 as uuidV4 } from "uuid"; + +import { alphaNumericNanoId } from "@app/lib/nanoid"; + +import { ProjectType, TableName } from "../schemas"; + +/* eslint-disable no-await-in-loop,@typescript-eslint/ban-ts-comment */ +const newProject = async (knex: Knex, projectId: string, projectType: ProjectType) => { + const newProjectId = uuidV4(); + const project = await knex(TableName.Project).where("id", projectId).first(); + await knex(TableName.Project).insert({ + ...project, + type: projectType, + // @ts-ignore id is required + id: newProjectId, + slug: slugify(`${project?.name}-${alphaNumericNanoId(4)}`) + }); + + const customRoleMapping: Record = {}; + const projectCustomRoles = await knex(TableName.ProjectRoles).where("projectId", projectId); + if (projectCustomRoles.length) { + await knex.batchInsert( + TableName.ProjectRoles, + projectCustomRoles.map((el) => { + const id = uuidV4(); + customRoleMapping[el.id] = id; + return { + ...el, + id, + projectId: newProjectId, + permissions: el.permissions ? JSON.stringify(el.permissions) : el.permissions + }; + }) + ); + } + const groupMembershipMapping: Record = {}; + const groupMemberships = await knex(TableName.GroupProjectMembership).where("projectId", projectId); + if (groupMemberships.length) { + await knex.batchInsert( + TableName.GroupProjectMembership, + groupMemberships.map((el) => { + const id = uuidV4(); + groupMembershipMapping[el.id] = id; + return { ...el, id, projectId: newProjectId }; + }) + ); + } + + const groupMembershipRoles = await knex(TableName.GroupProjectMembershipRole).whereIn( + "projectMembershipId", + groupMemberships.map((el) => el.id) + ); + if (groupMembershipRoles.length) { + await knex.batchInsert( + TableName.GroupProjectMembershipRole, + groupMembershipRoles.map((el) => { + const id = uuidV4(); + const projectMembershipId = groupMembershipMapping[el.projectMembershipId]; + const customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId; + return { ...el, id, projectMembershipId, customRoleId }; + }) + ); + } + + const identityProjectMembershipMapping: Record = {}; + const identities = await knex(TableName.IdentityProjectMembership).where("projectId", projectId); + if (identities.length) { + await knex.batchInsert( + TableName.IdentityProjectMembership, + identities.map((el) => { + const id = uuidV4(); + identityProjectMembershipMapping[el.id] = id; + return { ...el, id, projectId: newProjectId }; + }) + ); + } + + const identitiesRoles = await knex(TableName.IdentityProjectMembershipRole).whereIn( + "projectMembershipId", + identities.map((el) => el.id) + ); + if (identitiesRoles.length) { + await knex.batchInsert( + TableName.IdentityProjectMembershipRole, + identitiesRoles.map((el) => { + const id = uuidV4(); + const projectMembershipId = identityProjectMembershipMapping[el.projectMembershipId]; + const customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId; + return { ...el, id, projectMembershipId, customRoleId }; + }) + ); + } + + const projectMembershipMapping: Record = {}; + const projectUserMembers = await knex(TableName.ProjectMembership).where("projectId", projectId); + if (projectUserMembers.length) { + await knex.batchInsert( + TableName.ProjectMembership, + projectUserMembers.map((el) => { + const id = uuidV4(); + projectMembershipMapping[el.id] = id; + return { ...el, id, projectId: newProjectId }; + }) + ); + } + const membershipRoles = await knex(TableName.ProjectUserMembershipRole).whereIn( + "projectMembershipId", + projectUserMembers.map((el) => el.id) + ); + if (membershipRoles.length) { + await knex.batchInsert( + TableName.ProjectUserMembershipRole, + membershipRoles.map((el) => { + const id = uuidV4(); + const projectMembershipId = projectMembershipMapping[el.projectMembershipId]; + const customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId; + return { ...el, id, projectMembershipId, customRoleId }; + }) + ); + } + + const kmsKeys = await knex(TableName.KmsKey).where("projectId", projectId).andWhere("isReserved", true); + if (kmsKeys.length) { + await knex.batchInsert( + TableName.KmsKey, + kmsKeys.map((el) => { + const id = uuidV4(); + const slug = slugify(alphaNumericNanoId(8).toLowerCase()); + return { ...el, id, slug, projectId: newProjectId }; + }) + ); + } + + const projectBot = await knex(TableName.ProjectBot).where("projectId", projectId).first(); + if (projectBot) { + const newProjectBot = { ...projectBot, id: uuidV4(), projectId: newProjectId }; + await knex(TableName.ProjectBot).insert(newProjectBot); + } + + const projectKeys = await knex(TableName.ProjectKeys).where("projectId", projectId); + if (projectKeys.length) { + await knex.batchInsert( + TableName.ProjectKeys, + projectKeys.map((el) => { + const id = uuidV4(); + return { ...el, id, projectId: newProjectId }; + }) + ); + } + + return newProjectId; +}; + +const BATCH_SIZE = 500; +export async function up(knex: Knex): Promise { + const hasSplitMappingTable = await knex.schema.hasTable(TableName.ProjectSplitBackfillIds); + if (!hasSplitMappingTable) { + await knex.schema.createTable(TableName.ProjectSplitBackfillIds, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("sourceProjectId", 36).notNullable(); + t.foreign("sourceProjectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.string("destinationProjectType").notNullable(); + t.string("destinationProjectId", 36).notNullable(); + t.foreign("destinationProjectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + }); + } + + const hasTypeColumn = await knex.schema.hasColumn(TableName.Project, "type"); + if (!hasTypeColumn) { + await knex.schema.alterTable(TableName.Project, (t) => { + t.string("type"); + }); + + let projectsToBeTyped; + do { + // eslint-disable-next-line no-await-in-loop + projectsToBeTyped = await knex(TableName.Project).whereNull("type").limit(BATCH_SIZE).select("id"); + if (projectsToBeTyped.length) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.Project) + .whereIn( + "id", + projectsToBeTyped.map((el) => el.id) + ) + .update({ type: ProjectType.SecretManager }); + } + } while (projectsToBeTyped.length > 0); + + const projectsWithCertificates = await knex(TableName.CertificateAuthority) + .distinct("projectId") + .select("projectId"); + /* eslint-disable no-await-in-loop,no-param-reassign */ + for (const { projectId } of projectsWithCertificates) { + const newProjectId = await newProject(knex, projectId, ProjectType.CertificateManager); + await knex(TableName.CertificateAuthority).where("projectId", projectId).update({ projectId: newProjectId }); + await knex(TableName.PkiAlert).where("projectId", projectId).update({ projectId: newProjectId }); + await knex(TableName.PkiCollection).where("projectId", projectId).update({ projectId: newProjectId }); + await knex(TableName.ProjectSplitBackfillIds).insert({ + sourceProjectId: projectId, + destinationProjectType: ProjectType.CertificateManager, + destinationProjectId: newProjectId + }); + } + + const projectsWithCmek = await knex(TableName.KmsKey) + .where("isReserved", false) + .whereNotNull("projectId") + .distinct("projectId") + .select("projectId"); + for (const { projectId } of projectsWithCmek) { + if (projectId) { + const newProjectId = await newProject(knex, projectId, ProjectType.KMS); + await knex(TableName.KmsKey) + .where({ + isReserved: false, + projectId + }) + .update({ projectId: newProjectId }); + await knex(TableName.ProjectSplitBackfillIds).insert({ + sourceProjectId: projectId, + destinationProjectType: ProjectType.KMS, + destinationProjectId: newProjectId + }); + } + } + + /* eslint-enable */ + await knex.schema.alterTable(TableName.Project, (t) => { + t.string("type").notNullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasTypeColumn = await knex.schema.hasColumn(TableName.Project, "type"); + const hasSplitMappingTable = await knex.schema.hasTable(TableName.ProjectSplitBackfillIds); + + if (hasTypeColumn && hasSplitMappingTable) { + const splitProjectMappings = await knex(TableName.ProjectSplitBackfillIds).where({}); + const certMapping = splitProjectMappings.filter( + (el) => el.destinationProjectType === ProjectType.CertificateManager + ); + /* eslint-disable no-await-in-loop */ + for (const project of certMapping) { + await knex(TableName.CertificateAuthority) + .where("projectId", project.destinationProjectId) + .update({ projectId: project.sourceProjectId }); + await knex(TableName.PkiAlert) + .where("projectId", project.destinationProjectId) + .update({ projectId: project.sourceProjectId }); + await knex(TableName.PkiCollection) + .where("projectId", project.destinationProjectId) + .update({ projectId: project.sourceProjectId }); + } + + /* eslint-enable */ + const kmsMapping = splitProjectMappings.filter((el) => el.destinationProjectType === ProjectType.KMS); + /* eslint-disable no-await-in-loop */ + for (const project of kmsMapping) { + await knex(TableName.KmsKey) + .where({ + isReserved: false, + projectId: project.destinationProjectId + }) + .update({ projectId: project.sourceProjectId }); + } + /* eslint-enable */ + await knex(TableName.ProjectMembership) + .whereIn( + "projectId", + splitProjectMappings.map((el) => el.destinationProjectId) + ) + .delete(); + await knex(TableName.ProjectRoles) + .whereIn( + "projectId", + splitProjectMappings.map((el) => el.destinationProjectId) + ) + .delete(); + await knex(TableName.Project) + .whereIn( + "id", + splitProjectMappings.map((el) => el.destinationProjectId) + ) + .delete(); + + await knex.schema.alterTable(TableName.Project, (t) => { + t.dropColumn("type"); + }); + } + + if (hasSplitMappingTable) { + await knex.schema.dropTableIfExists(TableName.ProjectSplitBackfillIds); + } +} diff --git a/backend/src/db/migrations/20241216013357_ssh-mgmt.ts b/backend/src/db/migrations/20241216013357_ssh-mgmt.ts new file mode 100644 index 000000000..92831d382 --- /dev/null +++ b/backend/src/db/migrations/20241216013357_ssh-mgmt.ts @@ -0,0 +1,99 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.SshCertificateAuthority))) { + await knex.schema.createTable(TableName.SshCertificateAuthority, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + t.string("projectId").notNullable(); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.string("status").notNullable(); // active / disabled + t.string("friendlyName").notNullable(); + t.string("keyAlgorithm").notNullable(); + }); + await createOnUpdateTrigger(knex, TableName.SshCertificateAuthority); + } + + if (!(await knex.schema.hasTable(TableName.SshCertificateAuthoritySecret))) { + await knex.schema.createTable(TableName.SshCertificateAuthoritySecret, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + t.uuid("sshCaId").notNullable().unique(); + t.foreign("sshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE"); + t.binary("encryptedPrivateKey").notNullable(); + }); + await createOnUpdateTrigger(knex, TableName.SshCertificateAuthoritySecret); + } + + if (!(await knex.schema.hasTable(TableName.SshCertificateTemplate))) { + await knex.schema.createTable(TableName.SshCertificateTemplate, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + t.uuid("sshCaId").notNullable(); + t.foreign("sshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE"); + t.string("status").notNullable(); // active / disabled + t.string("name").notNullable(); + t.string("ttl").notNullable(); + t.string("maxTTL").notNullable(); + t.specificType("allowedUsers", "text[]").notNullable(); + t.specificType("allowedHosts", "text[]").notNullable(); + t.boolean("allowUserCertificates").notNullable(); + t.boolean("allowHostCertificates").notNullable(); + t.boolean("allowCustomKeyIds").notNullable(); + }); + await createOnUpdateTrigger(knex, TableName.SshCertificateTemplate); + } + + if (!(await knex.schema.hasTable(TableName.SshCertificate))) { + await knex.schema.createTable(TableName.SshCertificate, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + t.uuid("sshCaId").notNullable(); + t.foreign("sshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("SET NULL"); + t.uuid("sshCertificateTemplateId"); + t.foreign("sshCertificateTemplateId") + .references("id") + .inTable(TableName.SshCertificateTemplate) + .onDelete("SET NULL"); + t.string("serialNumber").notNullable().unique(); + t.string("certType").notNullable(); // user or host + t.specificType("principals", "text[]").notNullable(); + t.string("keyId").notNullable(); + t.datetime("notBefore").notNullable(); + t.datetime("notAfter").notNullable(); + }); + await createOnUpdateTrigger(knex, TableName.SshCertificate); + } + + if (!(await knex.schema.hasTable(TableName.SshCertificateBody))) { + await knex.schema.createTable(TableName.SshCertificateBody, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + t.uuid("sshCertId").notNullable().unique(); + t.foreign("sshCertId").references("id").inTable(TableName.SshCertificate).onDelete("CASCADE"); + t.binary("encryptedCertificate").notNullable(); + }); + + await createOnUpdateTrigger(knex, TableName.SshCertificateBody); + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.SshCertificateBody); + await dropOnUpdateTrigger(knex, TableName.SshCertificateBody); + + await knex.schema.dropTableIfExists(TableName.SshCertificate); + await dropOnUpdateTrigger(knex, TableName.SshCertificate); + + await knex.schema.dropTableIfExists(TableName.SshCertificateTemplate); + await dropOnUpdateTrigger(knex, TableName.SshCertificateTemplate); + + await knex.schema.dropTableIfExists(TableName.SshCertificateAuthoritySecret); + await dropOnUpdateTrigger(knex, TableName.SshCertificateAuthoritySecret); + + await knex.schema.dropTableIfExists(TableName.SshCertificateAuthority); + await dropOnUpdateTrigger(knex, TableName.SshCertificateAuthority); +} diff --git a/backend/src/db/migrations/20241218165837_resource-metadata.ts b/backend/src/db/migrations/20241218165837_resource-metadata.ts new file mode 100644 index 000000000..af62f9895 --- /dev/null +++ b/backend/src/db/migrations/20241218165837_resource-metadata.ts @@ -0,0 +1,40 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.ResourceMetadata))) { + await knex.schema.createTable(TableName.ResourceMetadata, (tb) => { + tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + tb.string("key").notNullable(); + tb.string("value", 1020).notNullable(); + tb.uuid("orgId").notNullable(); + tb.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + tb.uuid("userId"); + tb.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + tb.uuid("identityId"); + tb.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE"); + tb.uuid("secretId"); + tb.foreign("secretId").references("id").inTable(TableName.SecretV2).onDelete("CASCADE"); + tb.timestamps(true, true, true); + }); + } + + const hasSecretMetadataField = await knex.schema.hasColumn(TableName.SecretApprovalRequestSecretV2, "secretMetadata"); + if (!hasSecretMetadataField) { + await knex.schema.alterTable(TableName.SecretApprovalRequestSecretV2, (t) => { + t.jsonb("secretMetadata"); + }); + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.ResourceMetadata); + + const hasSecretMetadataField = await knex.schema.hasColumn(TableName.SecretApprovalRequestSecretV2, "secretMetadata"); + if (hasSecretMetadataField) { + await knex.schema.alterTable(TableName.SecretApprovalRequestSecretV2, (t) => { + t.dropColumn("secretMetadata"); + }); + } +} diff --git a/backend/src/db/migrations/20241218181018_app-connection.ts b/backend/src/db/migrations/20241218181018_app-connection.ts new file mode 100644 index 000000000..d09907ae1 --- /dev/null +++ b/backend/src/db/migrations/20241218181018_app-connection.ts @@ -0,0 +1,28 @@ +import { Knex } from "knex"; + +import { TableName } from "@app/db/schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "@app/db/utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.AppConnection))) { + await knex.schema.createTable(TableName.AppConnection, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("name", 32).notNullable(); + t.string("description"); + t.string("app").notNullable(); + t.string("method").notNullable(); + t.binary("encryptedCredentials").notNullable(); + t.integer("version").defaultTo(1).notNullable(); + t.uuid("orgId").notNullable(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.AppConnection); + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.AppConnection); + await dropOnUpdateTrigger(knex, TableName.AppConnection); +} diff --git a/backend/src/db/migrations/20250115222458_groups-unique-name.ts b/backend/src/db/migrations/20250115222458_groups-unique-name.ts new file mode 100644 index 000000000..1c40c8cba --- /dev/null +++ b/backend/src/db/migrations/20250115222458_groups-unique-name.ts @@ -0,0 +1,49 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + // find any duplicate group names within organizations + const duplicates = await knex(TableName.Groups) + .select("orgId", "name") + .count("* as count") + .groupBy("orgId", "name") + .having(knex.raw("count(*) > 1")); + + // for each set of duplicates, update all but one with a numbered suffix + for await (const duplicate of duplicates) { + const groups = await knex(TableName.Groups) + .select("id", "name") + .where({ + orgId: duplicate.orgId, + name: duplicate.name + }) + .orderBy("createdAt", "asc"); // keep original name for oldest group + + // skip the first (oldest) group, rename others with numbered suffix + for (let i = 1; i < groups.length; i += 1) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.Groups) + .where("id", groups[i].id) + .update({ + name: `${groups[i].name} (${i})`, + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore TS doesn't know about Knex's timestamp types + updatedAt: new Date() + }); + } + } + + // add the unique constraint + await knex.schema.alterTable(TableName.Groups, (t) => { + t.unique(["orgId", "name"]); + }); +} + +export async function down(knex: Knex): Promise { + // Remove the unique constraint + await knex.schema.alterTable(TableName.Groups, (t) => { + t.dropUnique(["orgId", "name"]); + }); +} diff --git a/backend/src/db/migrations/20250116092245_add-enforce-capitalization-project-flag.ts b/backend/src/db/migrations/20250116092245_add-enforce-capitalization-project-flag.ts new file mode 100644 index 000000000..1cb6644f5 --- /dev/null +++ b/backend/src/db/migrations/20250116092245_add-enforce-capitalization-project-flag.ts @@ -0,0 +1,33 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasEnforceCapitalizationCol = await knex.schema.hasColumn(TableName.Project, "enforceCapitalization"); + const hasAutoCapitalizationCol = await knex.schema.hasColumn(TableName.Project, "autoCapitalization"); + + await knex.schema.alterTable(TableName.Project, (t) => { + if (!hasEnforceCapitalizationCol) { + t.boolean("enforceCapitalization").defaultTo(false).notNullable(); + } + + if (hasAutoCapitalizationCol) { + t.boolean("autoCapitalization").defaultTo(false).alter(); + } + }); +} + +export async function down(knex: Knex): Promise { + const hasEnforceCapitalizationCol = await knex.schema.hasColumn(TableName.Project, "enforceCapitalization"); + const hasAutoCapitalizationCol = await knex.schema.hasColumn(TableName.Project, "autoCapitalization"); + + await knex.schema.alterTable(TableName.Project, (t) => { + if (hasEnforceCapitalizationCol) { + t.dropColumn("enforceCapitalization"); + } + + if (hasAutoCapitalizationCol) { + t.boolean("autoCapitalization").defaultTo(true).alter(); + } + }); +} diff --git a/backend/src/db/migrations/20250122055102_secret-sync.ts b/backend/src/db/migrations/20250122055102_secret-sync.ts new file mode 100644 index 000000000..5f37950e3 --- /dev/null +++ b/backend/src/db/migrations/20250122055102_secret-sync.ts @@ -0,0 +1,50 @@ +import { Knex } from "knex"; + +import { TableName } from "@app/db/schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "@app/db/utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.SecretSync))) { + await knex.schema.createTable(TableName.SecretSync, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("name", 32).notNullable(); + t.string("description"); + t.string("destination").notNullable(); + t.boolean("isAutoSyncEnabled").notNullable().defaultTo(true); + t.integer("version").defaultTo(1).notNullable(); + t.jsonb("destinationConfig").notNullable(); + t.jsonb("syncOptions").notNullable(); + // we're including projectId in addition to folder ID because we allow folderId to be null (if the folder + // is deleted), to preserve sync configuration + t.string("projectId").notNullable(); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.uuid("folderId"); + t.foreign("folderId").references("id").inTable(TableName.SecretFolder).onDelete("SET NULL"); + t.uuid("connectionId").notNullable(); + t.foreign("connectionId").references("id").inTable(TableName.AppConnection); + t.timestamps(true, true, true); + // sync secrets to destination + t.string("syncStatus"); + t.string("lastSyncJobId"); + t.string("lastSyncMessage"); + t.datetime("lastSyncedAt"); + // import secrets from destination + t.string("importStatus"); + t.string("lastImportJobId"); + t.string("lastImportMessage"); + t.datetime("lastImportedAt"); + // remove secrets from destination + t.string("removeStatus"); + t.string("lastRemoveJobId"); + t.string("lastRemoveMessage"); + t.datetime("lastRemovedAt"); + }); + + await createOnUpdateTrigger(knex, TableName.SecretSync); + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.SecretSync); + await dropOnUpdateTrigger(knex, TableName.SecretSync); +} diff --git a/backend/src/db/migrations/20250129214629_oidc-configs-manage-group-memberships-col.ts b/backend/src/db/migrations/20250129214629_oidc-configs-manage-group-memberships-col.ts new file mode 100644 index 000000000..74b866b77 --- /dev/null +++ b/backend/src/db/migrations/20250129214629_oidc-configs-manage-group-memberships-col.ts @@ -0,0 +1,23 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasManageGroupMembershipsCol = await knex.schema.hasColumn(TableName.OidcConfig, "manageGroupMemberships"); + + await knex.schema.alterTable(TableName.OidcConfig, (tb) => { + if (!hasManageGroupMembershipsCol) { + tb.boolean("manageGroupMemberships").notNullable().defaultTo(false); + } + }); +} + +export async function down(knex: Knex): Promise { + const hasManageGroupMembershipsCol = await knex.schema.hasColumn(TableName.OidcConfig, "manageGroupMemberships"); + + await knex.schema.alterTable(TableName.OidcConfig, (t) => { + if (hasManageGroupMembershipsCol) { + t.dropColumn("manageGroupMemberships"); + } + }); +} diff --git a/backend/src/db/migrations/20250203141127_add-kmip.ts b/backend/src/db/migrations/20250203141127_add-kmip.ts new file mode 100644 index 000000000..ae63fbfe4 --- /dev/null +++ b/backend/src/db/migrations/20250203141127_add-kmip.ts @@ -0,0 +1,108 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + const hasKmipClientTable = await knex.schema.hasTable(TableName.KmipClient); + if (!hasKmipClientTable) { + await knex.schema.createTable(TableName.KmipClient, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("name").notNullable(); + t.specificType("permissions", "text[]"); + t.string("description"); + t.string("projectId").notNullable(); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + }); + } + + const hasKmipOrgPkiConfig = await knex.schema.hasTable(TableName.KmipOrgConfig); + if (!hasKmipOrgPkiConfig) { + await knex.schema.createTable(TableName.KmipOrgConfig, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.uuid("orgId").notNullable(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.unique("orgId"); + + t.string("caKeyAlgorithm").notNullable(); + + t.datetime("rootCaIssuedAt").notNullable(); + t.datetime("rootCaExpiration").notNullable(); + t.string("rootCaSerialNumber").notNullable(); + t.binary("encryptedRootCaCertificate").notNullable(); + t.binary("encryptedRootCaPrivateKey").notNullable(); + + t.datetime("serverIntermediateCaIssuedAt").notNullable(); + t.datetime("serverIntermediateCaExpiration").notNullable(); + t.string("serverIntermediateCaSerialNumber"); + t.binary("encryptedServerIntermediateCaCertificate").notNullable(); + t.binary("encryptedServerIntermediateCaChain").notNullable(); + t.binary("encryptedServerIntermediateCaPrivateKey").notNullable(); + + t.datetime("clientIntermediateCaIssuedAt").notNullable(); + t.datetime("clientIntermediateCaExpiration").notNullable(); + t.string("clientIntermediateCaSerialNumber").notNullable(); + t.binary("encryptedClientIntermediateCaCertificate").notNullable(); + t.binary("encryptedClientIntermediateCaChain").notNullable(); + t.binary("encryptedClientIntermediateCaPrivateKey").notNullable(); + + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.KmipOrgConfig); + } + + const hasKmipOrgServerCertTable = await knex.schema.hasTable(TableName.KmipOrgServerCertificates); + if (!hasKmipOrgServerCertTable) { + await knex.schema.createTable(TableName.KmipOrgServerCertificates, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.uuid("orgId").notNullable(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.string("commonName").notNullable(); + t.string("altNames").notNullable(); + t.string("serialNumber").notNullable(); + t.string("keyAlgorithm").notNullable(); + t.datetime("issuedAt").notNullable(); + t.datetime("expiration").notNullable(); + t.binary("encryptedCertificate").notNullable(); + t.binary("encryptedChain").notNullable(); + }); + } + + const hasKmipClientCertTable = await knex.schema.hasTable(TableName.KmipClientCertificates); + if (!hasKmipClientCertTable) { + await knex.schema.createTable(TableName.KmipClientCertificates, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.uuid("kmipClientId").notNullable(); + t.foreign("kmipClientId").references("id").inTable(TableName.KmipClient).onDelete("CASCADE"); + t.string("serialNumber").notNullable(); + t.string("keyAlgorithm").notNullable(); + t.datetime("issuedAt").notNullable(); + t.datetime("expiration").notNullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasKmipOrgPkiConfig = await knex.schema.hasTable(TableName.KmipOrgConfig); + if (hasKmipOrgPkiConfig) { + await knex.schema.dropTable(TableName.KmipOrgConfig); + await dropOnUpdateTrigger(knex, TableName.KmipOrgConfig); + } + + const hasKmipOrgServerCertTable = await knex.schema.hasTable(TableName.KmipOrgServerCertificates); + if (hasKmipOrgServerCertTable) { + await knex.schema.dropTable(TableName.KmipOrgServerCertificates); + } + + const hasKmipClientCertTable = await knex.schema.hasTable(TableName.KmipClientCertificates); + if (hasKmipClientCertTable) { + await knex.schema.dropTable(TableName.KmipClientCertificates); + } + + const hasKmipClientTable = await knex.schema.hasTable(TableName.KmipClient); + if (hasKmipClientTable) { + await knex.schema.dropTable(TableName.KmipClient); + } +} diff --git a/backend/src/db/migrations/20250204025010_app-connections-and-secret-syncs-unique-constraint.ts b/backend/src/db/migrations/20250204025010_app-connections-and-secret-syncs-unique-constraint.ts new file mode 100644 index 000000000..348694ae7 --- /dev/null +++ b/backend/src/db/migrations/20250204025010_app-connections-and-secret-syncs-unique-constraint.ts @@ -0,0 +1,23 @@ +import { Knex } from "knex"; + +import { TableName } from "@app/db/schemas"; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable(TableName.AppConnection, (t) => { + t.unique(["orgId", "name"]); + }); + + await knex.schema.alterTable(TableName.SecretSync, (t) => { + t.unique(["projectId", "name"]); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable(TableName.AppConnection, (t) => { + t.dropUnique(["orgId", "name"]); + }); + + await knex.schema.alterTable(TableName.SecretSync, (t) => { + t.dropUnique(["projectId", "name"]); + }); +} diff --git a/backend/src/db/migrations/20250205045509_increase-gcp-auth-limit.ts b/backend/src/db/migrations/20250205045509_increase-gcp-auth-limit.ts new file mode 100644 index 000000000..6d5e7cc4a --- /dev/null +++ b/backend/src/db/migrations/20250205045509_increase-gcp-auth-limit.ts @@ -0,0 +1,37 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasTable = await knex.schema.hasTable(TableName.IdentityGcpAuth); + const hasAllowedProjectsColumn = await knex.schema.hasColumn(TableName.IdentityGcpAuth, "allowedProjects"); + const hasAllowedServiceAccountsColumn = await knex.schema.hasColumn( + TableName.IdentityGcpAuth, + "allowedServiceAccounts" + ); + const hasAllowedZones = await knex.schema.hasColumn(TableName.IdentityGcpAuth, "allowedZones"); + if (hasTable) { + await knex.schema.alterTable(TableName.IdentityGcpAuth, (t) => { + if (hasAllowedProjectsColumn) t.string("allowedProjects", 2500).alter(); + if (hasAllowedServiceAccountsColumn) t.string("allowedServiceAccounts", 5000).alter(); + if (hasAllowedZones) t.string("allowedZones", 2500).alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasTable = await knex.schema.hasTable(TableName.IdentityGcpAuth); + const hasAllowedProjectsColumn = await knex.schema.hasColumn(TableName.IdentityGcpAuth, "allowedProjects"); + const hasAllowedServiceAccountsColumn = await knex.schema.hasColumn( + TableName.IdentityGcpAuth, + "allowedServiceAccounts" + ); + const hasAllowedZones = await knex.schema.hasColumn(TableName.IdentityGcpAuth, "allowedZones"); + if (hasTable) { + await knex.schema.alterTable(TableName.IdentityGcpAuth, (t) => { + if (hasAllowedProjectsColumn) t.string("allowedProjects").alter(); + if (hasAllowedServiceAccountsColumn) t.string("allowedServiceAccounts").alter(); + if (hasAllowedZones) t.string("allowedZones").alter(); + }); + } +} diff --git a/backend/src/db/migrations/20250205220952_kms-keys-drop-slug-col.ts b/backend/src/db/migrations/20250205220952_kms-keys-drop-slug-col.ts new file mode 100644 index 000000000..525e459b3 --- /dev/null +++ b/backend/src/db/migrations/20250205220952_kms-keys-drop-slug-col.ts @@ -0,0 +1,27 @@ +import { Knex } from "knex"; + +import { TableName } from "@app/db/schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.KmsKey)) { + const hasSlugCol = await knex.schema.hasColumn(TableName.KmsKey, "slug"); + + if (hasSlugCol) { + await knex.schema.alterTable(TableName.KmsKey, (t) => { + t.dropColumn("slug"); + }); + } + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.KmsKey)) { + const hasSlugCol = await knex.schema.hasColumn(TableName.KmsKey, "slug"); + + if (!hasSlugCol) { + await knex.schema.alterTable(TableName.KmsKey, (t) => { + t.string("slug", 32); + }); + } + } +} diff --git a/backend/src/db/migrations/20250207002643_secret-syncs-increase-message-length.ts b/backend/src/db/migrations/20250207002643_secret-syncs-increase-message-length.ts new file mode 100644 index 000000000..d550810aa --- /dev/null +++ b/backend/src/db/migrations/20250207002643_secret-syncs-increase-message-length.ts @@ -0,0 +1,31 @@ +import { Knex } from "knex"; + +import { TableName } from "@app/db/schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.SecretSync)) { + const hasLastSyncMessage = await knex.schema.hasColumn(TableName.SecretSync, "lastSyncMessage"); + const hasLastImportMessage = await knex.schema.hasColumn(TableName.SecretSync, "lastImportMessage"); + const hasLastRemoveMessage = await knex.schema.hasColumn(TableName.SecretSync, "lastRemoveMessage"); + + await knex.schema.alterTable(TableName.SecretSync, (t) => { + if (hasLastSyncMessage) t.string("lastSyncMessage", 1024).alter(); + if (hasLastImportMessage) t.string("lastImportMessage", 1024).alter(); + if (hasLastRemoveMessage) t.string("lastRemoveMessage", 1024).alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.SecretSync)) { + const hasLastSyncMessage = await knex.schema.hasColumn(TableName.SecretSync, "lastSyncMessage"); + const hasLastImportMessage = await knex.schema.hasColumn(TableName.SecretSync, "lastImportMessage"); + const hasLastRemoveMessage = await knex.schema.hasColumn(TableName.SecretSync, "lastRemoveMessage"); + + await knex.schema.alterTable(TableName.SecretSync, (t) => { + if (hasLastSyncMessage) t.string("lastSyncMessage").alter(); + if (hasLastImportMessage) t.string("lastImportMessage").alter(); + if (hasLastRemoveMessage) t.string("lastRemoveMessage").alter(); + }); + } +} diff --git a/backend/src/db/migrations/20250210101840_webhook-to-kms.ts b/backend/src/db/migrations/20250210101840_webhook-to-kms.ts new file mode 100644 index 000000000..a2d856388 --- /dev/null +++ b/backend/src/db/migrations/20250210101840_webhook-to-kms.ts @@ -0,0 +1,130 @@ +import { Knex } from "knex"; + +import { inMemoryKeyStore } from "@app/keystore/memory"; +import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { initLogger } from "@app/lib/logger"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { SecretKeyEncoding, TableName } from "../schemas"; +import { getMigrationEnvConfig } from "./utils/env-config"; +import { createCircularCache } from "./utils/ring-buffer"; +import { getMigrationEncryptionServices } from "./utils/services"; + +const BATCH_SIZE = 500; +export async function up(knex: Knex): Promise { + const hasEncryptedKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedPassKey"); + const hasEncryptedUrl = await knex.schema.hasColumn(TableName.Webhook, "encryptedUrl"); + const hasUrl = await knex.schema.hasColumn(TableName.Webhook, "url"); + + const hasWebhookTable = await knex.schema.hasTable(TableName.Webhook); + if (hasWebhookTable) { + await knex.schema.alterTable(TableName.Webhook, (t) => { + if (!hasEncryptedKey) t.binary("encryptedPassKey"); + if (!hasEncryptedUrl) t.binary("encryptedUrl"); + if (hasUrl) t.string("url").nullable().alter(); + }); + } + + initLogger(); + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + const projectEncryptionRingBuffer = + createCircularCache>>(25); + const webhooks = await knex(TableName.Webhook) + .where({}) + .join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.Webhook}.envId`) + .select( + "url", + "encryptedSecretKey", + "iv", + "tag", + "keyEncoding", + "urlCipherText", + "urlIV", + "urlTag", + knex.ref("id").withSchema(TableName.Webhook), + "envId" + ) + .select(knex.ref("projectId").withSchema(TableName.Environment)) + .orderBy(`${TableName.Environment}.projectId` as "projectId"); + + const updatedWebhooks = await Promise.all( + webhooks.map(async (el) => { + let projectKmsService = projectEncryptionRingBuffer.getItem(el.projectId); + if (!projectKmsService) { + projectKmsService = await kmsService.createCipherPairWithDataKey( + { + type: KmsDataKey.SecretManager, + projectId: el.projectId + }, + knex + ); + projectEncryptionRingBuffer.push(el.projectId, projectKmsService); + } + + let encryptedSecretKey = null; + if (el.encryptedSecretKey && el.iv && el.tag && el.keyEncoding) { + const decyptedSecretKey = infisicalSymmetricDecrypt({ + keyEncoding: el.keyEncoding as SecretKeyEncoding, + iv: el.iv, + tag: el.tag, + ciphertext: el.encryptedSecretKey + }); + encryptedSecretKey = projectKmsService.encryptor({ + plainText: Buffer.from(decyptedSecretKey, "utf8") + }).cipherTextBlob; + } + + const decryptedUrl = + el.urlIV && el.urlTag && el.urlCipherText && el.keyEncoding + ? infisicalSymmetricDecrypt({ + keyEncoding: el.keyEncoding as SecretKeyEncoding, + iv: el.urlIV, + tag: el.urlTag, + ciphertext: el.urlCipherText + }) + : null; + + const encryptedUrl = projectKmsService.encryptor({ + plainText: Buffer.from(decryptedUrl || el.url || "") + }).cipherTextBlob; + return { id: el.id, encryptedUrl, encryptedSecretKey, envId: el.envId }; + }) + ); + + for (let i = 0; i < updatedWebhooks.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.Webhook) + .insert( + updatedWebhooks.slice(i, i + BATCH_SIZE).map((el) => ({ + id: el.id, + envId: el.envId, + url: "", + encryptedUrl: el.encryptedUrl, + encryptedPassKey: el.encryptedSecretKey + })) + ) + .onConflict("id") + .merge(); + } + + if (hasWebhookTable) { + await knex.schema.alterTable(TableName.Webhook, (t) => { + if (!hasEncryptedUrl) t.binary("encryptedUrl").notNullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasEncryptedKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedPassKey"); + const hasEncryptedUrl = await knex.schema.hasColumn(TableName.Webhook, "encryptedUrl"); + + const hasWebhookTable = await knex.schema.hasTable(TableName.Webhook); + if (hasWebhookTable) { + await knex.schema.alterTable(TableName.Webhook, (t) => { + if (hasEncryptedKey) t.dropColumn("encryptedPassKey"); + if (hasEncryptedUrl) t.dropColumn("encryptedUrl"); + }); + } +} diff --git a/backend/src/db/migrations/20250210101841_dynamic-secret-root-to-kms.ts b/backend/src/db/migrations/20250210101841_dynamic-secret-root-to-kms.ts new file mode 100644 index 000000000..dde1e7188 --- /dev/null +++ b/backend/src/db/migrations/20250210101841_dynamic-secret-root-to-kms.ts @@ -0,0 +1,111 @@ +import { Knex } from "knex"; + +import { inMemoryKeyStore } from "@app/keystore/memory"; +import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { selectAllTableCols } from "@app/lib/knex"; +import { initLogger } from "@app/lib/logger"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { SecretKeyEncoding, TableName } from "../schemas"; +import { getMigrationEnvConfig } from "./utils/env-config"; +import { createCircularCache } from "./utils/ring-buffer"; +import { getMigrationEncryptionServices } from "./utils/services"; + +const BATCH_SIZE = 500; +export async function up(knex: Knex): Promise { + const hasEncryptedInputColumn = await knex.schema.hasColumn(TableName.DynamicSecret, "encryptedInput"); + const hasInputCiphertextColumn = await knex.schema.hasColumn(TableName.DynamicSecret, "inputCiphertext"); + const hasInputIVColumn = await knex.schema.hasColumn(TableName.DynamicSecret, "inputIV"); + const hasInputTagColumn = await knex.schema.hasColumn(TableName.DynamicSecret, "inputTag"); + + const hasDynamicSecretTable = await knex.schema.hasTable(TableName.DynamicSecret); + if (hasDynamicSecretTable) { + await knex.schema.alterTable(TableName.DynamicSecret, (t) => { + if (!hasEncryptedInputColumn) t.binary("encryptedInput"); + if (hasInputCiphertextColumn) t.text("inputCiphertext").nullable().alter(); + if (hasInputIVColumn) t.string("inputIV").nullable().alter(); + if (hasInputTagColumn) t.string("inputTag").nullable().alter(); + }); + } + + initLogger(); + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + const projectEncryptionRingBuffer = + createCircularCache>>(25); + + const dynamicSecretRootCredentials = await knex(TableName.DynamicSecret) + .join(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`) + .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) + .select(selectAllTableCols(TableName.DynamicSecret)) + .select(knex.ref("projectId").withSchema(TableName.Environment)) + .orderBy(`${TableName.Environment}.projectId` as "projectId"); + + const updatedDynamicSecrets = await Promise.all( + dynamicSecretRootCredentials.map(async ({ projectId, ...el }) => { + let projectKmsService = projectEncryptionRingBuffer.getItem(projectId); + if (!projectKmsService) { + projectKmsService = await kmsService.createCipherPairWithDataKey( + { + type: KmsDataKey.SecretManager, + projectId + }, + knex + ); + projectEncryptionRingBuffer.push(projectId, projectKmsService); + } + + const decryptedInputData = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.inputIV && el.inputTag && el.inputCiphertext && el.keyEncoding + ? infisicalSymmetricDecrypt({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + keyEncoding: el.keyEncoding as SecretKeyEncoding, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.inputIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.inputTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.inputCiphertext + }) + : ""; + + const encryptedInput = projectKmsService.encryptor({ + plainText: Buffer.from(decryptedInputData) + }).cipherTextBlob; + + return { ...el, encryptedInput }; + }) + ); + + for (let i = 0; i < updatedDynamicSecrets.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.DynamicSecret) + .insert(updatedDynamicSecrets.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } + + if (hasDynamicSecretTable) { + await knex.schema.alterTable(TableName.DynamicSecret, (t) => { + if (!hasEncryptedInputColumn) t.binary("encryptedInput").notNullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasEncryptedInputColumn = await knex.schema.hasColumn(TableName.DynamicSecret, "encryptedInput"); + + const hasDynamicSecretTable = await knex.schema.hasTable(TableName.DynamicSecret); + if (hasDynamicSecretTable) { + await knex.schema.alterTable(TableName.DynamicSecret, (t) => { + if (hasEncryptedInputColumn) t.dropColumn("encryptedInput"); + }); + } +} diff --git a/backend/src/db/migrations/20250210101841_secret-rotation-to-kms.ts b/backend/src/db/migrations/20250210101841_secret-rotation-to-kms.ts new file mode 100644 index 000000000..e11ef926e --- /dev/null +++ b/backend/src/db/migrations/20250210101841_secret-rotation-to-kms.ts @@ -0,0 +1,103 @@ +import { Knex } from "knex"; + +import { inMemoryKeyStore } from "@app/keystore/memory"; +import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { selectAllTableCols } from "@app/lib/knex"; +import { initLogger } from "@app/lib/logger"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { SecretKeyEncoding, TableName } from "../schemas"; +import { getMigrationEnvConfig } from "./utils/env-config"; +import { createCircularCache } from "./utils/ring-buffer"; +import { getMigrationEncryptionServices } from "./utils/services"; + +const BATCH_SIZE = 500; +export async function up(knex: Knex): Promise { + const hasEncryptedRotationData = await knex.schema.hasColumn(TableName.SecretRotation, "encryptedRotationData"); + + const hasRotationTable = await knex.schema.hasTable(TableName.SecretRotation); + if (hasRotationTable) { + await knex.schema.alterTable(TableName.SecretRotation, (t) => { + if (!hasEncryptedRotationData) t.binary("encryptedRotationData"); + }); + } + + initLogger(); + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + const projectEncryptionRingBuffer = + createCircularCache>>(25); + + const secretRotations = await knex(TableName.SecretRotation) + .join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretRotation}.envId`) + .select(selectAllTableCols(TableName.SecretRotation)) + .select(knex.ref("projectId").withSchema(TableName.Environment)) + .orderBy(`${TableName.Environment}.projectId` as "projectId"); + + const updatedRotationData = await Promise.all( + secretRotations.map(async ({ projectId, ...el }) => { + let projectKmsService = projectEncryptionRingBuffer.getItem(projectId); + if (!projectKmsService) { + projectKmsService = await kmsService.createCipherPairWithDataKey( + { + type: KmsDataKey.SecretManager, + projectId + }, + knex + ); + projectEncryptionRingBuffer.push(projectId, projectKmsService); + } + + const decryptedRotationData = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedDataTag && el.encryptedDataIV && el.encryptedData && el.keyEncoding + ? infisicalSymmetricDecrypt({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + keyEncoding: el.keyEncoding as SecretKeyEncoding, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.encryptedDataIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.encryptedDataTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedData + }) + : ""; + + const encryptedRotationData = projectKmsService.encryptor({ + plainText: Buffer.from(decryptedRotationData) + }).cipherTextBlob; + return { ...el, encryptedRotationData }; + }) + ); + + for (let i = 0; i < updatedRotationData.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.SecretRotation) + .insert(updatedRotationData.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } + + if (hasRotationTable) { + await knex.schema.alterTable(TableName.SecretRotation, (t) => { + if (!hasEncryptedRotationData) t.binary("encryptedRotationData").notNullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasEncryptedRotationData = await knex.schema.hasColumn(TableName.SecretRotation, "encryptedRotationData"); + + const hasRotationTable = await knex.schema.hasTable(TableName.SecretRotation); + if (hasRotationTable) { + await knex.schema.alterTable(TableName.SecretRotation, (t) => { + if (hasEncryptedRotationData) t.dropColumn("encryptedRotationData"); + }); + } +} diff --git a/backend/src/db/migrations/20250210101842_identity-k8-auth-to-kms.ts b/backend/src/db/migrations/20250210101842_identity-k8-auth-to-kms.ts new file mode 100644 index 000000000..934dce5e8 --- /dev/null +++ b/backend/src/db/migrations/20250210101842_identity-k8-auth-to-kms.ts @@ -0,0 +1,200 @@ +import { Knex } from "knex"; + +import { inMemoryKeyStore } from "@app/keystore/memory"; +import { decryptSymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { selectAllTableCols } from "@app/lib/knex"; +import { initLogger } from "@app/lib/logger"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { SecretKeyEncoding, TableName, TOrgBots } from "../schemas"; +import { getMigrationEnvConfig } from "./utils/env-config"; +import { createCircularCache } from "./utils/ring-buffer"; +import { getMigrationEncryptionServices } from "./utils/services"; + +const BATCH_SIZE = 500; +const reencryptIdentityK8sAuth = async (knex: Knex) => { + const hasEncryptedKubernetesTokenReviewerJwt = await knex.schema.hasColumn( + TableName.IdentityKubernetesAuth, + "encryptedKubernetesTokenReviewerJwt" + ); + const hasEncryptedCertificateColumn = await knex.schema.hasColumn( + TableName.IdentityKubernetesAuth, + "encryptedKubernetesCaCertificate" + ); + const hasidentityKubernetesAuthTable = await knex.schema.hasTable(TableName.IdentityKubernetesAuth); + + const hasEncryptedCaCertColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "encryptedCaCert"); + const hasCaCertIVColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "caCertIV"); + const hasCaCertTagColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "caCertTag"); + const hasEncryptedTokenReviewerJwtColumn = await knex.schema.hasColumn( + TableName.IdentityKubernetesAuth, + "encryptedTokenReviewerJwt" + ); + const hasTokenReviewerJwtIVColumn = await knex.schema.hasColumn( + TableName.IdentityKubernetesAuth, + "tokenReviewerJwtIV" + ); + const hasTokenReviewerJwtTagColumn = await knex.schema.hasColumn( + TableName.IdentityKubernetesAuth, + "tokenReviewerJwtTag" + ); + + if (hasidentityKubernetesAuthTable) { + await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (t) => { + if (hasEncryptedCaCertColumn) t.text("encryptedCaCert").nullable().alter(); + if (hasCaCertIVColumn) t.string("caCertIV").nullable().alter(); + if (hasCaCertTagColumn) t.string("caCertTag").nullable().alter(); + if (hasEncryptedTokenReviewerJwtColumn) t.text("encryptedTokenReviewerJwt").nullable().alter(); + if (hasTokenReviewerJwtIVColumn) t.string("tokenReviewerJwtIV").nullable().alter(); + if (hasTokenReviewerJwtTagColumn) t.string("tokenReviewerJwtTag").nullable().alter(); + + if (!hasEncryptedKubernetesTokenReviewerJwt) t.binary("encryptedKubernetesTokenReviewerJwt"); + if (!hasEncryptedCertificateColumn) t.binary("encryptedKubernetesCaCertificate"); + }); + } + + initLogger(); + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + const orgEncryptionRingBuffer = + createCircularCache>>(25); + const identityKubernetesConfigs = await knex(TableName.IdentityKubernetesAuth) + .join( + TableName.IdentityOrgMembership, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityKubernetesAuth}.identityId` + ) + .join(TableName.OrgBot, `${TableName.OrgBot}.orgId`, `${TableName.IdentityOrgMembership}.orgId`) + .select(selectAllTableCols(TableName.IdentityKubernetesAuth)) + .select( + knex.ref("encryptedSymmetricKey").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyIV").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyTag").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyKeyEncoding").withSchema(TableName.OrgBot), + knex.ref("orgId").withSchema(TableName.OrgBot) + ) + .orderBy(`${TableName.OrgBot}.orgId` as "orgId"); + + const updatedIdentityKubernetesConfigs = []; + + for await (const { + encryptedSymmetricKey, + symmetricKeyKeyEncoding, + symmetricKeyTag, + symmetricKeyIV, + orgId, + ...el + } of identityKubernetesConfigs) { + let orgKmsService = orgEncryptionRingBuffer.getItem(orgId); + + if (!orgKmsService) { + orgKmsService = await kmsService.createCipherPairWithDataKey( + { + type: KmsDataKey.Organization, + orgId + }, + knex + ); + orgEncryptionRingBuffer.push(orgId, orgKmsService); + } + + const key = infisicalSymmetricDecrypt({ + ciphertext: encryptedSymmetricKey, + iv: symmetricKeyIV, + tag: symmetricKeyTag, + keyEncoding: symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + const decryptedTokenReviewerJwt = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedTokenReviewerJwt && el.tokenReviewerJwtIV && el.tokenReviewerJwtTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.tokenReviewerJwtIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.tokenReviewerJwtTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedTokenReviewerJwt + }) + : ""; + + const decryptedCertificate = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedCaCert && el.caCertIV && el.caCertTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.caCertIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.caCertTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedCaCert + }) + : ""; + + const encryptedKubernetesTokenReviewerJwt = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedTokenReviewerJwt) + }).cipherTextBlob; + const encryptedKubernetesCaCertificate = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedCertificate) + }).cipherTextBlob; + + updatedIdentityKubernetesConfigs.push({ + ...el, + accessTokenTrustedIps: JSON.stringify(el.accessTokenTrustedIps), + encryptedKubernetesCaCertificate, + encryptedKubernetesTokenReviewerJwt + }); + } + + for (let i = 0; i < updatedIdentityKubernetesConfigs.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.IdentityKubernetesAuth) + .insert(updatedIdentityKubernetesConfigs.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } + if (hasidentityKubernetesAuthTable) { + await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (t) => { + if (!hasEncryptedKubernetesTokenReviewerJwt) + t.binary("encryptedKubernetesTokenReviewerJwt").notNullable().alter(); + }); + } +}; + +export async function up(knex: Knex): Promise { + await reencryptIdentityK8sAuth(knex); +} + +const dropIdentityK8sColumns = async (knex: Knex) => { + const hasEncryptedKubernetesTokenReviewerJwt = await knex.schema.hasColumn( + TableName.IdentityKubernetesAuth, + "encryptedKubernetesTokenReviewerJwt" + ); + const hasEncryptedCertificateColumn = await knex.schema.hasColumn( + TableName.IdentityKubernetesAuth, + "encryptedKubernetesCaCertificate" + ); + const hasidentityKubernetesAuthTable = await knex.schema.hasTable(TableName.IdentityKubernetesAuth); + + if (hasidentityKubernetesAuthTable) { + await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (t) => { + if (hasEncryptedKubernetesTokenReviewerJwt) t.dropColumn("encryptedKubernetesTokenReviewerJwt"); + if (hasEncryptedCertificateColumn) t.dropColumn("encryptedKubernetesCaCertificate"); + }); + } +}; + +export async function down(knex: Knex): Promise { + await dropIdentityK8sColumns(knex); +} diff --git a/backend/src/db/migrations/20250210101842_identity-oidc-auth-to-kms.ts b/backend/src/db/migrations/20250210101842_identity-oidc-auth-to-kms.ts new file mode 100644 index 000000000..011585bda --- /dev/null +++ b/backend/src/db/migrations/20250210101842_identity-oidc-auth-to-kms.ts @@ -0,0 +1,141 @@ +import { Knex } from "knex"; + +import { inMemoryKeyStore } from "@app/keystore/memory"; +import { decryptSymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { selectAllTableCols } from "@app/lib/knex"; +import { initLogger } from "@app/lib/logger"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { SecretKeyEncoding, TableName, TOrgBots } from "../schemas"; +import { getMigrationEnvConfig } from "./utils/env-config"; +import { createCircularCache } from "./utils/ring-buffer"; +import { getMigrationEncryptionServices } from "./utils/services"; + +const BATCH_SIZE = 500; +const reencryptIdentityOidcAuth = async (knex: Knex) => { + const hasEncryptedCertificateColumn = await knex.schema.hasColumn( + TableName.IdentityOidcAuth, + "encryptedCaCertificate" + ); + const hasidentityOidcAuthTable = await knex.schema.hasTable(TableName.IdentityOidcAuth); + + const hasEncryptedCaCertColumn = await knex.schema.hasColumn(TableName.IdentityOidcAuth, "encryptedCaCert"); + const hasCaCertIVColumn = await knex.schema.hasColumn(TableName.IdentityOidcAuth, "caCertIV"); + const hasCaCertTagColumn = await knex.schema.hasColumn(TableName.IdentityOidcAuth, "caCertTag"); + + if (hasidentityOidcAuthTable) { + await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => { + if (hasEncryptedCaCertColumn) t.text("encryptedCaCert").nullable().alter(); + if (hasCaCertIVColumn) t.string("caCertIV").nullable().alter(); + if (hasCaCertTagColumn) t.string("caCertTag").nullable().alter(); + + if (!hasEncryptedCertificateColumn) t.binary("encryptedCaCertificate"); + }); + } + + initLogger(); + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + const orgEncryptionRingBuffer = + createCircularCache>>(25); + + const identityOidcConfig = await knex(TableName.IdentityOidcAuth) + .join( + TableName.IdentityOrgMembership, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityOidcAuth}.identityId` + ) + .join(TableName.OrgBot, `${TableName.OrgBot}.orgId`, `${TableName.IdentityOrgMembership}.orgId`) + .select(selectAllTableCols(TableName.IdentityOidcAuth)) + .select( + knex.ref("encryptedSymmetricKey").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyIV").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyTag").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyKeyEncoding").withSchema(TableName.OrgBot), + knex.ref("orgId").withSchema(TableName.OrgBot) + ) + .orderBy(`${TableName.OrgBot}.orgId` as "orgId"); + + const updatedIdentityOidcConfigs = await Promise.all( + identityOidcConfig.map( + async ({ encryptedSymmetricKey, symmetricKeyKeyEncoding, symmetricKeyTag, symmetricKeyIV, orgId, ...el }) => { + let orgKmsService = orgEncryptionRingBuffer.getItem(orgId); + if (!orgKmsService) { + orgKmsService = await kmsService.createCipherPairWithDataKey( + { + type: KmsDataKey.Organization, + orgId + }, + knex + ); + orgEncryptionRingBuffer.push(orgId, orgKmsService); + } + const key = infisicalSymmetricDecrypt({ + ciphertext: encryptedSymmetricKey, + iv: symmetricKeyIV, + tag: symmetricKeyTag, + keyEncoding: symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + const decryptedCertificate = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedCaCert && el.caCertIV && el.caCertTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.caCertIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.caCertTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedCaCert + }) + : ""; + + const encryptedCaCertificate = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedCertificate) + }).cipherTextBlob; + + return { + ...el, + accessTokenTrustedIps: JSON.stringify(el.accessTokenTrustedIps), + encryptedCaCertificate + }; + } + ) + ); + + for (let i = 0; i < updatedIdentityOidcConfigs.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.IdentityOidcAuth) + .insert(updatedIdentityOidcConfigs.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } +}; + +export async function up(knex: Knex): Promise { + await reencryptIdentityOidcAuth(knex); +} + +const dropIdentityOidcColumns = async (knex: Knex) => { + const hasEncryptedCertificateColumn = await knex.schema.hasColumn( + TableName.IdentityOidcAuth, + "encryptedCaCertificate" + ); + const hasidentityOidcTable = await knex.schema.hasTable(TableName.IdentityOidcAuth); + + if (hasidentityOidcTable) { + await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => { + if (hasEncryptedCertificateColumn) t.dropColumn("encryptedCaCertificate"); + }); + } +}; + +export async function down(knex: Knex): Promise { + await dropIdentityOidcColumns(knex); +} diff --git a/backend/src/db/migrations/20250210101845_directory-config-to-kms.ts b/backend/src/db/migrations/20250210101845_directory-config-to-kms.ts new file mode 100644 index 000000000..f5107b301 --- /dev/null +++ b/backend/src/db/migrations/20250210101845_directory-config-to-kms.ts @@ -0,0 +1,493 @@ +import { Knex } from "knex"; + +import { inMemoryKeyStore } from "@app/keystore/memory"; +import { decryptSymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { selectAllTableCols } from "@app/lib/knex"; +import { initLogger } from "@app/lib/logger"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { SecretKeyEncoding, TableName } from "../schemas"; +import { getMigrationEnvConfig } from "./utils/env-config"; +import { createCircularCache } from "./utils/ring-buffer"; +import { getMigrationEncryptionServices } from "./utils/services"; + +const BATCH_SIZE = 500; +const reencryptSamlConfig = async (knex: Knex) => { + const hasEncryptedEntrypointColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlEntryPoint"); + const hasEncryptedIssuerColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlIssuer"); + const hasEncryptedCertificateColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlCertificate"); + const hasSamlConfigTable = await knex.schema.hasTable(TableName.SamlConfig); + + if (hasSamlConfigTable) { + await knex.schema.alterTable(TableName.SamlConfig, (t) => { + if (!hasEncryptedEntrypointColumn) t.binary("encryptedSamlEntryPoint"); + if (!hasEncryptedIssuerColumn) t.binary("encryptedSamlIssuer"); + if (!hasEncryptedCertificateColumn) t.binary("encryptedSamlCertificate"); + }); + } + + initLogger(); + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + const orgEncryptionRingBuffer = + createCircularCache>>(25); + + const samlConfigs = await knex(TableName.SamlConfig) + .join(TableName.OrgBot, `${TableName.OrgBot}.orgId`, `${TableName.SamlConfig}.orgId`) + .select(selectAllTableCols(TableName.SamlConfig)) + .select( + knex.ref("encryptedSymmetricKey").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyIV").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyTag").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyKeyEncoding").withSchema(TableName.OrgBot) + ) + .orderBy(`${TableName.OrgBot}.orgId` as "orgId"); + + const updatedSamlConfigs = await Promise.all( + samlConfigs.map( + async ({ encryptedSymmetricKey, symmetricKeyKeyEncoding, symmetricKeyTag, symmetricKeyIV, ...el }) => { + let orgKmsService = orgEncryptionRingBuffer.getItem(el.orgId); + if (!orgKmsService) { + orgKmsService = await kmsService.createCipherPairWithDataKey( + { + type: KmsDataKey.Organization, + orgId: el.orgId + }, + knex + ); + orgEncryptionRingBuffer.push(el.orgId, orgKmsService); + } + const key = infisicalSymmetricDecrypt({ + ciphertext: encryptedSymmetricKey, + iv: symmetricKeyIV, + tag: symmetricKeyTag, + keyEncoding: symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + const decryptedEntryPoint = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedEntryPoint && el.entryPointIV && el.entryPointTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.entryPointIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.entryPointTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedEntryPoint + }) + : ""; + + const decryptedIssuer = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedIssuer && el.issuerIV && el.issuerTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.issuerIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.issuerTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedIssuer + }) + : ""; + + const decryptedCertificate = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedCert && el.certIV && el.certTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.certIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.certTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedCert + }) + : ""; + + const encryptedSamlIssuer = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedIssuer) + }).cipherTextBlob; + const encryptedSamlCertificate = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedCertificate) + }).cipherTextBlob; + const encryptedSamlEntryPoint = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedEntryPoint) + }).cipherTextBlob; + return { ...el, encryptedSamlCertificate, encryptedSamlEntryPoint, encryptedSamlIssuer }; + } + ) + ); + + for (let i = 0; i < updatedSamlConfigs.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.SamlConfig) + .insert(updatedSamlConfigs.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } + + if (hasSamlConfigTable) { + await knex.schema.alterTable(TableName.SamlConfig, (t) => { + if (!hasEncryptedEntrypointColumn) t.binary("encryptedSamlEntryPoint").notNullable().alter(); + if (!hasEncryptedIssuerColumn) t.binary("encryptedSamlIssuer").notNullable().alter(); + if (!hasEncryptedCertificateColumn) t.binary("encryptedSamlCertificate").notNullable().alter(); + }); + } +}; + +const reencryptLdapConfig = async (knex: Knex) => { + const hasEncryptedLdapBindDNColum = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapBindDN"); + const hasEncryptedLdapBindPassColumn = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapBindPass"); + const hasEncryptedCertificateColumn = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapCaCertificate"); + const hasLdapConfigTable = await knex.schema.hasTable(TableName.LdapConfig); + + const hasEncryptedCACertColumn = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedCACert"); + const hasCaCertIVColumn = await knex.schema.hasColumn(TableName.LdapConfig, "caCertIV"); + const hasCaCertTagColumn = await knex.schema.hasColumn(TableName.LdapConfig, "caCertTag"); + const hasEncryptedBindPassColumn = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedBindPass"); + const hasBindPassIVColumn = await knex.schema.hasColumn(TableName.LdapConfig, "bindPassIV"); + const hasBindPassTagColumn = await knex.schema.hasColumn(TableName.LdapConfig, "bindPassTag"); + const hasEncryptedBindDNColumn = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedBindDN"); + const hasBindDNIVColumn = await knex.schema.hasColumn(TableName.LdapConfig, "bindDNIV"); + const hasBindDNTagColumn = await knex.schema.hasColumn(TableName.LdapConfig, "bindDNTag"); + + if (hasLdapConfigTable) { + await knex.schema.alterTable(TableName.LdapConfig, (t) => { + if (hasEncryptedCACertColumn) t.text("encryptedCACert").nullable().alter(); + if (hasCaCertIVColumn) t.string("caCertIV").nullable().alter(); + if (hasCaCertTagColumn) t.string("caCertTag").nullable().alter(); + if (hasEncryptedBindPassColumn) t.string("encryptedBindPass").nullable().alter(); + if (hasBindPassIVColumn) t.string("bindPassIV").nullable().alter(); + if (hasBindPassTagColumn) t.string("bindPassTag").nullable().alter(); + if (hasEncryptedBindDNColumn) t.string("encryptedBindDN").nullable().alter(); + if (hasBindDNIVColumn) t.string("bindDNIV").nullable().alter(); + if (hasBindDNTagColumn) t.string("bindDNTag").nullable().alter(); + + if (!hasEncryptedLdapBindDNColum) t.binary("encryptedLdapBindDN"); + if (!hasEncryptedLdapBindPassColumn) t.binary("encryptedLdapBindPass"); + if (!hasEncryptedCertificateColumn) t.binary("encryptedLdapCaCertificate"); + }); + } + + initLogger(); + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + const orgEncryptionRingBuffer = + createCircularCache>>(25); + + const ldapConfigs = await knex(TableName.LdapConfig) + .join(TableName.OrgBot, `${TableName.OrgBot}.orgId`, `${TableName.LdapConfig}.orgId`) + .select(selectAllTableCols(TableName.LdapConfig)) + .select( + knex.ref("encryptedSymmetricKey").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyIV").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyTag").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyKeyEncoding").withSchema(TableName.OrgBot) + ) + .orderBy(`${TableName.OrgBot}.orgId` as "orgId"); + + const updatedLdapConfigs = await Promise.all( + ldapConfigs.map( + async ({ encryptedSymmetricKey, symmetricKeyKeyEncoding, symmetricKeyTag, symmetricKeyIV, ...el }) => { + let orgKmsService = orgEncryptionRingBuffer.getItem(el.orgId); + if (!orgKmsService) { + orgKmsService = await kmsService.createCipherPairWithDataKey( + { + type: KmsDataKey.Organization, + orgId: el.orgId + }, + knex + ); + orgEncryptionRingBuffer.push(el.orgId, orgKmsService); + } + const key = infisicalSymmetricDecrypt({ + ciphertext: encryptedSymmetricKey, + iv: symmetricKeyIV, + tag: symmetricKeyTag, + keyEncoding: symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + const decryptedBindDN = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedBindDN && el.bindDNIV && el.bindDNTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.bindDNIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.bindDNTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedBindDN + }) + : ""; + + const decryptedBindPass = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedBindPass && el.bindPassIV && el.bindPassTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.bindPassIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.bindPassTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedBindPass + }) + : ""; + + const decryptedCertificate = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedCACert && el.caCertIV && el.caCertTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.caCertIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.caCertTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedCACert + }) + : ""; + + const encryptedLdapBindDN = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedBindDN) + }).cipherTextBlob; + const encryptedLdapBindPass = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedBindPass) + }).cipherTextBlob; + const encryptedLdapCaCertificate = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedCertificate) + }).cipherTextBlob; + return { ...el, encryptedLdapBindPass, encryptedLdapBindDN, encryptedLdapCaCertificate }; + } + ) + ); + + for (let i = 0; i < updatedLdapConfigs.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.LdapConfig) + .insert(updatedLdapConfigs.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } + if (hasLdapConfigTable) { + await knex.schema.alterTable(TableName.LdapConfig, (t) => { + if (!hasEncryptedLdapBindPassColumn) t.binary("encryptedLdapBindPass").notNullable().alter(); + if (!hasEncryptedLdapBindDNColum) t.binary("encryptedLdapBindDN").notNullable().alter(); + }); + } +}; + +const reencryptOidcConfig = async (knex: Knex) => { + const hasEncryptedOidcClientIdColumn = await knex.schema.hasColumn(TableName.OidcConfig, "encryptedOidcClientId"); + const hasEncryptedOidcClientSecretColumn = await knex.schema.hasColumn( + TableName.OidcConfig, + "encryptedOidcClientSecret" + ); + + const hasEncryptedClientIdColumn = await knex.schema.hasColumn(TableName.OidcConfig, "encryptedClientId"); + const hasClientIdIVColumn = await knex.schema.hasColumn(TableName.OidcConfig, "clientIdIV"); + const hasClientIdTagColumn = await knex.schema.hasColumn(TableName.OidcConfig, "clientIdTag"); + const hasEncryptedClientSecretColumn = await knex.schema.hasColumn(TableName.OidcConfig, "encryptedClientSecret"); + const hasClientSecretIVColumn = await knex.schema.hasColumn(TableName.OidcConfig, "clientSecretIV"); + const hasClientSecretTagColumn = await knex.schema.hasColumn(TableName.OidcConfig, "clientSecretTag"); + + const hasOidcConfigTable = await knex.schema.hasTable(TableName.OidcConfig); + + if (hasOidcConfigTable) { + await knex.schema.alterTable(TableName.OidcConfig, (t) => { + if (hasEncryptedClientIdColumn) t.text("encryptedClientId").nullable().alter(); + if (hasClientIdIVColumn) t.string("clientIdIV").nullable().alter(); + if (hasClientIdTagColumn) t.string("clientIdTag").nullable().alter(); + if (hasEncryptedClientSecretColumn) t.text("encryptedClientSecret").nullable().alter(); + if (hasClientSecretIVColumn) t.string("clientSecretIV").nullable().alter(); + if (hasClientSecretTagColumn) t.string("clientSecretTag").nullable().alter(); + + if (!hasEncryptedOidcClientIdColumn) t.binary("encryptedOidcClientId"); + if (!hasEncryptedOidcClientSecretColumn) t.binary("encryptedOidcClientSecret"); + }); + } + + initLogger(); + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + const orgEncryptionRingBuffer = + createCircularCache>>(25); + + const oidcConfigs = await knex(TableName.OidcConfig) + .join(TableName.OrgBot, `${TableName.OrgBot}.orgId`, `${TableName.OidcConfig}.orgId`) + .select(selectAllTableCols(TableName.OidcConfig)) + .select( + knex.ref("encryptedSymmetricKey").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyIV").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyTag").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyKeyEncoding").withSchema(TableName.OrgBot) + ) + .orderBy(`${TableName.OrgBot}.orgId` as "orgId"); + + const updatedOidcConfigs = await Promise.all( + oidcConfigs.map( + async ({ encryptedSymmetricKey, symmetricKeyKeyEncoding, symmetricKeyTag, symmetricKeyIV, ...el }) => { + let orgKmsService = orgEncryptionRingBuffer.getItem(el.orgId); + if (!orgKmsService) { + orgKmsService = await kmsService.createCipherPairWithDataKey( + { + type: KmsDataKey.Organization, + orgId: el.orgId + }, + knex + ); + orgEncryptionRingBuffer.push(el.orgId, orgKmsService); + } + const key = infisicalSymmetricDecrypt({ + ciphertext: encryptedSymmetricKey, + iv: symmetricKeyIV, + tag: symmetricKeyTag, + keyEncoding: symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + const decryptedClientId = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedClientId && el.clientIdIV && el.clientIdTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.clientIdIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.clientIdTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedClientId + }) + : ""; + + const decryptedClientSecret = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedClientSecret && el.clientSecretIV && el.clientSecretTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.clientSecretIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.clientSecretTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedClientSecret + }) + : ""; + + const encryptedOidcClientId = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedClientId) + }).cipherTextBlob; + const encryptedOidcClientSecret = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedClientSecret) + }).cipherTextBlob; + return { ...el, encryptedOidcClientId, encryptedOidcClientSecret }; + } + ) + ); + + for (let i = 0; i < updatedOidcConfigs.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.OidcConfig) + .insert(updatedOidcConfigs.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } + if (hasOidcConfigTable) { + await knex.schema.alterTable(TableName.OidcConfig, (t) => { + if (!hasEncryptedOidcClientIdColumn) t.binary("encryptedOidcClientId").notNullable().alter(); + if (!hasEncryptedOidcClientSecretColumn) t.binary("encryptedOidcClientSecret").notNullable().alter(); + }); + } +}; + +export async function up(knex: Knex): Promise { + await reencryptSamlConfig(knex); + await reencryptLdapConfig(knex); + await reencryptOidcConfig(knex); +} + +const dropSamlConfigColumns = async (knex: Knex) => { + const hasEncryptedEntrypointColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlEntryPoint"); + const hasEncryptedIssuerColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlIssuer"); + const hasEncryptedCertificateColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlCertificate"); + const hasSamlConfigTable = await knex.schema.hasTable(TableName.SamlConfig); + + if (hasSamlConfigTable) { + await knex.schema.alterTable(TableName.SamlConfig, (t) => { + if (hasEncryptedEntrypointColumn) t.dropColumn("encryptedSamlEntryPoint"); + if (hasEncryptedIssuerColumn) t.dropColumn("encryptedSamlIssuer"); + if (hasEncryptedCertificateColumn) t.dropColumn("encryptedSamlCertificate"); + }); + } +}; + +const dropLdapConfigColumns = async (knex: Knex) => { + const hasEncryptedBindDN = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapBindDN"); + const hasEncryptedBindPass = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapBindPass"); + const hasEncryptedCertificateColumn = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapCaCertificate"); + const hasLdapConfigTable = await knex.schema.hasTable(TableName.LdapConfig); + + if (hasLdapConfigTable) { + await knex.schema.alterTable(TableName.LdapConfig, (t) => { + if (hasEncryptedBindDN) t.dropColumn("encryptedLdapBindDN"); + if (hasEncryptedBindPass) t.dropColumn("encryptedLdapBindPass"); + if (hasEncryptedCertificateColumn) t.dropColumn("encryptedLdapCaCertificate"); + }); + } +}; + +const dropOidcConfigColumns = async (knex: Knex) => { + const hasEncryptedClientId = await knex.schema.hasColumn(TableName.OidcConfig, "encryptedOidcClientId"); + const hasEncryptedClientSecret = await knex.schema.hasColumn(TableName.OidcConfig, "encryptedOidcClientSecret"); + const hasOidcConfigTable = await knex.schema.hasTable(TableName.OidcConfig); + + if (hasOidcConfigTable) { + await knex.schema.alterTable(TableName.OidcConfig, (t) => { + if (hasEncryptedClientId) t.dropColumn("encryptedOidcClientId"); + if (hasEncryptedClientSecret) t.dropColumn("encryptedOidcClientSecret"); + }); + } +}; + +export async function down(knex: Knex): Promise { + await dropSamlConfigColumns(knex); + await dropLdapConfigColumns(knex); + await dropOidcConfigColumns(knex); +} diff --git a/backend/src/db/migrations/20250212191958_create-gateway.ts b/backend/src/db/migrations/20250212191958_create-gateway.ts new file mode 100644 index 000000000..14c498ca9 --- /dev/null +++ b/backend/src/db/migrations/20250212191958_create-gateway.ts @@ -0,0 +1,115 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.OrgGatewayConfig))) { + await knex.schema.createTable(TableName.OrgGatewayConfig, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("rootCaKeyAlgorithm").notNullable(); + + t.datetime("rootCaIssuedAt").notNullable(); + t.datetime("rootCaExpiration").notNullable(); + t.string("rootCaSerialNumber").notNullable(); + t.binary("encryptedRootCaCertificate").notNullable(); + t.binary("encryptedRootCaPrivateKey").notNullable(); + + t.datetime("clientCaIssuedAt").notNullable(); + t.datetime("clientCaExpiration").notNullable(); + t.string("clientCaSerialNumber"); + t.binary("encryptedClientCaCertificate").notNullable(); + t.binary("encryptedClientCaPrivateKey").notNullable(); + + t.string("clientCertSerialNumber").notNullable(); + t.string("clientCertKeyAlgorithm").notNullable(); + t.datetime("clientCertIssuedAt").notNullable(); + t.datetime("clientCertExpiration").notNullable(); + t.binary("encryptedClientCertificate").notNullable(); + t.binary("encryptedClientPrivateKey").notNullable(); + + t.datetime("gatewayCaIssuedAt").notNullable(); + t.datetime("gatewayCaExpiration").notNullable(); + t.string("gatewayCaSerialNumber").notNullable(); + t.binary("encryptedGatewayCaCertificate").notNullable(); + t.binary("encryptedGatewayCaPrivateKey").notNullable(); + + t.uuid("orgId").notNullable(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.unique("orgId"); + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.OrgGatewayConfig); + } + + if (!(await knex.schema.hasTable(TableName.Gateway))) { + await knex.schema.createTable(TableName.Gateway, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.string("name").notNullable(); + t.string("serialNumber").notNullable(); + t.string("keyAlgorithm").notNullable(); + t.datetime("issuedAt").notNullable(); + t.datetime("expiration").notNullable(); + t.datetime("heartbeat"); + + t.binary("relayAddress").notNullable(); + + t.uuid("orgGatewayRootCaId").notNullable(); + t.foreign("orgGatewayRootCaId").references("id").inTable(TableName.OrgGatewayConfig).onDelete("CASCADE"); + + t.uuid("identityId").notNullable(); + t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE"); + + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.Gateway); + } + + if (!(await knex.schema.hasTable(TableName.ProjectGateway))) { + await knex.schema.createTable(TableName.ProjectGateway, (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.uuid("gatewayId").notNullable(); + t.foreign("gatewayId").references("id").inTable(TableName.Gateway).onDelete("CASCADE"); + + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.ProjectGateway); + } + + if (await knex.schema.hasTable(TableName.DynamicSecret)) { + const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "projectGatewayId"); + await knex.schema.alterTable(TableName.DynamicSecret, (t) => { + // not setting a foreign constraint so that cascade effects are not triggered + if (!doesGatewayColExist) { + t.uuid("projectGatewayId"); + t.foreign("projectGatewayId").references("id").inTable(TableName.ProjectGateway); + } + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.DynamicSecret)) { + const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "projectGatewayId"); + await knex.schema.alterTable(TableName.DynamicSecret, (t) => { + if (doesGatewayColExist) t.dropColumn("projectGatewayId"); + }); + } + + await knex.schema.dropTableIfExists(TableName.ProjectGateway); + await dropOnUpdateTrigger(knex, TableName.ProjectGateway); + + await knex.schema.dropTableIfExists(TableName.Gateway); + await dropOnUpdateTrigger(knex, TableName.Gateway); + + await knex.schema.dropTableIfExists(TableName.OrgGatewayConfig); + await dropOnUpdateTrigger(knex, TableName.OrgGatewayConfig); +} diff --git a/backend/src/db/migrations/20250226021631_secret-requests.ts b/backend/src/db/migrations/20250226021631_secret-requests.ts new file mode 100644 index 000000000..cac47bd88 --- /dev/null +++ b/backend/src/db/migrations/20250226021631_secret-requests.ts @@ -0,0 +1,25 @@ +import { Knex } from "knex"; + +import { SecretSharingType } from "@app/services/secret-sharing/secret-sharing-types"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasSharingTypeColumn = await knex.schema.hasColumn(TableName.SecretSharing, "type"); + + await knex.schema.alterTable(TableName.SecretSharing, (table) => { + if (!hasSharingTypeColumn) { + table.string("type", 32).defaultTo(SecretSharingType.Share).notNullable(); + } + }); +} + +export async function down(knex: Knex): Promise { + const hasSharingTypeColumn = await knex.schema.hasColumn(TableName.SecretSharing, "type"); + + await knex.schema.alterTable(TableName.SecretSharing, (table) => { + if (hasSharingTypeColumn) { + table.dropColumn("type"); + } + }); +} diff --git a/backend/src/db/migrations/20250226082254_add-gov-banner-and-consent-fields.ts b/backend/src/db/migrations/20250226082254_add-gov-banner-and-consent-fields.ts new file mode 100644 index 000000000..2a159af56 --- /dev/null +++ b/backend/src/db/migrations/20250226082254_add-gov-banner-and-consent-fields.ts @@ -0,0 +1,31 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasAuthConsentContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "authConsentContent"); + const hasPageFrameContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "pageFrameContent"); + if (await knex.schema.hasTable(TableName.SuperAdmin)) { + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + if (!hasAuthConsentContentCol) { + t.text("authConsentContent"); + } + if (!hasPageFrameContentCol) { + t.text("pageFrameContent"); + } + }); + } +} + +export async function down(knex: Knex): Promise { + const hasAuthConsentContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "authConsentContent"); + const hasPageFrameContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "pageFrameContent"); + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + if (hasAuthConsentContentCol) { + t.dropColumn("authConsentContent"); + } + if (hasPageFrameContentCol) { + t.dropColumn("pageFrameContent"); + } + }); +} diff --git a/backend/src/db/migrations/20250228022604_increase-secret-reminder-note-max-length.ts b/backend/src/db/migrations/20250228022604_increase-secret-reminder-note-max-length.ts new file mode 100644 index 000000000..f0ab5f92e --- /dev/null +++ b/backend/src/db/migrations/20250228022604_increase-secret-reminder-note-max-length.ts @@ -0,0 +1,35 @@ +import { Knex } from "knex"; + +import { TableName } from "@app/db/schemas"; + +export async function up(knex: Knex): Promise { + for await (const tableName of [ + TableName.SecretV2, + TableName.SecretVersionV2, + TableName.SecretApprovalRequestSecretV2 + ]) { + const hasReminderNoteCol = await knex.schema.hasColumn(tableName, "reminderNote"); + + if (hasReminderNoteCol) { + await knex.schema.alterTable(tableName, (t) => { + t.string("reminderNote", 1024).alter(); + }); + } + } +} + +export async function down(knex: Knex): Promise { + for await (const tableName of [ + TableName.SecretV2, + TableName.SecretVersionV2, + TableName.SecretApprovalRequestSecretV2 + ]) { + const hasReminderNoteCol = await knex.schema.hasColumn(tableName, "reminderNote"); + + if (hasReminderNoteCol) { + await knex.schema.alterTable(tableName, (t) => { + t.string("reminderNote").alter(); + }); + } + } +} diff --git a/backend/src/db/migrations/20250303213350_add-folder-description.ts b/backend/src/db/migrations/20250303213350_add-folder-description.ts new file mode 100644 index 000000000..6a5d0a076 --- /dev/null +++ b/backend/src/db/migrations/20250303213350_add-folder-description.ts @@ -0,0 +1,23 @@ +import { Knex } from "knex"; + +import { TableName } from "@app/db/schemas"; + +export async function up(knex: Knex): Promise { + const hasProjectDescription = await knex.schema.hasColumn(TableName.SecretFolder, "description"); + + if (!hasProjectDescription) { + await knex.schema.alterTable(TableName.SecretFolder, (t) => { + t.string("description"); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasProjectDescription = await knex.schema.hasColumn(TableName.SecretFolder, "description"); + + if (hasProjectDescription) { + await knex.schema.alterTable(TableName.SecretFolder, (t) => { + t.dropColumn("description"); + }); + } +} diff --git a/backend/src/db/migrations/20250305080145_add-secret-review-comment.ts b/backend/src/db/migrations/20250305080145_add-secret-review-comment.ts new file mode 100644 index 000000000..7d51bb226 --- /dev/null +++ b/backend/src/db/migrations/20250305080145_add-secret-review-comment.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.SecretApprovalRequestReviewer, "comment"))) { + await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (t) => { + t.string("comment"); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "comment")) { + await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (t) => { + t.dropColumn("comment"); + }); + } +} diff --git a/backend/src/db/migrations/20250305131152_add-actor-id-to-secret-versions-v2.ts b/backend/src/db/migrations/20250305131152_add-actor-id-to-secret-versions-v2.ts new file mode 100644 index 000000000..fb9a047af --- /dev/null +++ b/backend/src/db/migrations/20250305131152_add-actor-id-to-secret-versions-v2.ts @@ -0,0 +1,45 @@ +import { Knex } from "knex"; + +import { TableName } from "@app/db/schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.SecretVersionV2)) { + const hasSecretVersionV2UserActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "userActorId"); + const hasSecretVersionV2IdentityActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "identityActorId"); + const hasSecretVersionV2ActorType = await knex.schema.hasColumn(TableName.SecretVersionV2, "actorType"); + + await knex.schema.alterTable(TableName.SecretVersionV2, (t) => { + if (!hasSecretVersionV2UserActorId) { + t.uuid("userActorId"); + t.foreign("userActorId").references("id").inTable(TableName.Users); + } + if (!hasSecretVersionV2IdentityActorId) { + t.uuid("identityActorId"); + t.foreign("identityActorId").references("id").inTable(TableName.Identity); + } + if (!hasSecretVersionV2ActorType) { + t.string("actorType"); + } + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.SecretVersionV2)) { + const hasSecretVersionV2UserActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "userActorId"); + const hasSecretVersionV2IdentityActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "identityActorId"); + const hasSecretVersionV2ActorType = await knex.schema.hasColumn(TableName.SecretVersionV2, "actorType"); + + await knex.schema.alterTable(TableName.SecretVersionV2, (t) => { + if (hasSecretVersionV2UserActorId) { + t.dropColumn("userActorId"); + } + if (hasSecretVersionV2IdentityActorId) { + t.dropColumn("identityActorId"); + } + if (hasSecretVersionV2ActorType) { + t.dropColumn("actorType"); + } + }); + } +} diff --git a/backend/src/db/migrations/20250311105617_add-share-to-anyone-setting-to-organizations.ts b/backend/src/db/migrations/20250311105617_add-share-to-anyone-setting-to-organizations.ts new file mode 100644 index 000000000..8f767c748 --- /dev/null +++ b/backend/src/db/migrations/20250311105617_add-share-to-anyone-setting-to-organizations.ts @@ -0,0 +1,32 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.Organization)) { + const hasSecretShareToAnyoneCol = await knex.schema.hasColumn( + TableName.Organization, + "allowSecretSharingOutsideOrganization" + ); + + if (!hasSecretShareToAnyoneCol) { + await knex.schema.alterTable(TableName.Organization, (t) => { + t.boolean("allowSecretSharingOutsideOrganization").defaultTo(true); + }); + } + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.Organization)) { + const hasSecretShareToAnyoneCol = await knex.schema.hasColumn( + TableName.Organization, + "allowSecretSharingOutsideOrganization" + ); + if (hasSecretShareToAnyoneCol) { + await knex.schema.alterTable(TableName.Organization, (t) => { + t.dropColumn("allowSecretSharingOutsideOrganization"); + }); + } + } +} diff --git a/backend/src/db/migrations/20250313124706_add-privilege-upgrade-field.ts b/backend/src/db/migrations/20250313124706_add-privilege-upgrade-field.ts new file mode 100644 index 000000000..5a16666af --- /dev/null +++ b/backend/src/db/migrations/20250313124706_add-privilege-upgrade-field.ts @@ -0,0 +1,31 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.Organization, "shouldUseNewPrivilegeSystem"))) { + await knex.schema.alterTable(TableName.Organization, (t) => { + t.boolean("shouldUseNewPrivilegeSystem"); + t.string("privilegeUpgradeInitiatedByUsername"); + t.dateTime("privilegeUpgradeInitiatedAt"); + }); + + await knex(TableName.Organization).update({ + shouldUseNewPrivilegeSystem: false + }); + + await knex.schema.alterTable(TableName.Organization, (t) => { + t.boolean("shouldUseNewPrivilegeSystem").defaultTo(true).notNullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.Organization, "shouldUseNewPrivilegeSystem")) { + await knex.schema.alterTable(TableName.Organization, (t) => { + t.dropColumn("shouldUseNewPrivilegeSystem"); + t.dropColumn("privilegeUpgradeInitiatedByUsername"); + t.dropColumn("privilegeUpgradeInitiatedAt"); + }); + } +} diff --git a/backend/src/db/migrations/20250314145202_identity-oidc-claim-mapping.ts b/backend/src/db/migrations/20250314145202_identity-oidc-claim-mapping.ts new file mode 100644 index 000000000..482c5c4eb --- /dev/null +++ b/backend/src/db/migrations/20250314145202_identity-oidc-claim-mapping.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasMappingField = await knex.schema.hasColumn(TableName.IdentityOidcAuth, "claimMetadataMapping"); + if (!hasMappingField) { + await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => { + t.jsonb("claimMetadataMapping"); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasMappingField = await knex.schema.hasColumn(TableName.IdentityOidcAuth, "claimMetadataMapping"); + if (hasMappingField) { + await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => { + t.dropColumn("claimMetadataMapping"); + }); + } +} diff --git a/backend/src/db/migrations/20250317101525_add-instance-admin-mi.ts b/backend/src/db/migrations/20250317101525_add-instance-admin-mi.ts new file mode 100644 index 000000000..7646b48a9 --- /dev/null +++ b/backend/src/db/migrations/20250317101525_add-instance-admin-mi.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas/models"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.SuperAdmin, "adminIdentityIds"))) { + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + t.specificType("adminIdentityIds", "text[]"); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.SuperAdmin, "adminIdentityIds")) { + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + t.dropColumn("adminIdentityIds"); + }); + } +} diff --git a/backend/src/db/migrations/20250319134021_recursive-folder-index.ts b/backend/src/db/migrations/20250319134021_recursive-folder-index.ts new file mode 100644 index 000000000..e26b6d876 --- /dev/null +++ b/backend/src/db/migrations/20250319134021_recursive-folder-index.ts @@ -0,0 +1,23 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const doesParentColumExist = await knex.schema.hasColumn(TableName.SecretFolder, "parentId"); + const doesNameColumnExist = await knex.schema.hasColumn(TableName.SecretFolder, "name"); + if (doesParentColumExist && doesNameColumnExist) { + await knex.schema.alterTable(TableName.SecretFolder, (t) => { + t.index(["parentId", "name"]); + }); + } +} + +export async function down(knex: Knex): Promise { + const doesParentColumExist = await knex.schema.hasColumn(TableName.SecretFolder, "parentId"); + const doesNameColumnExist = await knex.schema.hasColumn(TableName.SecretFolder, "name"); + if (doesParentColumExist && doesNameColumnExist) { + await knex.schema.alterTable(TableName.SecretFolder, (t) => { + t.dropIndex(["parentId", "name"]); + }); + } +} diff --git a/backend/src/db/migrations/20250321100157_k8s-self-reviewer-jwt.ts b/backend/src/db/migrations/20250321100157_k8s-self-reviewer-jwt.ts new file mode 100644 index 000000000..6cc3a2696 --- /dev/null +++ b/backend/src/db/migrations/20250321100157_k8s-self-reviewer-jwt.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasReviewerJwtCol = await knex.schema.hasColumn( + TableName.IdentityKubernetesAuth, + "encryptedKubernetesTokenReviewerJwt" + ); + if (hasReviewerJwtCol) { + await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (t) => { + t.binary("encryptedKubernetesTokenReviewerJwt").nullable().alter(); + }); + } +} + +export async function down(): Promise { + // we can't make it back to non nullable, it will fail +} diff --git a/backend/src/db/migrations/20250324142102_add-self-approvals-to-secret-approval-policies.ts b/backend/src/db/migrations/20250324142102_add-self-approvals-to-secret-approval-policies.ts new file mode 100644 index 000000000..ee99fce67 --- /dev/null +++ b/backend/src/db/migrations/20250324142102_add-self-approvals-to-secret-approval-policies.ts @@ -0,0 +1,29 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas/models"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.SecretApprovalPolicy, "allowedSelfApprovals"))) { + await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => { + t.boolean("allowedSelfApprovals").notNullable().defaultTo(true); + }); + } + if (!(await knex.schema.hasColumn(TableName.AccessApprovalPolicy, "allowedSelfApprovals"))) { + await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => { + t.boolean("allowedSelfApprovals").notNullable().defaultTo(true); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.SecretApprovalPolicy, "allowedSelfApprovals")) { + await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => { + t.dropColumn("allowedSelfApprovals"); + }); + } + if (await knex.schema.hasColumn(TableName.AccessApprovalPolicy, "allowedSelfApprovals")) { + await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => { + t.dropColumn("allowedSelfApprovals"); + }); + } +} diff --git a/backend/src/db/migrations/20250324142104_app-connection-is-platform-managed-credentials-col.ts b/backend/src/db/migrations/20250324142104_app-connection-is-platform-managed-credentials-col.ts new file mode 100644 index 000000000..79d455605 --- /dev/null +++ b/backend/src/db/migrations/20250324142104_app-connection-is-platform-managed-credentials-col.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +import { TableName } from "@app/db/schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.AppConnection, "isPlatformManagedCredentials"))) { + await knex.schema.alterTable(TableName.AppConnection, (t) => { + t.boolean("isPlatformManagedCredentials").defaultTo(false); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.AppConnection, "isPlatformManagedCredentials")) { + await knex.schema.alterTable(TableName.AppConnection, (t) => { + t.dropColumn("isPlatformManagedCredentials"); + }); + } +} diff --git a/backend/src/db/migrations/20250324142105_secret-rotation-v2.ts b/backend/src/db/migrations/20250324142105_secret-rotation-v2.ts new file mode 100644 index 000000000..dfe0e6888 --- /dev/null +++ b/backend/src/db/migrations/20250324142105_secret-rotation-v2.ts @@ -0,0 +1,58 @@ +import { Knex } from "knex"; + +import { TableName } from "@app/db/schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "@app/db/utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.SecretRotationV2))) { + await knex.schema.createTable(TableName.SecretRotationV2, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("name", 32).notNullable(); + t.string("description"); + t.string("type").notNullable(); + t.jsonb("parameters").notNullable(); + t.jsonb("secretsMapping").notNullable(); + t.binary("encryptedGeneratedCredentials").notNullable(); + t.boolean("isAutoRotationEnabled").notNullable().defaultTo(true); + t.integer("activeIndex").notNullable().defaultTo(0); + t.uuid("folderId").notNullable(); + t.foreign("folderId").references("id").inTable(TableName.SecretFolder).onDelete("CASCADE"); + t.uuid("connectionId").notNullable(); + t.foreign("connectionId").references("id").inTable(TableName.AppConnection); + t.timestamps(true, true, true); + t.integer("rotationInterval").notNullable(); + t.jsonb("rotateAtUtc").notNullable(); // { hours: number; minutes: number } + t.string("rotationStatus").notNullable(); + t.datetime("lastRotationAttemptedAt").notNullable(); + t.datetime("lastRotatedAt").notNullable(); + t.binary("encryptedLastRotationMessage"); // we encrypt this because it may contain sensitive info (SQL errors showing credentials) + t.string("lastRotationJobId"); + t.datetime("nextRotationAt"); + t.boolean("isLastRotationManual").notNullable().defaultTo(true); // creation is considered a "manual" rotation + }); + + await createOnUpdateTrigger(knex, TableName.SecretRotationV2); + + await knex.schema.alterTable(TableName.SecretRotationV2, (t) => { + t.unique(["folderId", "name"]); + }); + } + + if (!(await knex.schema.hasTable(TableName.SecretRotationV2SecretMapping))) { + await knex.schema.createTable(TableName.SecretRotationV2SecretMapping, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.uuid("secretId").notNullable(); + // scott: this is deferred to block secret deletion but not prevent folder/environment/project deletion + // ie, if rotation is being deleted as well we permit it, otherwise throw + t.foreign("secretId").references("id").inTable(TableName.SecretV2).deferrable("deferred"); + t.uuid("rotationId").notNullable(); + t.foreign("rotationId").references("id").inTable(TableName.SecretRotationV2).onDelete("CASCADE"); + }); + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.SecretRotationV2SecretMapping); + await knex.schema.dropTableIfExists(TableName.SecretRotationV2); + await dropOnUpdateTrigger(knex, TableName.SecretRotationV2); +} diff --git a/backend/src/db/migrations/20250326171707_folder-last-secret-modified.ts b/backend/src/db/migrations/20250326171707_folder-last-secret-modified.ts new file mode 100644 index 000000000..227fe6ef7 --- /dev/null +++ b/backend/src/db/migrations/20250326171707_folder-last-secret-modified.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasCol = await knex.schema.hasColumn(TableName.SecretFolder, "lastSecretModified"); + if (!hasCol) { + await knex.schema.alterTable(TableName.SecretFolder, (t) => { + t.datetime("lastSecretModified"); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasCol = await knex.schema.hasColumn(TableName.SecretFolder, "lastSecretModified"); + if (hasCol) { + await knex.schema.alterTable(TableName.SecretFolder, (t) => { + t.dropColumn("lastSecretModified"); + }); + } +} diff --git a/backend/src/db/migrations/20250402000941_add-type-to-kms-keys.ts b/backend/src/db/migrations/20250402000941_add-type-to-kms-keys.ts new file mode 100644 index 000000000..591c5f1ec --- /dev/null +++ b/backend/src/db/migrations/20250402000941_add-type-to-kms-keys.ts @@ -0,0 +1,25 @@ +import { Knex } from "knex"; + +import { KmsKeyUsage } from "@app/services/kms/kms-types"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasKeyUsageColumn = await knex.schema.hasColumn(TableName.KmsKey, "keyUsage"); + + if (!hasKeyUsageColumn) { + await knex.schema.alterTable(TableName.KmsKey, (t) => { + t.string("keyUsage").notNullable().defaultTo(KmsKeyUsage.ENCRYPT_DECRYPT); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasKeyUsageColumn = await knex.schema.hasColumn(TableName.KmsKey, "keyUsage"); + + if (hasKeyUsageColumn) { + await knex.schema.alterTable(TableName.KmsKey, (t) => { + t.dropColumn("keyUsage"); + }); + } +} diff --git a/backend/src/db/migrations/20250404022310_ssh-ca-key-source.ts b/backend/src/db/migrations/20250404022310_ssh-ca-key-source.ts new file mode 100644 index 000000000..dc05eb9e5 --- /dev/null +++ b/backend/src/db/migrations/20250404022310_ssh-ca-key-source.ts @@ -0,0 +1,32 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.SshCertificateAuthority, "keySource"))) { + await knex.schema.alterTable(TableName.SshCertificateAuthority, (t) => { + t.string("keySource"); + }); + + // Backfilling the keySource to internal + await knex(TableName.SshCertificateAuthority).update({ keySource: "internal" }); + + await knex.schema.alterTable(TableName.SshCertificateAuthority, (t) => { + t.string("keySource").notNullable().alter(); + }); + } + + if (await knex.schema.hasColumn(TableName.SshCertificate, "sshCaId")) { + await knex.schema.alterTable(TableName.SshCertificate, (t) => { + t.uuid("sshCaId").nullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.SshCertificateAuthority, "keySource")) { + await knex.schema.alterTable(TableName.SshCertificateAuthority, (t) => { + t.dropColumn("keySource"); + }); + } +} diff --git a/backend/src/db/migrations/20250405185753_ssh-mgmt-v2.ts b/backend/src/db/migrations/20250405185753_ssh-mgmt-v2.ts new file mode 100644 index 000000000..560fca9b1 --- /dev/null +++ b/backend/src/db/migrations/20250405185753_ssh-mgmt-v2.ts @@ -0,0 +1,93 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.SshHost))) { + await knex.schema.createTable(TableName.SshHost, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + t.string("projectId").notNullable(); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.string("hostname").notNullable(); + t.string("userCertTtl").notNullable(); + t.string("hostCertTtl").notNullable(); + t.uuid("userSshCaId").notNullable(); + t.foreign("userSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE"); + t.uuid("hostSshCaId").notNullable(); + t.foreign("hostSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE"); + t.unique(["projectId", "hostname"]); + }); + await createOnUpdateTrigger(knex, TableName.SshHost); + } + + if (!(await knex.schema.hasTable(TableName.SshHostLoginUser))) { + await knex.schema.createTable(TableName.SshHostLoginUser, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + t.uuid("sshHostId").notNullable(); + t.foreign("sshHostId").references("id").inTable(TableName.SshHost).onDelete("CASCADE"); + t.string("loginUser").notNullable(); // e.g. ubuntu, root, ec2-user, ... + t.unique(["sshHostId", "loginUser"]); + }); + await createOnUpdateTrigger(knex, TableName.SshHostLoginUser); + } + + if (!(await knex.schema.hasTable(TableName.SshHostLoginUserMapping))) { + await knex.schema.createTable(TableName.SshHostLoginUserMapping, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + t.uuid("sshHostLoginUserId").notNullable(); + t.foreign("sshHostLoginUserId").references("id").inTable(TableName.SshHostLoginUser).onDelete("CASCADE"); + t.uuid("userId").nullable(); + t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + t.unique(["sshHostLoginUserId", "userId"]); + }); + await createOnUpdateTrigger(knex, TableName.SshHostLoginUserMapping); + } + + if (!(await knex.schema.hasTable(TableName.ProjectSshConfig))) { + // new table to store configuration for projects of type SSH (i.e. Infisical SSH) + await knex.schema.createTable(TableName.ProjectSshConfig, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + t.string("projectId").notNullable(); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.uuid("defaultUserSshCaId"); + t.foreign("defaultUserSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE"); + t.uuid("defaultHostSshCaId"); + t.foreign("defaultHostSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE"); + }); + await createOnUpdateTrigger(knex, TableName.ProjectSshConfig); + } + + const hasColumn = await knex.schema.hasColumn(TableName.SshCertificate, "sshHostId"); + if (!hasColumn) { + await knex.schema.alterTable(TableName.SshCertificate, (t) => { + t.uuid("sshHostId").nullable(); + t.foreign("sshHostId").references("id").inTable(TableName.SshHost).onDelete("SET NULL"); + }); + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.ProjectSshConfig); + await dropOnUpdateTrigger(knex, TableName.ProjectSshConfig); + + await knex.schema.dropTableIfExists(TableName.SshHostLoginUserMapping); + await dropOnUpdateTrigger(knex, TableName.SshHostLoginUserMapping); + + await knex.schema.dropTableIfExists(TableName.SshHostLoginUser); + await dropOnUpdateTrigger(knex, TableName.SshHostLoginUser); + + const hasColumn = await knex.schema.hasColumn(TableName.SshCertificate, "sshHostId"); + if (hasColumn) { + await knex.schema.alterTable(TableName.SshCertificate, (t) => { + t.dropColumn("sshHostId"); + }); + } + + await knex.schema.dropTableIfExists(TableName.SshHost); + await dropOnUpdateTrigger(knex, TableName.SshHost); +} diff --git a/backend/src/db/migrations/20250409161555_add-dynamic-secret-to-resource-metadata.ts b/backend/src/db/migrations/20250409161555_add-dynamic-secret-to-resource-metadata.ts new file mode 100644 index 000000000..46df5cf80 --- /dev/null +++ b/backend/src/db/migrations/20250409161555_add-dynamic-secret-to-resource-metadata.ts @@ -0,0 +1,20 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.ResourceMetadata, "dynamicSecretId"))) { + await knex.schema.alterTable(TableName.ResourceMetadata, (tb) => { + tb.uuid("dynamicSecretId"); + tb.foreign("dynamicSecretId").references("id").inTable(TableName.DynamicSecret).onDelete("CASCADE"); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.ResourceMetadata, "dynamicSecretId")) { + await knex.schema.alterTable(TableName.ResourceMetadata, (tb) => { + tb.dropColumn("dynamicSecretId"); + }); + } +} diff --git a/backend/src/db/migrations/20250410203010_add-comment-to-access-request.ts b/backend/src/db/migrations/20250410203010_add-comment-to-access-request.ts new file mode 100644 index 000000000..0e0b46fd8 --- /dev/null +++ b/backend/src/db/migrations/20250410203010_add-comment-to-access-request.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasCol = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "note"); + if (!hasCol) { + await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => { + t.string("note").nullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasCol = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "note"); + if (hasCol) { + await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => { + t.dropColumn("note"); + }); + } +} diff --git a/backend/src/db/migrations/20250414203701_add-notification-flag-service-token.ts b/backend/src/db/migrations/20250414203701_add-notification-flag-service-token.ts new file mode 100644 index 000000000..9c5b4e730 --- /dev/null +++ b/backend/src/db/migrations/20250414203701_add-notification-flag-service-token.ts @@ -0,0 +1,27 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasCol = await knex.schema.hasColumn(TableName.ServiceToken, "expiryNotificationSent"); + if (!hasCol) { + await knex.schema.alterTable(TableName.ServiceToken, (t) => { + t.boolean("expiryNotificationSent").defaultTo(false); + }); + + // Update only tokens where expiresAt is before current time + await knex(TableName.ServiceToken) + .whereRaw(`${TableName.ServiceToken}."expiresAt" < NOW()`) + .whereNotNull("expiresAt") + .update({ expiryNotificationSent: true }); + } +} + +export async function down(knex: Knex): Promise { + const hasCol = await knex.schema.hasColumn(TableName.ServiceToken, "expiryNotificationSent"); + if (hasCol) { + await knex.schema.alterTable(TableName.ServiceToken, (t) => { + t.dropColumn("expiryNotificationSent"); + }); + } +} diff --git a/backend/src/db/migrations/20250414234624_add-project-delete-protection.ts b/backend/src/db/migrations/20250414234624_add-project-delete-protection.ts new file mode 100644 index 000000000..f33353e42 --- /dev/null +++ b/backend/src/db/migrations/20250414234624_add-project-delete-protection.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasCol = await knex.schema.hasColumn(TableName.Project, "hasDeleteProtection"); + if (!hasCol) { + await knex.schema.alterTable(TableName.Project, (t) => { + t.boolean("hasDeleteProtection").defaultTo(false); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasCol = await knex.schema.hasColumn(TableName.Project, "hasDeleteProtection"); + if (hasCol) { + await knex.schema.alterTable(TableName.Project, (t) => { + t.dropColumn("hasDeleteProtection"); + }); + } +} diff --git a/backend/src/db/migrations/20250415010421_increase-certificate-altnames-character-limit.ts b/backend/src/db/migrations/20250415010421_increase-certificate-altnames-character-limit.ts new file mode 100644 index 000000000..5703351a8 --- /dev/null +++ b/backend/src/db/migrations/20250415010421_increase-certificate-altnames-character-limit.ts @@ -0,0 +1,15 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable(TableName.Certificate, (t) => { + t.string("altNames", 4096).alter(); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable(TableName.Certificate, (t) => { + t.string("altNames").alter(); // Defaults to varchar(255) + }); +} diff --git a/backend/src/db/migrations/20250415020304_increase-kmip-certificate-altnames-character-limit.ts b/backend/src/db/migrations/20250415020304_increase-kmip-certificate-altnames-character-limit.ts new file mode 100644 index 000000000..e412d612a --- /dev/null +++ b/backend/src/db/migrations/20250415020304_increase-kmip-certificate-altnames-character-limit.ts @@ -0,0 +1,15 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable(TableName.KmipOrgServerCertificates, (t) => { + t.string("altNames", 4096).alter(); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable(TableName.KmipOrgServerCertificates, (t) => { + t.string("altNames").alter(); // Defaults to varchar(255) + }); +} diff --git a/backend/src/db/migrations/20250416113437_add-oidc-jwt-signature-algorithm.ts b/backend/src/db/migrations/20250416113437_add-oidc-jwt-signature-algorithm.ts new file mode 100644 index 000000000..5adbf71ec --- /dev/null +++ b/backend/src/db/migrations/20250416113437_add-oidc-jwt-signature-algorithm.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { OIDCJWTSignatureAlgorithm } from "@app/ee/services/oidc/oidc-config-types"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.OidcConfig, "jwtSignatureAlgorithm"))) { + await knex.schema.alterTable(TableName.OidcConfig, (t) => { + t.string("jwtSignatureAlgorithm").defaultTo(OIDCJWTSignatureAlgorithm.RS256).notNullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.OidcConfig, "jwtSignatureAlgorithm")) { + await knex.schema.alterTable(TableName.OidcConfig, (t) => { + t.dropColumn("jwtSignatureAlgorithm"); + }); + } +} diff --git a/backend/src/db/migrations/20250416145120_add-enable-bypass-org-auth-flag.ts b/backend/src/db/migrations/20250416145120_add-enable-bypass-org-auth-flag.ts new file mode 100644 index 000000000..fb9a12625 --- /dev/null +++ b/backend/src/db/migrations/20250416145120_add-enable-bypass-org-auth-flag.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.Organization, "bypassOrgAuthEnabled"))) { + await knex.schema.alterTable(TableName.Organization, (t) => { + t.boolean("bypassOrgAuthEnabled").defaultTo(false).notNullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.Organization, "bypassOrgAuthEnabled")) { + await knex.schema.alterTable(TableName.Organization, (t) => { + t.dropColumn("bypassOrgAuthEnabled"); + }); + } +} diff --git a/backend/src/db/migrations/20250421165221_fix-identites-and-user-deletion-secret-version-reference.ts b/backend/src/db/migrations/20250421165221_fix-identites-and-user-deletion-secret-version-reference.ts new file mode 100644 index 000000000..f5280c979 --- /dev/null +++ b/backend/src/db/migrations/20250421165221_fix-identites-and-user-deletion-secret-version-reference.ts @@ -0,0 +1,29 @@ +import { Knex } from "knex"; + +import { TableName } from "@app/db/schemas"; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable(TableName.SecretVersionV2, (table) => { + table.dropForeign(["userActorId"]); + table.dropForeign(["identityActorId"]); + }); + + await knex.schema.alterTable(TableName.SecretVersionV2, (table) => { + table.foreign("userActorId").references("id").inTable(TableName.Users).onDelete("SET NULL"); + + table.foreign("identityActorId").references("id").inTable(TableName.Identity).onDelete("SET NULL"); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable(TableName.SecretVersionV2, (table) => { + table.dropForeign(["userActorId"]); + table.dropForeign(["identityActorId"]); + }); + + await knex.schema.alterTable(TableName.SecretVersionV2, (table) => { + table.foreign("userActorId").references("id").inTable(TableName.Users); + + table.foreign("identityActorId").references("id").inTable(TableName.Identity); + }); +} diff --git a/backend/src/db/migrations/utils/env-config.ts b/backend/src/db/migrations/utils/env-config.ts new file mode 100644 index 000000000..8744308ab --- /dev/null +++ b/backend/src/db/migrations/utils/env-config.ts @@ -0,0 +1,53 @@ +import { z } from "zod"; + +import { zpStr } from "@app/lib/zod"; + +const envSchema = z + .object({ + DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")).default( + `postgresql://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}` + ), + DB_ROOT_CERT: zpStr(z.string().describe("Postgres database base64-encoded CA cert").optional()), + DB_HOST: zpStr(z.string().describe("Postgres database host").optional()), + DB_PORT: zpStr(z.string().describe("Postgres database port").optional()).default("5432"), + DB_USER: zpStr(z.string().describe("Postgres database username").optional()), + DB_PASSWORD: zpStr(z.string().describe("Postgres database password").optional()), + DB_NAME: zpStr(z.string().describe("Postgres database name").optional()), + // TODO(akhilmhdh): will be changed to one + ENCRYPTION_KEY: zpStr(z.string().optional()), + ROOT_ENCRYPTION_KEY: zpStr(z.string().optional()), + // HSM + HSM_LIB_PATH: zpStr(z.string().optional()), + HSM_PIN: zpStr(z.string().optional()), + HSM_KEY_LABEL: zpStr(z.string().optional()), + HSM_SLOT: z.coerce.number().optional().default(0) + }) + // To ensure that basic encryption is always possible. + .refine( + (data) => Boolean(data.ENCRYPTION_KEY) || Boolean(data.ROOT_ENCRYPTION_KEY), + "Either ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY must be defined." + ) + .transform((data) => ({ + ...data, + isHsmConfigured: + Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined + })); + +export type TMigrationEnvConfig = z.infer; + +export const getMigrationEnvConfig = () => { + const parsedEnv = envSchema.safeParse(process.env); + if (!parsedEnv.success) { + // eslint-disable-next-line no-console + console.error("Invalid environment variables. Check the error below"); + // eslint-disable-next-line no-console + console.error( + "Infisical now automatically runs database migrations during boot up, so you no longer need to run them separately." + ); + // eslint-disable-next-line no-console + console.error(parsedEnv.error.issues); + process.exit(-1); + } + + return Object.freeze(parsedEnv.data); +}; diff --git a/backend/src/db/migrations/utils/kms.ts b/backend/src/db/migrations/utils/kms.ts deleted file mode 100644 index 9ed090978..000000000 --- a/backend/src/db/migrations/utils/kms.ts +++ /dev/null @@ -1,105 +0,0 @@ -import slugify from "@sindresorhus/slugify"; -import { Knex } from "knex"; - -import { TableName } from "@app/db/schemas"; -import { randomSecureBytes } from "@app/lib/crypto"; -import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher"; -import { alphaNumericNanoId } from "@app/lib/nanoid"; - -const getInstanceRootKey = async (knex: Knex) => { - const encryptionKey = process.env.ENCRYPTION_KEY || process.env.ROOT_ENCRYPTION_KEY; - // if root key its base64 encoded - const isBase64 = !process.env.ENCRYPTION_KEY; - if (!encryptionKey) throw new Error("ENCRYPTION_KEY variable needed for migration"); - const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8"); - - const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000"; - const kmsRootConfig = await knex(TableName.KmsServerRootConfig).where({ id: KMS_ROOT_CONFIG_UUID }).first(); - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); - if (kmsRootConfig) { - const decryptedRootKey = cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer); - // set the flag so that other instancen nodes can start - return decryptedRootKey; - } - - const newRootKey = randomSecureBytes(32); - const encryptedRootKey = cipher.encrypt(newRootKey, encryptionKeyBuffer); - await knex(TableName.KmsServerRootConfig).insert({ - encryptedRootKey, - // eslint-disable-next-line - // @ts-ignore id is kept as fixed for idempotence and to avoid race condition - id: KMS_ROOT_CONFIG_UUID - }); - return encryptedRootKey; -}; - -export const getSecretManagerDataKey = async (knex: Knex, projectId: string) => { - const KMS_VERSION = "v01"; - const KMS_VERSION_BLOB_LENGTH = 3; - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); - const project = await knex(TableName.Project).where({ id: projectId }).first(); - if (!project) throw new Error("Missing project id"); - - const ROOT_ENCRYPTION_KEY = await getInstanceRootKey(knex); - - let secretManagerKmsKey; - const projectSecretManagerKmsId = project?.kmsSecretManagerKeyId; - if (projectSecretManagerKmsId) { - const kmsDoc = await knex(TableName.KmsKey) - .leftJoin(TableName.InternalKms, `${TableName.KmsKey}.id`, `${TableName.InternalKms}.kmsKeyId`) - .where({ [`${TableName.KmsKey}.id` as "id"]: projectSecretManagerKmsId }) - .first(); - if (!kmsDoc) throw new Error("missing kms"); - secretManagerKmsKey = cipher.decrypt(kmsDoc.encryptedKey, ROOT_ENCRYPTION_KEY); - } else { - const [kmsDoc] = await knex(TableName.KmsKey) - .insert({ - name: slugify(alphaNumericNanoId(8).toLowerCase()), - orgId: project.orgId, - isReserved: false - }) - .returning("*"); - - secretManagerKmsKey = randomSecureBytes(32); - const encryptedKeyMaterial = cipher.encrypt(secretManagerKmsKey, ROOT_ENCRYPTION_KEY); - await knex(TableName.InternalKms).insert({ - version: 1, - encryptedKey: encryptedKeyMaterial, - encryptionAlgorithm: SymmetricEncryption.AES_GCM_256, - kmsKeyId: kmsDoc.id - }); - } - - const encryptedSecretManagerDataKey = project?.kmsSecretManagerEncryptedDataKey; - let dataKey: Buffer; - if (!encryptedSecretManagerDataKey) { - dataKey = randomSecureBytes(); - // the below versioning we do it automatically in kms service - const unversionedDataKey = cipher.encrypt(dataKey, secretManagerKmsKey); - const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3 - await knex(TableName.Project) - .where({ id: projectId }) - .update({ - kmsSecretManagerEncryptedDataKey: Buffer.concat([unversionedDataKey, versionBlob]) - }); - } else { - const cipherTextBlob = encryptedSecretManagerDataKey.subarray(0, -KMS_VERSION_BLOB_LENGTH); - dataKey = cipher.decrypt(cipherTextBlob, secretManagerKmsKey); - } - - return { - encryptor: ({ plainText }: { plainText: Buffer }) => { - const encryptedPlainTextBlob = cipher.encrypt(plainText, dataKey); - - // Buffer#1 encrypted text + Buffer#2 version number - const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3 - const cipherTextBlob = Buffer.concat([encryptedPlainTextBlob, versionBlob]); - return { cipherTextBlob }; - }, - decryptor: ({ cipherTextBlob: versionedCipherTextBlob }: { cipherTextBlob: Buffer }) => { - const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH); - const decryptedBlob = cipher.decrypt(cipherTextBlob, dataKey); - return decryptedBlob; - } - }; -}; diff --git a/backend/src/db/migrations/utils/ring-buffer.ts b/backend/src/db/migrations/utils/ring-buffer.ts new file mode 100644 index 000000000..8e5c58662 --- /dev/null +++ b/backend/src/db/migrations/utils/ring-buffer.ts @@ -0,0 +1,19 @@ +export const createCircularCache = (bufferSize = 10) => { + const bufferItems: { id: string; item: T }[] = []; + let bufferIndex = 0; + + const push = (id: string, item: T) => { + if (bufferItems.length < bufferSize) { + bufferItems.push({ id, item }); + } else { + bufferItems[bufferIndex] = { id, item }; + } + bufferIndex = (bufferIndex + 1) % bufferSize; + }; + + const getItem = (id: string) => { + return bufferItems.find((i) => i.id === id)?.item; + }; + + return { push, getItem }; +}; diff --git a/backend/src/db/migrations/utils/services.ts b/backend/src/db/migrations/utils/services.ts new file mode 100644 index 000000000..731f703e2 --- /dev/null +++ b/backend/src/db/migrations/utils/services.ts @@ -0,0 +1,52 @@ +import { Knex } from "knex"; + +import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns"; +import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; +import { TKeyStoreFactory } from "@app/keystore/keystore"; +import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal"; +import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal"; +import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; +import { kmsServiceFactory } from "@app/services/kms/kms-service"; +import { orgDALFactory } from "@app/services/org/org-dal"; +import { projectDALFactory } from "@app/services/project/project-dal"; + +import { TMigrationEnvConfig } from "./env-config"; + +type TDependencies = { + envConfig: TMigrationEnvConfig; + db: Knex; + keyStore: TKeyStoreFactory; +}; + +export const getMigrationEncryptionServices = async ({ envConfig, db, keyStore }: TDependencies) => { + // eslint-disable-next-line no-param-reassign + const hsmModule = initializeHsmModule(envConfig); + hsmModule.initialize(); + + const hsmService = hsmServiceFactory({ + hsmModule: hsmModule.getModule(), + envConfig + }); + + const orgDAL = orgDALFactory(db); + const kmsRootConfigDAL = kmsRootConfigDALFactory(db); + const kmsDAL = kmskeyDALFactory(db); + const internalKmsDAL = internalKmsDALFactory(db); + const projectDAL = projectDALFactory(db); + + const kmsService = kmsServiceFactory({ + kmsRootConfigDAL, + keyStore, + kmsDAL, + internalKmsDAL, + orgDAL, + projectDAL, + hsmService, + envConfig + }); + + await hsmService.startService(); + await kmsService.startService(); + + return { kmsService }; +}; diff --git a/backend/src/db/rename-migrations-to-mjs.ts b/backend/src/db/rename-migrations-to-mjs.ts new file mode 100644 index 000000000..d09b5097d --- /dev/null +++ b/backend/src/db/rename-migrations-to-mjs.ts @@ -0,0 +1,56 @@ +import path from "node:path"; + +import dotenv from "dotenv"; + +import { initAuditLogDbConnection, initDbConnection } from "./instance"; + +const isProduction = process.env.NODE_ENV === "production"; + +// Update with your config settings. . +dotenv.config({ + path: path.join(__dirname, "../../../.env.migration") +}); +dotenv.config({ + path: path.join(__dirname, "../../../.env") +}); + +const runRename = async () => { + if (!isProduction) return; + const migrationTable = "infisical_migrations"; + const applicationDb = initDbConnection({ + dbConnectionUri: process.env.DB_CONNECTION_URI as string, + dbRootCert: process.env.DB_ROOT_CERT + }); + + const auditLogDb = process.env.AUDIT_LOGS_DB_CONNECTION_URI + ? initAuditLogDbConnection({ + dbConnectionUri: process.env.AUDIT_LOGS_DB_CONNECTION_URI, + dbRootCert: process.env.AUDIT_LOGS_DB_ROOT_CERT + }) + : undefined; + + const hasMigrationTable = await applicationDb.schema.hasTable(migrationTable); + if (hasMigrationTable) { + const firstFile = (await applicationDb(migrationTable).where({}).first()) as { name: string }; + if (firstFile?.name?.includes(".ts")) { + await applicationDb(migrationTable).update({ + name: applicationDb.raw("REPLACE(name, '.ts', '.mjs')") + }); + } + } + if (auditLogDb) { + const hasMigrationTableInAuditLog = await auditLogDb.schema.hasTable(migrationTable); + if (hasMigrationTableInAuditLog) { + const firstFile = (await auditLogDb(migrationTable).where({}).first()) as { name: string }; + if (firstFile?.name?.includes(".ts")) { + await auditLogDb(migrationTable).update({ + name: auditLogDb.raw("REPLACE(name, '.ts', '.mjs')") + }); + } + } + } + await applicationDb.destroy(); + await auditLogDb?.destroy(); +}; + +void runRename(); diff --git a/backend/src/db/schemas/access-approval-policies.ts b/backend/src/db/schemas/access-approval-policies.ts index f4c525a4f..19a98675f 100644 --- a/backend/src/db/schemas/access-approval-policies.ts +++ b/backend/src/db/schemas/access-approval-policies.ts @@ -15,7 +15,9 @@ export const AccessApprovalPoliciesSchema = z.object({ envId: z.string().uuid(), createdAt: z.date(), updatedAt: z.date(), - enforcementLevel: z.string().default("hard") + enforcementLevel: z.string().default("hard"), + deletedAt: z.date().nullable().optional(), + allowedSelfApprovals: z.boolean().default(true) }); export type TAccessApprovalPolicies = z.infer; diff --git a/backend/src/db/schemas/access-approval-requests.ts b/backend/src/db/schemas/access-approval-requests.ts index 0b20202f5..bfe990b3a 100644 --- a/backend/src/db/schemas/access-approval-requests.ts +++ b/backend/src/db/schemas/access-approval-requests.ts @@ -17,7 +17,8 @@ export const AccessApprovalRequestsSchema = z.object({ permissions: z.unknown(), createdAt: z.date(), updatedAt: z.date(), - requestedByUserId: z.string().uuid() + requestedByUserId: z.string().uuid(), + note: z.string().nullable().optional() }); export type TAccessApprovalRequests = z.infer; diff --git a/backend/src/db/schemas/app-connections.ts b/backend/src/db/schemas/app-connections.ts new file mode 100644 index 000000000..ee4282b73 --- /dev/null +++ b/backend/src/db/schemas/app-connections.ts @@ -0,0 +1,28 @@ +// 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 AppConnectionsSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + description: z.string().nullable().optional(), + app: z.string(), + method: z.string(), + encryptedCredentials: zodBuffer, + version: z.number().default(1), + orgId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + isPlatformManagedCredentials: z.boolean().default(false).nullable().optional() +}); + +export type TAppConnections = z.infer; +export type TAppConnectionsInsert = Omit, TImmutableDBKeys>; +export type TAppConnectionsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/certificate-template-est-configs.ts b/backend/src/db/schemas/certificate-template-est-configs.ts index 654b5413a..262f22b06 100644 --- a/backend/src/db/schemas/certificate-template-est-configs.ts +++ b/backend/src/db/schemas/certificate-template-est-configs.ts @@ -12,11 +12,12 @@ import { TImmutableDBKeys } from "./models"; export const CertificateTemplateEstConfigsSchema = z.object({ id: z.string().uuid(), certificateTemplateId: z.string().uuid(), - encryptedCaChain: zodBuffer, + encryptedCaChain: zodBuffer.nullable().optional(), hashedPassphrase: z.string(), isEnabled: z.boolean(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + disableBootstrapCertValidation: z.boolean().default(false) }); export type TCertificateTemplateEstConfigs = z.infer; diff --git a/backend/src/db/schemas/dynamic-secrets.ts b/backend/src/db/schemas/dynamic-secrets.ts index b27da396c..913a6d475 100644 --- a/backend/src/db/schemas/dynamic-secrets.ts +++ b/backend/src/db/schemas/dynamic-secrets.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const DynamicSecretsSchema = z.object({ @@ -14,16 +16,18 @@ export const DynamicSecretsSchema = z.object({ type: z.string(), defaultTTL: z.string(), maxTTL: z.string().nullable().optional(), - inputIV: z.string(), - inputCiphertext: z.string(), - inputTag: z.string(), + inputIV: z.string().nullable().optional(), + inputCiphertext: z.string().nullable().optional(), + inputTag: z.string().nullable().optional(), algorithm: z.string().default("aes-256-gcm"), keyEncoding: z.string().default("utf8"), folderId: z.string().uuid(), status: z.string().nullable().optional(), statusDetails: z.string().nullable().optional(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + encryptedInput: zodBuffer, + projectGatewayId: z.string().uuid().nullable().optional() }); export type TDynamicSecrets = z.infer; diff --git a/backend/src/db/schemas/gateways.ts b/backend/src/db/schemas/gateways.ts new file mode 100644 index 000000000..30f5f25e0 --- /dev/null +++ b/backend/src/db/schemas/gateways.ts @@ -0,0 +1,29 @@ +// 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 GatewaysSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + serialNumber: z.string(), + keyAlgorithm: z.string(), + issuedAt: z.date(), + expiration: z.date(), + heartbeat: z.date().nullable().optional(), + relayAddress: zodBuffer, + orgGatewayRootCaId: z.string().uuid(), + identityId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TGateways = z.infer; +export type TGatewaysInsert = Omit, TImmutableDBKeys>; +export type TGatewaysUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/identity-gcp-auths.ts b/backend/src/db/schemas/identity-gcp-auths.ts index 65c7db837..208058f60 100644 --- a/backend/src/db/schemas/identity-gcp-auths.ts +++ b/backend/src/db/schemas/identity-gcp-auths.ts @@ -17,9 +17,9 @@ export const IdentityGcpAuthsSchema = z.object({ updatedAt: z.date(), identityId: z.string().uuid(), type: z.string(), - allowedServiceAccounts: z.string(), - allowedProjects: z.string(), - allowedZones: z.string() + allowedServiceAccounts: z.string().nullable().optional(), + allowedProjects: z.string().nullable().optional(), + allowedZones: z.string().nullable().optional() }); export type TIdentityGcpAuths = z.infer; diff --git a/backend/src/db/schemas/identity-jwt-auths.ts b/backend/src/db/schemas/identity-jwt-auths.ts new file mode 100644 index 000000000..1d3ea9c03 --- /dev/null +++ b/backend/src/db/schemas/identity-jwt-auths.ts @@ -0,0 +1,33 @@ +// 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 IdentityJwtAuthsSchema = z.object({ + id: z.string().uuid(), + accessTokenTTL: z.coerce.number().default(7200), + accessTokenMaxTTL: z.coerce.number().default(7200), + accessTokenNumUsesLimit: z.coerce.number().default(0), + accessTokenTrustedIps: z.unknown(), + identityId: z.string().uuid(), + configurationType: z.string(), + jwksUrl: z.string(), + encryptedJwksCaCert: zodBuffer, + encryptedPublicKeys: zodBuffer, + boundIssuer: z.string(), + boundAudiences: z.string(), + boundClaims: z.unknown(), + boundSubject: z.string(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TIdentityJwtAuths = z.infer; +export type TIdentityJwtAuthsInsert = Omit, TImmutableDBKeys>; +export type TIdentityJwtAuthsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/identity-kubernetes-auths.ts b/backend/src/db/schemas/identity-kubernetes-auths.ts index ed99dec86..448cec386 100644 --- a/backend/src/db/schemas/identity-kubernetes-auths.ts +++ b/backend/src/db/schemas/identity-kubernetes-auths.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const IdentityKubernetesAuthsSchema = z.object({ @@ -17,15 +19,17 @@ export const IdentityKubernetesAuthsSchema = z.object({ updatedAt: z.date(), identityId: z.string().uuid(), kubernetesHost: z.string(), - encryptedCaCert: z.string(), - caCertIV: z.string(), - caCertTag: z.string(), - encryptedTokenReviewerJwt: z.string(), - tokenReviewerJwtIV: z.string(), - tokenReviewerJwtTag: z.string(), + encryptedCaCert: z.string().nullable().optional(), + caCertIV: z.string().nullable().optional(), + caCertTag: z.string().nullable().optional(), + encryptedTokenReviewerJwt: z.string().nullable().optional(), + tokenReviewerJwtIV: z.string().nullable().optional(), + tokenReviewerJwtTag: z.string().nullable().optional(), allowedNamespaces: z.string(), allowedNames: z.string(), - allowedAudience: z.string() + allowedAudience: z.string(), + encryptedKubernetesTokenReviewerJwt: zodBuffer.nullable().optional(), + encryptedKubernetesCaCertificate: zodBuffer.nullable().optional() }); export type TIdentityKubernetesAuths = z.infer; diff --git a/backend/src/db/schemas/identity-oidc-auths.ts b/backend/src/db/schemas/identity-oidc-auths.ts index 3d7d38c41..03bfcf40a 100644 --- a/backend/src/db/schemas/identity-oidc-auths.ts +++ b/backend/src/db/schemas/identity-oidc-auths.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const IdentityOidcAuthsSchema = z.object({ @@ -15,15 +17,17 @@ export const IdentityOidcAuthsSchema = z.object({ accessTokenTrustedIps: z.unknown(), identityId: z.string().uuid(), oidcDiscoveryUrl: z.string(), - encryptedCaCert: z.string(), - caCertIV: z.string(), - caCertTag: z.string(), + encryptedCaCert: z.string().nullable().optional(), + caCertIV: z.string().nullable().optional(), + caCertTag: z.string().nullable().optional(), boundIssuer: z.string(), boundAudiences: z.string(), boundClaims: z.unknown(), boundSubject: z.string().nullable().optional(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + encryptedCaCertificate: zodBuffer.nullable().optional(), + claimMetadataMapping: z.unknown().nullable().optional() }); export type TIdentityOidcAuths = z.infer; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 680d8df5b..8543417cf 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -3,6 +3,7 @@ export * from "./access-approval-policies-approvers"; export * from "./access-approval-requests"; export * from "./access-approval-requests-reviewers"; export * from "./api-keys"; +export * from "./app-connections"; export * from "./audit-log-streams"; export * from "./audit-logs"; export * from "./auth-token-sessions"; @@ -19,7 +20,9 @@ export * from "./certificate-templates"; export * from "./certificates"; export * from "./dynamic-secret-leases"; export * from "./dynamic-secrets"; +export * from "./external-group-org-role-mappings"; export * from "./external-kms"; +export * from "./gateways"; export * from "./git-app-install-sessions"; export * from "./git-app-org"; export * from "./group-project-membership-roles"; @@ -30,6 +33,7 @@ export * from "./identity-access-tokens"; export * from "./identity-aws-auths"; export * from "./identity-azure-auths"; export * from "./identity-gcp-auths"; +export * from "./identity-jwt-auths"; export * from "./identity-kubernetes-auths"; export * from "./identity-metadata"; export * from "./identity-oidc-auths"; @@ -44,6 +48,10 @@ export * from "./incident-contacts"; export * from "./integration-auths"; export * from "./integrations"; export * from "./internal-kms"; +export * from "./kmip-client-certificates"; +export * from "./kmip-clients"; +export * from "./kmip-org-configs"; +export * from "./kmip-org-server-certificates"; export * from "./kms-key-versions"; export * from "./kms-keys"; export * from "./kms-root-config"; @@ -52,6 +60,7 @@ export * from "./ldap-group-maps"; export * from "./models"; export * from "./oidc-configs"; export * from "./org-bots"; +export * from "./org-gateway-config"; export * from "./org-memberships"; export * from "./org-roles"; export * from "./organizations"; @@ -60,15 +69,19 @@ export * from "./pki-collection-items"; export * from "./pki-collections"; export * from "./project-bots"; export * from "./project-environments"; +export * from "./project-gateways"; export * from "./project-keys"; export * from "./project-memberships"; export * from "./project-roles"; export * from "./project-slack-configs"; +export * from "./project-split-backfill-ids"; +export * from "./project-ssh-configs"; export * from "./project-templates"; export * from "./project-user-additional-privilege"; export * from "./project-user-membership-roles"; export * from "./projects"; export * from "./rate-limit"; +export * from "./resource-metadata"; export * from "./saml-configs"; export * from "./scim-tokens"; export * from "./secret-approval-policies"; @@ -87,13 +100,16 @@ export * from "./secret-references"; export * from "./secret-references-v2"; export * from "./secret-rotation-output-v2"; export * from "./secret-rotation-outputs"; +export * from "./secret-rotation-v2-secret-mappings"; export * from "./secret-rotations"; +export * from "./secret-rotations-v2"; export * from "./secret-scanning-git-risks"; export * from "./secret-sharing"; export * from "./secret-snapshot-folders"; export * from "./secret-snapshot-secrets"; export * from "./secret-snapshot-secrets-v2"; export * from "./secret-snapshots"; +export * from "./secret-syncs"; export * from "./secret-tag-junction"; export * from "./secret-tags"; export * from "./secret-v2-tag-junction"; @@ -105,7 +121,16 @@ export * from "./secrets"; export * from "./secrets-v2"; export * from "./service-tokens"; export * from "./slack-integrations"; +export * from "./ssh-certificate-authorities"; +export * from "./ssh-certificate-authority-secrets"; +export * from "./ssh-certificate-bodies"; +export * from "./ssh-certificate-templates"; +export * from "./ssh-certificates"; +export * from "./ssh-host-login-user-mappings"; +export * from "./ssh-host-login-users"; +export * from "./ssh-hosts"; export * from "./super-admin"; +export * from "./totp-configs"; export * from "./trusted-ips"; export * from "./user-actions"; export * from "./user-aliases"; diff --git a/backend/src/db/schemas/kmip-client-certificates.ts b/backend/src/db/schemas/kmip-client-certificates.ts new file mode 100644 index 000000000..a42d94a98 --- /dev/null +++ b/backend/src/db/schemas/kmip-client-certificates.ts @@ -0,0 +1,23 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const KmipClientCertificatesSchema = z.object({ + id: z.string().uuid(), + kmipClientId: z.string().uuid(), + serialNumber: z.string(), + keyAlgorithm: z.string(), + issuedAt: z.date(), + expiration: z.date() +}); + +export type TKmipClientCertificates = z.infer; +export type TKmipClientCertificatesInsert = Omit, TImmutableDBKeys>; +export type TKmipClientCertificatesUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/kmip-clients.ts b/backend/src/db/schemas/kmip-clients.ts new file mode 100644 index 000000000..eb8f31bfb --- /dev/null +++ b/backend/src/db/schemas/kmip-clients.ts @@ -0,0 +1,20 @@ +// 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 KmipClientsSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + permissions: z.string().array().nullable().optional(), + description: z.string().nullable().optional(), + projectId: z.string() +}); + +export type TKmipClients = z.infer; +export type TKmipClientsInsert = Omit, TImmutableDBKeys>; +export type TKmipClientsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/kmip-org-configs.ts b/backend/src/db/schemas/kmip-org-configs.ts new file mode 100644 index 000000000..e75d76413 --- /dev/null +++ b/backend/src/db/schemas/kmip-org-configs.ts @@ -0,0 +1,39 @@ +// 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 KmipOrgConfigsSchema = z.object({ + id: z.string().uuid(), + orgId: z.string().uuid(), + caKeyAlgorithm: z.string(), + rootCaIssuedAt: z.date(), + rootCaExpiration: z.date(), + rootCaSerialNumber: z.string(), + encryptedRootCaCertificate: zodBuffer, + encryptedRootCaPrivateKey: zodBuffer, + serverIntermediateCaIssuedAt: z.date(), + serverIntermediateCaExpiration: z.date(), + serverIntermediateCaSerialNumber: z.string().nullable().optional(), + encryptedServerIntermediateCaCertificate: zodBuffer, + encryptedServerIntermediateCaChain: zodBuffer, + encryptedServerIntermediateCaPrivateKey: zodBuffer, + clientIntermediateCaIssuedAt: z.date(), + clientIntermediateCaExpiration: z.date(), + clientIntermediateCaSerialNumber: z.string(), + encryptedClientIntermediateCaCertificate: zodBuffer, + encryptedClientIntermediateCaChain: zodBuffer, + encryptedClientIntermediateCaPrivateKey: zodBuffer, + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TKmipOrgConfigs = z.infer; +export type TKmipOrgConfigsInsert = Omit, TImmutableDBKeys>; +export type TKmipOrgConfigsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/kmip-org-server-certificates.ts b/backend/src/db/schemas/kmip-org-server-certificates.ts new file mode 100644 index 000000000..66e5dcbd6 --- /dev/null +++ b/backend/src/db/schemas/kmip-org-server-certificates.ts @@ -0,0 +1,29 @@ +// 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 KmipOrgServerCertificatesSchema = z.object({ + id: z.string().uuid(), + orgId: z.string().uuid(), + commonName: z.string(), + altNames: z.string(), + serialNumber: z.string(), + keyAlgorithm: z.string(), + issuedAt: z.date(), + expiration: z.date(), + encryptedCertificate: zodBuffer, + encryptedChain: zodBuffer +}); + +export type TKmipOrgServerCertificates = z.infer; +export type TKmipOrgServerCertificatesInsert = Omit, TImmutableDBKeys>; +export type TKmipOrgServerCertificatesUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/kms-keys.ts b/backend/src/db/schemas/kms-keys.ts index dffaeec24..ccb779d57 100644 --- a/backend/src/db/schemas/kms-keys.ts +++ b/backend/src/db/schemas/kms-keys.ts @@ -17,7 +17,7 @@ export const KmsKeysSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), projectId: z.string().nullable().optional(), - slug: z.string().nullable().optional() + keyUsage: z.string().default("encrypt-decrypt") }); export type TKmsKeys = z.infer; diff --git a/backend/src/db/schemas/kms-root-config.ts b/backend/src/db/schemas/kms-root-config.ts index d2c0edbc5..c9c1ebda5 100644 --- a/backend/src/db/schemas/kms-root-config.ts +++ b/backend/src/db/schemas/kms-root-config.ts @@ -11,7 +11,10 @@ import { TImmutableDBKeys } from "./models"; export const KmsRootConfigSchema = z.object({ id: z.string().uuid(), - encryptedRootKey: zodBuffer + encryptedRootKey: zodBuffer, + encryptionStrategy: z.string().default("SOFTWARE").nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() }); export type TKmsRootConfig = z.infer; diff --git a/backend/src/db/schemas/ldap-configs.ts b/backend/src/db/schemas/ldap-configs.ts index 460c2cff6..778e7be6e 100644 --- a/backend/src/db/schemas/ldap-configs.ts +++ b/backend/src/db/schemas/ldap-configs.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const LdapConfigsSchema = z.object({ @@ -12,22 +14,25 @@ export const LdapConfigsSchema = z.object({ orgId: z.string().uuid(), isActive: z.boolean(), url: z.string(), - encryptedBindDN: z.string(), - bindDNIV: z.string(), - bindDNTag: z.string(), - encryptedBindPass: z.string(), - bindPassIV: z.string(), - bindPassTag: z.string(), + encryptedBindDN: z.string().nullable().optional(), + bindDNIV: z.string().nullable().optional(), + bindDNTag: z.string().nullable().optional(), + encryptedBindPass: z.string().nullable().optional(), + bindPassIV: z.string().nullable().optional(), + bindPassTag: z.string().nullable().optional(), searchBase: z.string(), - encryptedCACert: z.string(), - caCertIV: z.string(), - caCertTag: z.string(), + encryptedCACert: z.string().nullable().optional(), + caCertIV: z.string().nullable().optional(), + caCertTag: z.string().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), groupSearchBase: z.string().default(""), groupSearchFilter: z.string().default(""), searchFilter: z.string().default(""), - uniqueUserAttribute: z.string().default("") + uniqueUserAttribute: z.string().default(""), + encryptedLdapBindDN: zodBuffer, + encryptedLdapBindPass: zodBuffer, + encryptedLdapCaCertificate: zodBuffer.nullable().optional() }); export type TLdapConfigs = z.infer; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 5bab447fd..95561c14a 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -2,6 +2,14 @@ import { z } from "zod"; export enum TableName { Users = "users", + SshHost = "ssh_hosts", + SshHostLoginUser = "ssh_host_login_users", + SshHostLoginUserMapping = "ssh_host_login_user_mappings", + SshCertificateAuthority = "ssh_certificate_authorities", + SshCertificateAuthoritySecret = "ssh_certificate_authority_secrets", + SshCertificateTemplate = "ssh_certificate_templates", + SshCertificate = "ssh_certificates", + SshCertificateBody = "ssh_certificate_bodies", CertificateAuthority = "certificate_authorities", CertificateTemplateEstConfig = "certificate_template_est_configs", CertificateAuthorityCert = "certificate_authority_certs", @@ -33,6 +41,7 @@ export enum TableName { SuperAdmin = "super_admin", RateLimit = "rate_limit", ApiKey = "api_keys", + ProjectSshConfig = "project_ssh_configs", Project = "projects", ProjectBot = "project_bots", Environment = "project_environments", @@ -68,12 +77,14 @@ export enum TableName { IdentityUaClientSecret = "identity_ua_client_secrets", IdentityAwsAuth = "identity_aws_auths", IdentityOidcAuth = "identity_oidc_auths", + IdentityJwtAuth = "identity_jwt_auths", IdentityOrgMembership = "identity_org_memberships", IdentityProjectMembership = "identity_project_memberships", IdentityProjectMembershipRole = "identity_project_membership_role", IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege", // used by both identity and users IdentityMetadata = "identity_metadata", + ResourceMetadata = "resource_metadata", ScimToken = "scim_tokens", AccessApprovalPolicy = "access_approval_policies", AccessApprovalPolicyApprover = "access_approval_policies_approvers", @@ -105,6 +116,11 @@ export enum TableName { SecretApprovalRequestSecretV2 = "secret_approval_requests_secrets_v2", SecretApprovalRequestSecretTagV2 = "secret_approval_request_secret_tags_v2", SnapshotSecretV2 = "secret_snapshot_secrets_v2", + ProjectSplitBackfillIds = "project_split_backfill_ids", + // Gateway + OrgGatewayConfig = "org_gateway_config", + Gateway = "gateways", + ProjectGateway = "project_gateways", // junction tables with tags SecretV2JnTag = "secret_v2_tag_junction", JnSecretTag = "secret_tag_junction", @@ -117,11 +133,20 @@ export enum TableName { ExternalKms = "external_kms", InternalKms = "internal_kms", InternalKmsKeyVersion = "internal_kms_key_version", + TotpConfig = "totp_configs", // @depreciated KmsKeyVersion = "kms_key_versions", WorkflowIntegrations = "workflow_integrations", SlackIntegrations = "slack_integrations", - ProjectSlackConfigs = "project_slack_configs" + ProjectSlackConfigs = "project_slack_configs", + AppConnection = "app_connections", + SecretSync = "secret_syncs", + KmipClient = "kmip_clients", + KmipOrgConfig = "kmip_org_configs", + KmipOrgServerCertificates = "kmip_org_server_certificates", + KmipClientCertificates = "kmip_client_certificates", + SecretRotationV2 = "secret_rotations_v2", + SecretRotationV2SecretMapping = "secret_rotation_v2_secret_mappings" } export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt"; @@ -195,5 +220,27 @@ export enum IdentityAuthMethod { GCP_AUTH = "gcp-auth", AWS_AUTH = "aws-auth", AZURE_AUTH = "azure-auth", - OIDC_AUTH = "oidc-auth" + OIDC_AUTH = "oidc-auth", + JWT_AUTH = "jwt-auth" +} + +export enum ProjectType { + SecretManager = "secret-manager", + CertificateManager = "cert-manager", + KMS = "kms", + SSH = "ssh" +} + +export enum ActionProjectType { + SecretManager = ProjectType.SecretManager, + CertificateManager = ProjectType.CertificateManager, + KMS = ProjectType.KMS, + SSH = ProjectType.SSH, + // project operations that happen on all types + Any = "any" +} + +export enum SortDirection { + ASC = "asc", + DESC = "desc" } diff --git a/backend/src/db/schemas/oidc-configs.ts b/backend/src/db/schemas/oidc-configs.ts index e8030267d..181df25f0 100644 --- a/backend/src/db/schemas/oidc-configs.ts +++ b/backend/src/db/schemas/oidc-configs.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const OidcConfigsSchema = z.object({ @@ -15,19 +17,23 @@ export const OidcConfigsSchema = z.object({ jwksUri: z.string().nullable().optional(), tokenEndpoint: z.string().nullable().optional(), userinfoEndpoint: z.string().nullable().optional(), - encryptedClientId: z.string(), + encryptedClientId: z.string().nullable().optional(), configurationType: z.string(), - clientIdIV: z.string(), - clientIdTag: z.string(), - encryptedClientSecret: z.string(), - clientSecretIV: z.string(), - clientSecretTag: z.string(), + clientIdIV: z.string().nullable().optional(), + clientIdTag: z.string().nullable().optional(), + encryptedClientSecret: z.string().nullable().optional(), + clientSecretIV: z.string().nullable().optional(), + clientSecretTag: z.string().nullable().optional(), allowedEmailDomains: z.string().nullable().optional(), isActive: z.boolean(), createdAt: z.date(), updatedAt: z.date(), orgId: z.string().uuid(), - lastUsed: z.date().nullable().optional() + lastUsed: z.date().nullable().optional(), + encryptedOidcClientId: zodBuffer, + encryptedOidcClientSecret: zodBuffer, + manageGroupMemberships: z.boolean().default(false), + jwtSignatureAlgorithm: z.string().default("RS256") }); export type TOidcConfigs = z.infer; diff --git a/backend/src/db/schemas/org-gateway-config.ts b/backend/src/db/schemas/org-gateway-config.ts new file mode 100644 index 000000000..ddf18c3fc --- /dev/null +++ b/backend/src/db/schemas/org-gateway-config.ts @@ -0,0 +1,43 @@ +// 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 OrgGatewayConfigSchema = z.object({ + id: z.string().uuid(), + rootCaKeyAlgorithm: z.string(), + rootCaIssuedAt: z.date(), + rootCaExpiration: z.date(), + rootCaSerialNumber: z.string(), + encryptedRootCaCertificate: zodBuffer, + encryptedRootCaPrivateKey: zodBuffer, + clientCaIssuedAt: z.date(), + clientCaExpiration: z.date(), + clientCaSerialNumber: z.string().nullable().optional(), + encryptedClientCaCertificate: zodBuffer, + encryptedClientCaPrivateKey: zodBuffer, + clientCertSerialNumber: z.string(), + clientCertKeyAlgorithm: z.string(), + clientCertIssuedAt: z.date(), + clientCertExpiration: z.date(), + encryptedClientCertificate: zodBuffer, + encryptedClientPrivateKey: zodBuffer, + gatewayCaIssuedAt: z.date(), + gatewayCaExpiration: z.date(), + gatewayCaSerialNumber: z.string(), + encryptedGatewayCaCertificate: zodBuffer, + encryptedGatewayCaPrivateKey: zodBuffer, + orgId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TOrgGatewayConfig = z.infer; +export type TOrgGatewayConfigInsert = Omit, TImmutableDBKeys>; +export type TOrgGatewayConfigUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/organizations.ts b/backend/src/db/schemas/organizations.ts index 31de98168..eea1808e0 100644 --- a/backend/src/db/schemas/organizations.ts +++ b/backend/src/db/schemas/organizations.ts @@ -21,7 +21,13 @@ export const OrganizationsSchema = z.object({ kmsDefaultKeyId: z.string().uuid().nullable().optional(), kmsEncryptedDataKey: zodBuffer.nullable().optional(), defaultMembershipRole: z.string().default("member"), - enforceMfa: z.boolean().default(false) + enforceMfa: z.boolean().default(false), + selectedMfaMethod: z.string().nullable().optional(), + allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional(), + shouldUseNewPrivilegeSystem: z.boolean().default(true), + privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(), + privilegeUpgradeInitiatedAt: z.date().nullable().optional(), + bypassOrgAuthEnabled: z.boolean().default(false) }); export type TOrganizations = z.infer; diff --git a/backend/src/db/schemas/project-gateways.ts b/backend/src/db/schemas/project-gateways.ts new file mode 100644 index 000000000..f4b572661 --- /dev/null +++ b/backend/src/db/schemas/project-gateways.ts @@ -0,0 +1,20 @@ +// 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 ProjectGatewaysSchema = z.object({ + id: z.string().uuid(), + projectId: z.string(), + gatewayId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TProjectGateways = z.infer; +export type TProjectGatewaysInsert = Omit, TImmutableDBKeys>; +export type TProjectGatewaysUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/project-split-backfill-ids.ts b/backend/src/db/schemas/project-split-backfill-ids.ts new file mode 100644 index 000000000..182d85049 --- /dev/null +++ b/backend/src/db/schemas/project-split-backfill-ids.ts @@ -0,0 +1,21 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const ProjectSplitBackfillIdsSchema = z.object({ + id: z.string().uuid(), + sourceProjectId: z.string(), + destinationProjectType: z.string(), + destinationProjectId: z.string() +}); + +export type TProjectSplitBackfillIds = z.infer; +export type TProjectSplitBackfillIdsInsert = Omit, TImmutableDBKeys>; +export type TProjectSplitBackfillIdsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/project-ssh-configs.ts b/backend/src/db/schemas/project-ssh-configs.ts new file mode 100644 index 000000000..d0be89ee3 --- /dev/null +++ b/backend/src/db/schemas/project-ssh-configs.ts @@ -0,0 +1,21 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const ProjectSshConfigsSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + projectId: z.string(), + defaultUserSshCaId: z.string().uuid().nullable().optional(), + defaultHostSshCaId: z.string().uuid().nullable().optional() +}); + +export type TProjectSshConfigs = z.infer; +export type TProjectSshConfigsInsert = Omit, TImmutableDBKeys>; +export type TProjectSshConfigsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts index deba51b9a..2403d6cf4 100644 --- a/backend/src/db/schemas/projects.ts +++ b/backend/src/db/schemas/projects.ts @@ -13,7 +13,7 @@ export const ProjectsSchema = z.object({ id: z.string(), name: z.string(), slug: z.string(), - autoCapitalization: z.boolean().default(true).nullable().optional(), + autoCapitalization: z.boolean().default(false).nullable().optional(), orgId: z.string().uuid(), createdAt: z.date(), updatedAt: z.date(), @@ -23,7 +23,11 @@ export const ProjectsSchema = z.object({ kmsCertificateKeyId: z.string().uuid().nullable().optional(), auditLogsRetentionDays: z.number().nullable().optional(), kmsSecretManagerKeyId: z.string().uuid().nullable().optional(), - kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional() + kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional(), + description: z.string().nullable().optional(), + type: z.string(), + enforceCapitalization: z.boolean().default(false), + hasDeleteProtection: z.boolean().default(true).nullable().optional() }); export type TProjects = z.infer; diff --git a/backend/src/db/schemas/resource-metadata.ts b/backend/src/db/schemas/resource-metadata.ts new file mode 100644 index 000000000..442de66b6 --- /dev/null +++ b/backend/src/db/schemas/resource-metadata.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 { TImmutableDBKeys } from "./models"; + +export const ResourceMetadataSchema = z.object({ + id: z.string().uuid(), + key: z.string(), + value: z.string(), + orgId: z.string().uuid(), + userId: z.string().uuid().nullable().optional(), + identityId: z.string().uuid().nullable().optional(), + secretId: z.string().uuid().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date(), + dynamicSecretId: z.string().uuid().nullable().optional() +}); + +export type TResourceMetadata = z.infer; +export type TResourceMetadataInsert = Omit, TImmutableDBKeys>; +export type TResourceMetadataUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/saml-configs.ts b/backend/src/db/schemas/saml-configs.ts index 67171469a..350e84492 100644 --- a/backend/src/db/schemas/saml-configs.ts +++ b/backend/src/db/schemas/saml-configs.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const SamlConfigsSchema = z.object({ @@ -23,7 +25,10 @@ export const SamlConfigsSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), orgId: z.string().uuid(), - lastUsed: z.date().nullable().optional() + lastUsed: z.date().nullable().optional(), + encryptedSamlEntryPoint: zodBuffer, + encryptedSamlIssuer: zodBuffer, + encryptedSamlCertificate: zodBuffer }); export type TSamlConfigs = z.infer; diff --git a/backend/src/db/schemas/secret-approval-policies.ts b/backend/src/db/schemas/secret-approval-policies.ts index 94aeba050..8b9174456 100644 --- a/backend/src/db/schemas/secret-approval-policies.ts +++ b/backend/src/db/schemas/secret-approval-policies.ts @@ -15,7 +15,9 @@ export const SecretApprovalPoliciesSchema = z.object({ envId: z.string().uuid(), createdAt: z.date(), updatedAt: z.date(), - enforcementLevel: z.string().default("hard") + enforcementLevel: z.string().default("hard"), + deletedAt: z.date().nullable().optional(), + allowedSelfApprovals: z.boolean().default(true) }); export type TSecretApprovalPolicies = z.infer; diff --git a/backend/src/db/schemas/secret-approval-requests-reviewers.ts b/backend/src/db/schemas/secret-approval-requests-reviewers.ts index a5c445587..147646b8d 100644 --- a/backend/src/db/schemas/secret-approval-requests-reviewers.ts +++ b/backend/src/db/schemas/secret-approval-requests-reviewers.ts @@ -13,7 +13,8 @@ export const SecretApprovalRequestsReviewersSchema = z.object({ requestId: z.string().uuid(), createdAt: z.date(), updatedAt: z.date(), - reviewerUserId: z.string().uuid() + reviewerUserId: z.string().uuid(), + comment: z.string().nullable().optional() }); export type TSecretApprovalRequestsReviewers = z.infer; diff --git a/backend/src/db/schemas/secret-approval-requests-secrets-v2.ts b/backend/src/db/schemas/secret-approval-requests-secrets-v2.ts index ee25ed6ef..298985fed 100644 --- a/backend/src/db/schemas/secret-approval-requests-secrets-v2.ts +++ b/backend/src/db/schemas/secret-approval-requests-secrets-v2.ts @@ -24,7 +24,8 @@ export const SecretApprovalRequestsSecretsV2Schema = z.object({ requestId: z.string().uuid(), op: z.string(), secretId: z.string().uuid().nullable().optional(), - secretVersion: z.string().uuid().nullable().optional() + secretVersion: z.string().uuid().nullable().optional(), + secretMetadata: z.unknown().nullable().optional() }); export type TSecretApprovalRequestsSecretsV2 = z.infer; diff --git a/backend/src/db/schemas/secret-folders.ts b/backend/src/db/schemas/secret-folders.ts index ad43ed1ad..09e2fe8c1 100644 --- a/backend/src/db/schemas/secret-folders.ts +++ b/backend/src/db/schemas/secret-folders.ts @@ -15,7 +15,9 @@ export const SecretFoldersSchema = z.object({ updatedAt: z.date(), envId: z.string().uuid(), parentId: z.string().uuid().nullable().optional(), - isReserved: z.boolean().default(false).nullable().optional() + isReserved: z.boolean().default(false).nullable().optional(), + description: z.string().nullable().optional(), + lastSecretModified: z.date().nullable().optional() }); export type TSecretFolders = z.infer; diff --git a/backend/src/db/schemas/secret-rotation-v2-secret-mappings.ts b/backend/src/db/schemas/secret-rotation-v2-secret-mappings.ts new file mode 100644 index 000000000..5baf6942c --- /dev/null +++ b/backend/src/db/schemas/secret-rotation-v2-secret-mappings.ts @@ -0,0 +1,23 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const SecretRotationV2SecretMappingsSchema = z.object({ + id: z.string().uuid(), + secretId: z.string().uuid(), + rotationId: z.string().uuid() +}); + +export type TSecretRotationV2SecretMappings = z.infer; +export type TSecretRotationV2SecretMappingsInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TSecretRotationV2SecretMappingsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/secret-rotations-v2.ts b/backend/src/db/schemas/secret-rotations-v2.ts new file mode 100644 index 000000000..95873b447 --- /dev/null +++ b/backend/src/db/schemas/secret-rotations-v2.ts @@ -0,0 +1,39 @@ +// 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 SecretRotationsV2Schema = z.object({ + id: z.string().uuid(), + name: z.string(), + description: z.string().nullable().optional(), + type: z.string(), + parameters: z.unknown(), + secretsMapping: z.unknown(), + encryptedGeneratedCredentials: zodBuffer, + isAutoRotationEnabled: z.boolean().default(true), + activeIndex: z.number().default(0), + folderId: z.string().uuid(), + connectionId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + rotationInterval: z.number(), + rotateAtUtc: z.unknown(), + rotationStatus: z.string(), + lastRotationAttemptedAt: z.date(), + lastRotatedAt: z.date(), + encryptedLastRotationMessage: zodBuffer.nullable().optional(), + lastRotationJobId: z.string().nullable().optional(), + nextRotationAt: z.date().nullable().optional(), + isLastRotationManual: z.boolean().default(true) +}); + +export type TSecretRotationsV2 = z.infer; +export type TSecretRotationsV2Insert = Omit, TImmutableDBKeys>; +export type TSecretRotationsV2Update = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/secret-rotations.ts b/backend/src/db/schemas/secret-rotations.ts index b491edc46..a3cd04ebb 100644 --- a/backend/src/db/schemas/secret-rotations.ts +++ b/backend/src/db/schemas/secret-rotations.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const SecretRotationsSchema = z.object({ @@ -22,7 +24,8 @@ export const SecretRotationsSchema = z.object({ keyEncoding: z.string().nullable().optional(), envId: z.string().uuid(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + encryptedRotationData: zodBuffer }); export type TSecretRotations = z.infer; diff --git a/backend/src/db/schemas/secret-sharing.ts b/backend/src/db/schemas/secret-sharing.ts index d47f288b2..24ea26677 100644 --- a/backend/src/db/schemas/secret-sharing.ts +++ b/backend/src/db/schemas/secret-sharing.ts @@ -26,7 +26,8 @@ export const SecretSharingSchema = z.object({ lastViewedAt: z.date().nullable().optional(), password: z.string().nullable().optional(), encryptedSecret: zodBuffer.nullable().optional(), - identifier: z.string().nullable().optional() + identifier: z.string().nullable().optional(), + type: z.string().default("share") }); export type TSecretSharing = z.infer; diff --git a/backend/src/db/schemas/secret-syncs.ts b/backend/src/db/schemas/secret-syncs.ts new file mode 100644 index 000000000..0e0728e87 --- /dev/null +++ b/backend/src/db/schemas/secret-syncs.ts @@ -0,0 +1,40 @@ +// 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 SecretSyncsSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + description: z.string().nullable().optional(), + destination: z.string(), + isAutoSyncEnabled: z.boolean().default(true), + version: z.number().default(1), + destinationConfig: z.unknown(), + syncOptions: z.unknown(), + projectId: z.string(), + folderId: z.string().uuid().nullable().optional(), + connectionId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + syncStatus: z.string().nullable().optional(), + lastSyncJobId: z.string().nullable().optional(), + lastSyncMessage: z.string().nullable().optional(), + lastSyncedAt: z.date().nullable().optional(), + importStatus: z.string().nullable().optional(), + lastImportJobId: z.string().nullable().optional(), + lastImportMessage: z.string().nullable().optional(), + lastImportedAt: z.date().nullable().optional(), + removeStatus: z.string().nullable().optional(), + lastRemoveJobId: z.string().nullable().optional(), + lastRemoveMessage: z.string().nullable().optional(), + lastRemovedAt: z.date().nullable().optional() +}); + +export type TSecretSyncs = z.infer; +export type TSecretSyncsInsert = Omit, TImmutableDBKeys>; +export type TSecretSyncsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/secret-versions-v2.ts b/backend/src/db/schemas/secret-versions-v2.ts index 160ed1c14..593a46b06 100644 --- a/backend/src/db/schemas/secret-versions-v2.ts +++ b/backend/src/db/schemas/secret-versions-v2.ts @@ -25,7 +25,10 @@ export const SecretVersionsV2Schema = z.object({ folderId: z.string().uuid(), userId: z.string().uuid().nullable().optional(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + userActorId: z.string().uuid().nullable().optional(), + identityActorId: z.string().uuid().nullable().optional(), + actorType: z.string().nullable().optional() }); export type TSecretVersionsV2 = z.infer; diff --git a/backend/src/db/schemas/service-tokens.ts b/backend/src/db/schemas/service-tokens.ts index 720c8fd6f..8ffddb10a 100644 --- a/backend/src/db/schemas/service-tokens.ts +++ b/backend/src/db/schemas/service-tokens.ts @@ -21,7 +21,8 @@ export const ServiceTokensSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), createdBy: z.string(), - projectId: z.string() + projectId: z.string(), + expiryNotificationSent: z.boolean().default(false).nullable().optional() }); export type TServiceTokens = z.infer; diff --git a/backend/src/db/schemas/ssh-certificate-authorities.ts b/backend/src/db/schemas/ssh-certificate-authorities.ts new file mode 100644 index 000000000..75603406f --- /dev/null +++ b/backend/src/db/schemas/ssh-certificate-authorities.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 { TImmutableDBKeys } from "./models"; + +export const SshCertificateAuthoritiesSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + projectId: z.string(), + status: z.string(), + friendlyName: z.string(), + keyAlgorithm: z.string(), + keySource: z.string() +}); + +export type TSshCertificateAuthorities = z.infer; +export type TSshCertificateAuthoritiesInsert = Omit, TImmutableDBKeys>; +export type TSshCertificateAuthoritiesUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/ssh-certificate-authority-secrets.ts b/backend/src/db/schemas/ssh-certificate-authority-secrets.ts new file mode 100644 index 000000000..934c10ab2 --- /dev/null +++ b/backend/src/db/schemas/ssh-certificate-authority-secrets.ts @@ -0,0 +1,27 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const SshCertificateAuthoritySecretsSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + sshCaId: z.string().uuid(), + encryptedPrivateKey: zodBuffer +}); + +export type TSshCertificateAuthoritySecrets = z.infer; +export type TSshCertificateAuthoritySecretsInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TSshCertificateAuthoritySecretsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/ssh-certificate-bodies.ts b/backend/src/db/schemas/ssh-certificate-bodies.ts new file mode 100644 index 000000000..baafb773b --- /dev/null +++ b/backend/src/db/schemas/ssh-certificate-bodies.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 { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const SshCertificateBodiesSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + sshCertId: z.string().uuid(), + encryptedCertificate: zodBuffer +}); + +export type TSshCertificateBodies = z.infer; +export type TSshCertificateBodiesInsert = Omit, TImmutableDBKeys>; +export type TSshCertificateBodiesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/ssh-certificate-templates.ts b/backend/src/db/schemas/ssh-certificate-templates.ts new file mode 100644 index 000000000..6c16c3942 --- /dev/null +++ b/backend/src/db/schemas/ssh-certificate-templates.ts @@ -0,0 +1,30 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const SshCertificateTemplatesSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + sshCaId: z.string().uuid(), + status: z.string(), + name: z.string(), + ttl: z.string(), + maxTTL: z.string(), + allowedUsers: z.string().array(), + allowedHosts: z.string().array(), + allowUserCertificates: z.boolean(), + allowHostCertificates: z.boolean(), + allowCustomKeyIds: z.boolean() +}); + +export type TSshCertificateTemplates = z.infer; +export type TSshCertificateTemplatesInsert = Omit, TImmutableDBKeys>; +export type TSshCertificateTemplatesUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/ssh-certificates.ts b/backend/src/db/schemas/ssh-certificates.ts new file mode 100644 index 000000000..1bfd1fe6e --- /dev/null +++ b/backend/src/db/schemas/ssh-certificates.ts @@ -0,0 +1,27 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const SshCertificatesSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + sshCaId: z.string().uuid().nullable().optional(), + sshCertificateTemplateId: z.string().uuid().nullable().optional(), + serialNumber: z.string(), + certType: z.string(), + principals: z.string().array(), + keyId: z.string(), + notBefore: z.date(), + notAfter: z.date(), + sshHostId: z.string().uuid().nullable().optional() +}); + +export type TSshCertificates = z.infer; +export type TSshCertificatesInsert = Omit, TImmutableDBKeys>; +export type TSshCertificatesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/ssh-host-login-user-mappings.ts b/backend/src/db/schemas/ssh-host-login-user-mappings.ts new file mode 100644 index 000000000..6edb0d5a3 --- /dev/null +++ b/backend/src/db/schemas/ssh-host-login-user-mappings.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 SshHostLoginUserMappingsSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + sshHostLoginUserId: z.string().uuid(), + userId: z.string().uuid().nullable().optional() +}); + +export type TSshHostLoginUserMappings = z.infer; +export type TSshHostLoginUserMappingsInsert = Omit, TImmutableDBKeys>; +export type TSshHostLoginUserMappingsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/ssh-host-login-users.ts b/backend/src/db/schemas/ssh-host-login-users.ts new file mode 100644 index 000000000..62454d3c9 --- /dev/null +++ b/backend/src/db/schemas/ssh-host-login-users.ts @@ -0,0 +1,20 @@ +// 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 SshHostLoginUsersSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + sshHostId: z.string().uuid(), + loginUser: z.string() +}); + +export type TSshHostLoginUsers = z.infer; +export type TSshHostLoginUsersInsert = Omit, TImmutableDBKeys>; +export type TSshHostLoginUsersUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/ssh-hosts.ts b/backend/src/db/schemas/ssh-hosts.ts new file mode 100644 index 000000000..7577e065b --- /dev/null +++ b/backend/src/db/schemas/ssh-hosts.ts @@ -0,0 +1,24 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const SshHostsSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + projectId: z.string(), + hostname: z.string(), + userCertTtl: z.string(), + hostCertTtl: z.string(), + userSshCaId: z.string().uuid(), + hostSshCaId: z.string().uuid() +}); + +export type TSshHosts = z.infer; +export type TSshHostsInsert = Omit, TImmutableDBKeys>; +export type TSshHostsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/super-admin.ts b/backend/src/db/schemas/super-admin.ts index edab3a0e9..01aac280b 100644 --- a/backend/src/db/schemas/super-admin.ts +++ b/backend/src/db/schemas/super-admin.ts @@ -23,7 +23,10 @@ export const SuperAdminSchema = z.object({ defaultAuthOrgId: z.string().uuid().nullable().optional(), enabledLoginMethods: z.string().array().nullable().optional(), encryptedSlackClientId: zodBuffer.nullable().optional(), - encryptedSlackClientSecret: zodBuffer.nullable().optional() + encryptedSlackClientSecret: zodBuffer.nullable().optional(), + authConsentContent: z.string().nullable().optional(), + pageFrameContent: z.string().nullable().optional(), + adminIdentityIds: z.string().array().nullable().optional() }); export type TSuperAdmin = z.infer; diff --git a/backend/src/db/schemas/totp-configs.ts b/backend/src/db/schemas/totp-configs.ts new file mode 100644 index 000000000..d6ec11592 --- /dev/null +++ b/backend/src/db/schemas/totp-configs.ts @@ -0,0 +1,24 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const TotpConfigsSchema = z.object({ + id: z.string().uuid(), + userId: z.string().uuid(), + isVerified: z.boolean().default(false), + encryptedRecoveryCodes: zodBuffer, + encryptedSecret: zodBuffer, + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TTotpConfigs = z.infer; +export type TTotpConfigsInsert = Omit, TImmutableDBKeys>; +export type TTotpConfigsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/users.ts b/backend/src/db/schemas/users.ts index 5134f3ee6..1c1f579ea 100644 --- a/backend/src/db/schemas/users.ts +++ b/backend/src/db/schemas/users.ts @@ -26,7 +26,8 @@ export const UsersSchema = z.object({ consecutiveFailedMfaAttempts: z.number().default(0).nullable().optional(), isLocked: z.boolean().default(false).nullable().optional(), temporaryLockDateEnd: z.date().nullable().optional(), - consecutiveFailedPasswordAttempts: z.number().default(0).nullable().optional() + consecutiveFailedPasswordAttempts: z.number().default(0).nullable().optional(), + selectedMfaMethod: z.string().nullable().optional() }); export type TUsers = z.infer; diff --git a/backend/src/db/schemas/webhooks.ts b/backend/src/db/schemas/webhooks.ts index a7aac2933..60f031fff 100644 --- a/backend/src/db/schemas/webhooks.ts +++ b/backend/src/db/schemas/webhooks.ts @@ -5,12 +5,14 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const WebhooksSchema = z.object({ id: z.string().uuid(), secretPath: z.string().default("/"), - url: z.string(), + url: z.string().nullable().optional(), lastStatus: z.string().nullable().optional(), lastRunErrorMessage: z.string().nullable().optional(), isDisabled: z.boolean().default(false), @@ -25,7 +27,9 @@ export const WebhooksSchema = z.object({ urlCipherText: z.string().nullable().optional(), urlIV: z.string().nullable().optional(), urlTag: z.string().nullable().optional(), - type: z.string().default("general").nullable().optional() + type: z.string().default("general").nullable().optional(), + encryptedPassKey: zodBuffer.nullable().optional(), + encryptedUrl: zodBuffer }); export type TWebhooks = z.infer; diff --git a/backend/src/db/seeds/3-project.ts b/backend/src/db/seeds/3-project.ts index 934130494..b6c80bb63 100644 --- a/backend/src/db/seeds/3-project.ts +++ b/backend/src/db/seeds/3-project.ts @@ -4,7 +4,7 @@ import { Knex } from "knex"; import { encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; -import { ProjectMembershipRole, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas"; +import { ProjectMembershipRole, ProjectType, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas"; import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data"; export const DEFAULT_PROJECT_ENVS = [ @@ -24,6 +24,7 @@ export async function seed(knex: Knex): Promise { name: seedData1.project.name, orgId: seedData1.organization.id, slug: "first-project", + type: ProjectType.SecretManager, // eslint-disable-next-line // @ts-ignore id: seedData1.project.id diff --git a/backend/src/db/seeds/4-project-v3.ts b/backend/src/db/seeds/4-project-v3.ts index 60431919d..f89b965a6 100644 --- a/backend/src/db/seeds/4-project-v3.ts +++ b/backend/src/db/seeds/4-project-v3.ts @@ -1,6 +1,6 @@ import { Knex } from "knex"; -import { ProjectMembershipRole, ProjectVersion, TableName } from "../schemas"; +import { ProjectMembershipRole, ProjectType, ProjectVersion, TableName } from "../schemas"; import { seedData1 } from "../seed-data"; export const DEFAULT_PROJECT_ENVS = [ @@ -16,6 +16,7 @@ export async function seed(knex: Knex): Promise { orgId: seedData1.organization.id, slug: seedData1.projectV3.slug, version: ProjectVersion.V3, + type: ProjectType.SecretManager, // eslint-disable-next-line // @ts-ignore id: seedData1.projectV3.id diff --git a/backend/src/db/utils.ts b/backend/src/db/utils.ts index 68c400596..e06cdd3f1 100644 --- a/backend/src/db/utils.ts +++ b/backend/src/db/utils.ts @@ -2,6 +2,9 @@ import { Knex } from "knex"; import { TableName } from "./schemas"; +interface PgTriggerResult { + rows: Array<{ exists: boolean }>; +} export const createJunctionTable = (knex: Knex, tableName: TableName, table1Name: TableName, table2Name: TableName) => knex.schema.createTable(tableName, (table) => { table.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); @@ -28,13 +31,26 @@ DROP FUNCTION IF EXISTS on_update_timestamp() CASCADE; // we would be using this to apply updatedAt where ever we wanta // remember to set `timestamps(true,true,true)` before this on schema -export const createOnUpdateTrigger = (knex: Knex, tableName: string) => - knex.raw(` -CREATE TRIGGER "${tableName}_updatedAt" -BEFORE UPDATE ON ${tableName} -FOR EACH ROW -EXECUTE PROCEDURE on_update_timestamp(); -`); +export const createOnUpdateTrigger = async (knex: Knex, tableName: string) => { + const triggerExists = await knex.raw(` + SELECT EXISTS ( + SELECT 1 + FROM pg_trigger + WHERE tgname = '${tableName}_updatedAt' + ); + `); + + if (!triggerExists?.rows?.[0]?.exists) { + return knex.raw(` + CREATE TRIGGER "${tableName}_updatedAt" + BEFORE UPDATE ON ${tableName} + FOR EACH ROW + EXECUTE PROCEDURE on_update_timestamp(); + `); + } + + return null; +}; export const dropOnUpdateTrigger = (knex: Knex, tableName: string) => knex.raw(`DROP TRIGGER IF EXISTS "${tableName}_updatedAt" ON ${tableName}`); diff --git a/backend/src/ee/routes/est/certificate-est-router.ts b/backend/src/ee/routes/est/certificate-est-router.ts index 7f401216e..e67d037ea 100644 --- a/backend/src/ee/routes/est/certificate-est-router.ts +++ b/backend/src/ee/routes/est/certificate-est-router.ts @@ -16,7 +16,7 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) = // for CSRs sent in PEM, we leave them as is // for CSRs sent in base64, we preprocess them to remove new lines and spaces if (!csrBody.includes("BEGIN CERTIFICATE REQUEST")) { - csrBody = csrBody.replace(/\n/g, "").replace(/ /g, ""); + csrBody = csrBody.replaceAll("\n", "").replaceAll(" ", ""); } done(null, csrBody); diff --git a/backend/src/ee/routes/v1/access-approval-policy-router.ts b/backend/src/ee/routes/v1/access-approval-policy-router.ts index 814d19841..97a819234 100644 --- a/backend/src/ee/routes/v1/access-approval-policy-router.ts +++ b/backend/src/ee/routes/v1/access-approval-policy-router.ts @@ -29,7 +29,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi .array() .min(1, { message: "At least one approver should be provided" }), approvals: z.number().min(1).default(1), - enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard) + enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard), + allowedSelfApprovals: z.boolean().default(true) }), response: { 200: z.object({ @@ -147,7 +148,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi .array() .min(1, { message: "At least one approver should be provided" }), approvals: z.number().min(1).optional(), - enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard) + enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard), + allowedSelfApprovals: z.boolean().default(true) }), response: { 200: z.object({ diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts index 7dbb62fc2..8a7ccfdef 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -22,7 +22,8 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv body: z.object({ permissions: z.any().array(), isTemporary: z.boolean(), - temporaryRange: z.string().optional() + temporaryRange: z.string().optional(), + note: z.string().max(255).optional() }), querystring: z.object({ projectSlug: z.string().trim() @@ -43,7 +44,8 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv actorOrgId: req.permission.orgId, projectSlug: req.query.projectSlug, temporaryRange: req.body.temporaryRange, - isTemporary: req.body.isTemporary + isTemporary: req.body.isTemporary, + note: req.body.note }); return { approval: request }; } @@ -109,7 +111,9 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv approvers: z.string().array(), secretPath: z.string().nullish(), envId: z.string(), - enforcementLevel: z.string() + enforcementLevel: z.string(), + deletedAt: z.date().nullish(), + allowedSelfApprovals: z.boolean() }), reviewers: z .object({ diff --git a/backend/src/ee/routes/v1/dynamic-secret-lease-router.ts b/backend/src/ee/routes/v1/dynamic-secret-lease-router.ts index c19af4d22..7c42c7f99 100644 --- a/backend/src/ee/routes/v1/dynamic-secret-lease-router.ts +++ b/backend/src/ee/routes/v1/dynamic-secret-lease-router.ts @@ -1,10 +1,10 @@ -import ms from "ms"; import { z } from "zod"; import { DynamicSecretLeasesSchema } from "@app/db/schemas"; -import { DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs"; +import { ApiDocsTags, DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs"; import { daysToMillisecond } from "@app/lib/dates"; import { removeTrailingSlash } from "@app/lib/fn"; +import { ms } from "@app/lib/ms"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas"; @@ -18,6 +18,8 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.DynamicSecrets], body: z.object({ dynamicSecretName: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.dynamicSecretName).toLowerCase(), projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.projectSlug), @@ -65,6 +67,8 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.DynamicSecrets], params: z.object({ leaseId: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.DELETE.leaseId) }), @@ -107,6 +111,8 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.DynamicSecrets], params: z.object({ leaseId: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.leaseId) }), @@ -160,6 +166,8 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.DynamicSecrets], params: z.object({ leaseId: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.GET_BY_LEASEID.leaseId) }), diff --git a/backend/src/ee/routes/v1/dynamic-secret-router.ts b/backend/src/ee/routes/v1/dynamic-secret-router.ts index 4b1566c55..6e70effe4 100644 --- a/backend/src/ee/routes/v1/dynamic-secret-router.ts +++ b/backend/src/ee/routes/v1/dynamic-secret-router.ts @@ -1,16 +1,17 @@ -import slugify from "@sindresorhus/slugify"; -import ms from "ms"; import { z } from "zod"; import { DynamicSecretLeasesSchema } from "@app/db/schemas"; import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/providers/models"; -import { DYNAMIC_SECRETS } from "@app/lib/api-docs"; +import { ApiDocsTags, DYNAMIC_SECRETS } from "@app/lib/api-docs"; import { daysToMillisecond } from "@app/lib/dates"; import { removeTrailingSlash } from "@app/lib/fn"; +import { ms } from "@app/lib/ms"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; +import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => { server.route({ @@ -20,6 +21,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.DynamicSecrets], body: z.object({ projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.CREATE.projectSlug), provider: DynamicSecretProviderSchema.describe(DYNAMIC_SECRETS.CREATE.provider), @@ -48,15 +51,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => .nullable(), path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash), environmentSlug: z.string().describe(DYNAMIC_SECRETS.CREATE.environmentSlug).min(1), - name: z - .string() - .describe(DYNAMIC_SECRETS.CREATE.name) - .min(1) - .toLowerCase() - .max(64) - .refine((v) => slugify(v) === v, { - message: "Slug must be a valid" - }) + name: slugSchema({ min: 1, max: 64, field: "Name" }).describe(DYNAMIC_SECRETS.CREATE.name), + metadata: ResourceMetadataSchema.optional() }), response: { 200: z.object({ @@ -117,6 +113,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.DynamicSecrets], params: z.object({ name: z.string().toLowerCase().describe(DYNAMIC_SECRETS.UPDATE.name) }), @@ -151,7 +149,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }) .nullable(), - newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional() + newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional(), + metadata: ResourceMetadataSchema.optional() }) }), response: { @@ -184,6 +183,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.DynamicSecrets], params: z.object({ name: z.string().toLowerCase().describe(DYNAMIC_SECRETS.DELETE.name) }), @@ -220,6 +221,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.DynamicSecrets], params: z.object({ name: z.string().min(1).describe(DYNAMIC_SECRETS.GET_BY_NAME.name) }), @@ -246,6 +249,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => name: req.params.name, ...req.query }); + return { dynamicSecret: dynamicSecretCfg }; } }); @@ -257,6 +261,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.DynamicSecrets], querystring: z.object({ projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.LIST.projectSlug), path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRETS.LIST.path), @@ -288,18 +294,20 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.DynamicSecrets], params: z.object({ - name: z.string().min(1).describe(DYNAMIC_SECRETS.LIST_LEAES_BY_NAME.name) + name: z.string().min(1).describe(DYNAMIC_SECRETS.LIST_LEASES_BY_NAME.name) }), querystring: z.object({ - projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.LIST_LEAES_BY_NAME.projectSlug), + projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.LIST_LEASES_BY_NAME.projectSlug), path: z .string() .trim() .default("/") .transform(removeTrailingSlash) - .describe(DYNAMIC_SECRETS.LIST_LEAES_BY_NAME.path), - environmentSlug: z.string().min(1).describe(DYNAMIC_SECRETS.LIST_LEAES_BY_NAME.environmentSlug) + .describe(DYNAMIC_SECRETS.LIST_LEASES_BY_NAME.path), + environmentSlug: z.string().min(1).describe(DYNAMIC_SECRETS.LIST_LEASES_BY_NAME.environmentSlug) }), response: { 200: z.object({ diff --git a/backend/src/ee/routes/v1/external-kms-router.ts b/backend/src/ee/routes/v1/external-kms-router.ts index 4e43d6ed9..a48e28e3d 100644 --- a/backend/src/ee/routes/v1/external-kms-router.ts +++ b/backend/src/ee/routes/v1/external-kms-router.ts @@ -4,9 +4,15 @@ import { ExternalKmsSchema, KmsKeysSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ExternalKmsAwsSchema, + ExternalKmsGcpCredentialSchema, + ExternalKmsGcpSchema, ExternalKmsInputSchema, - ExternalKmsInputUpdateSchema + ExternalKmsInputUpdateSchema, + KmsGcpKeyFetchAuthType, + KmsProviders, + TExternalKmsGcpCredentialSchema } from "@app/ee/services/external-kms/providers/model"; +import { NotFoundError } from "@app/lib/errors"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -44,7 +50,8 @@ const sanitizedExternalSchemaForGetById = KmsKeysSchema.extend({ statusDetails: true, provider: true }).extend({ - providerInput: ExternalKmsAwsSchema + // for GCP, we don't return the credential object as it is sensitive data that should not be exposed + providerInput: z.union([ExternalKmsAwsSchema, ExternalKmsGcpSchema.pick({ gcpRegion: true, keyName: true })]) }) }); @@ -286,4 +293,67 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => { return { externalKms }; } }); + + server.route({ + method: "POST", + url: "/gcp/keys", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.discriminatedUnion("authMethod", [ + z.object({ + authMethod: z.literal(KmsGcpKeyFetchAuthType.Credential), + region: z.string().trim().min(1), + credential: ExternalKmsGcpCredentialSchema + }), + z.object({ + authMethod: z.literal(KmsGcpKeyFetchAuthType.Kms), + region: z.string().trim().min(1), + kmsId: z.string().trim().min(1) + }) + ]), + response: { + 200: z.object({ + keys: z.string().array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { region, authMethod } = req.body; + let credentialJson: TExternalKmsGcpCredentialSchema | undefined; + + if (authMethod === KmsGcpKeyFetchAuthType.Credential) { + credentialJson = req.body.credential; + } else if (authMethod === KmsGcpKeyFetchAuthType.Kms) { + const externalKms = await server.services.externalKms.findById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.body.kmsId + }); + + if (!externalKms || externalKms.external.provider !== KmsProviders.Gcp) { + throw new NotFoundError({ message: "KMS not found or not of type GCP" }); + } + + credentialJson = externalKms.external.providerInput.credential as TExternalKmsGcpCredentialSchema; + } + + if (!credentialJson) { + throw new NotFoundError({ + message: "Something went wrong while fetching the GCP credential, please check inputs and try again" + }); + } + + const results = await server.services.externalKms.fetchGcpKeys({ + credential: credentialJson, + gcpRegion: region + }); + + return results; + } + }); }; diff --git a/backend/src/ee/routes/v1/gateway-router.ts b/backend/src/ee/routes/v1/gateway-router.ts new file mode 100644 index 000000000..c916e229e --- /dev/null +++ b/backend/src/ee/routes/v1/gateway-router.ts @@ -0,0 +1,265 @@ +import { z } from "zod"; + +import { GatewaysSchema } from "@app/db/schemas"; +import { isValidIp } from "@app/lib/ip"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +const SanitizedGatewaySchema = GatewaysSchema.pick({ + id: true, + identityId: true, + name: true, + createdAt: true, + updatedAt: true, + issuedAt: true, + serialNumber: true, + heartbeat: true +}); + +const isValidRelayAddress = (relayAddress: string) => { + const [ip, port] = relayAddress.split(":"); + return isValidIp(ip) && Number(port) <= 65535 && Number(port) >= 40000; +}; + +export const registerGatewayRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/register-identity", + config: { + rateLimit: writeLimit + }, + schema: { + response: { + 200: z.object({ + turnServerUsername: z.string(), + turnServerPassword: z.string(), + turnServerRealm: z.string(), + turnServerAddress: z.string(), + infisicalStaticIp: z.string().optional() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const relayDetails = await server.services.gateway.getGatewayRelayDetails( + req.permission.id, + req.permission.orgId, + req.permission.authMethod + ); + return relayDetails; + } + }); + + server.route({ + method: "POST", + url: "/exchange-cert", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + relayAddress: z.string().refine(isValidRelayAddress, { message: "Invalid relay address" }) + }), + response: { + 200: z.object({ + serialNumber: z.string(), + privateKey: z.string(), + certificate: z.string(), + certificateChain: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const gatewayCertificates = await server.services.gateway.exchangeAllocatedRelayAddress({ + identityOrg: req.permission.orgId, + identityId: req.permission.id, + relayAddress: req.body.relayAddress, + identityOrgAuthMethod: req.permission.authMethod + }); + return gatewayCertificates; + } + }); + + server.route({ + method: "POST", + url: "/heartbeat", + config: { + rateLimit: writeLimit + }, + schema: { + response: { + 200: z.object({ + message: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + await server.services.gateway.heartbeat({ + orgPermission: req.permission + }); + return { message: "Successfully registered heartbeat" }; + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + querystring: z.object({ + projectId: z.string().optional() + }), + response: { + 200: z.object({ + gateways: SanitizedGatewaySchema.extend({ + identity: z.object({ + name: z.string(), + id: z.string() + }), + projects: z + .object({ + name: z.string(), + id: z.string(), + slug: z.string() + }) + .array() + }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]), + handler: async (req) => { + const gateways = await server.services.gateway.listGateways({ + orgPermission: req.permission + }); + return { gateways }; + } + }); + + server.route({ + method: "GET", + url: "/projects/:projectId", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + projectId: z.string() + }), + response: { + 200: z.object({ + gateways: SanitizedGatewaySchema.extend({ + identity: z.object({ + name: z.string(), + id: z.string() + }), + projectGatewayId: z.string() + }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]), + handler: async (req) => { + const gateways = await server.services.gateway.getProjectGateways({ + projectId: req.params.projectId, + projectPermission: req.permission + }); + return { gateways }; + } + }); + + server.route({ + method: "GET", + url: "/:id", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + id: z.string() + }), + response: { + 200: z.object({ + gateway: SanitizedGatewaySchema.extend({ + identity: z.object({ + name: z.string(), + id: z.string() + }) + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]), + handler: async (req) => { + const gateway = await server.services.gateway.getGatewayById({ + orgPermission: req.permission, + id: req.params.id + }); + return { gateway }; + } + }); + + server.route({ + method: "PATCH", + url: "/:id", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + id: z.string() + }), + body: z.object({ + name: slugSchema({ field: "name" }).optional(), + projectIds: z.string().array().optional() + }), + response: { + 200: z.object({ + gateway: SanitizedGatewaySchema + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]), + handler: async (req) => { + const gateway = await server.services.gateway.updateGatewayById({ + orgPermission: req.permission, + id: req.params.id, + name: req.body.name, + projectIds: req.body.projectIds + }); + return { gateway }; + } + }); + + server.route({ + method: "DELETE", + url: "/:id", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + id: z.string() + }), + response: { + 200: z.object({ + gateway: SanitizedGatewaySchema + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]), + handler: async (req) => { + const gateway = await server.services.gateway.deleteGatewayById({ + orgPermission: req.permission, + id: req.params.id + }); + return { gateway }; + } + }); +}; diff --git a/backend/src/ee/routes/v1/group-router.ts b/backend/src/ee/routes/v1/group-router.ts index 780e5ec00..d10e1800b 100644 --- a/backend/src/ee/routes/v1/group-router.ts +++ b/backend/src/ee/routes/v1/group-router.ts @@ -1,8 +1,9 @@ -import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { GroupsSchema, OrgMembershipRole, UsersSchema } from "@app/db/schemas"; -import { GROUPS } from "@app/lib/api-docs"; +import { EFilterReturnedUsers } from "@app/ee/services/group/group-types"; +import { ApiDocsTags, GROUPS } from "@app/lib/api-docs"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -12,17 +13,11 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { method: "POST", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Groups], body: z.object({ name: z.string().trim().min(1).max(50).describe(GROUPS.CREATE.name), - slug: z - .string() - .min(5) - .max(36) - .refine((v) => slugify(v) === v, { - message: "Slug must be a valid slug" - }) - .optional() - .describe(GROUPS.CREATE.slug), + slug: slugSchema({ min: 5, max: 36 }).optional().describe(GROUPS.CREATE.slug), role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(GROUPS.CREATE.role) }), response: { @@ -47,6 +42,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { method: "GET", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Groups], params: z.object({ id: z.string().trim().describe(GROUPS.GET_BY_ID.id) }), @@ -72,6 +69,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { method: "GET", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Groups], response: { 200: GroupsSchema.array() } @@ -94,20 +93,15 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { method: "PATCH", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Groups], params: z.object({ id: z.string().trim().describe(GROUPS.UPDATE.id) }), body: z .object({ name: z.string().trim().min(1).describe(GROUPS.UPDATE.name), - slug: z - .string() - .min(5) - .max(36) - .refine((v) => slugify(v) === v, { - message: "Slug must be a valid slug" - }) - .describe(GROUPS.UPDATE.slug), + slug: slugSchema({ min: 5, max: 36 }).describe(GROUPS.UPDATE.slug), role: z.string().trim().min(1).describe(GROUPS.UPDATE.role) }) .partial(), @@ -134,6 +128,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { method: "DELETE", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Groups], params: z.object({ id: z.string().trim().describe(GROUPS.DELETE.id) }), @@ -159,6 +155,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { url: "/:id/users", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Groups], params: z.object({ id: z.string().trim().describe(GROUPS.LIST_USERS.id) }), @@ -166,7 +164,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { offset: z.coerce.number().min(0).max(100).default(0).describe(GROUPS.LIST_USERS.offset), limit: z.coerce.number().min(1).max(100).default(10).describe(GROUPS.LIST_USERS.limit), username: z.string().trim().optional().describe(GROUPS.LIST_USERS.username), - search: z.string().trim().optional().describe(GROUPS.LIST_USERS.search) + search: z.string().trim().optional().describe(GROUPS.LIST_USERS.search), + filter: z.nativeEnum(EFilterReturnedUsers).optional().describe(GROUPS.LIST_USERS.filterUsers) }), response: { 200: z.object({ @@ -179,7 +178,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { }) .merge( z.object({ - isPartOfGroup: z.boolean() + isPartOfGroup: z.boolean(), + joinedGroupAt: z.date().nullable() }) ) .array(), @@ -206,6 +206,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { url: "/:id/users/:username", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Groups], params: z.object({ id: z.string().trim().describe(GROUPS.ADD_USER.id), username: z.string().trim().describe(GROUPS.ADD_USER.username) @@ -239,6 +241,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { url: "/:id/users/:username", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Groups], params: z.object({ id: z.string().trim().describe(GROUPS.DELETE_USER.id), username: z.string().trim().describe(GROUPS.DELETE_USER.username) diff --git a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts index d342f95ce..f64d3c979 100644 --- a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts +++ b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts @@ -1,13 +1,14 @@ import slugify from "@sindresorhus/slugify"; -import ms from "ms"; import { z } from "zod"; import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types"; import { backfillPermissionV1SchemaToV2Schema } from "@app/ee/services/permission/project-permission"; -import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; +import { ApiDocsTags, IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; import { UnauthorizedError } from "@app/lib/errors"; +import { ms } from "@app/lib/ms"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { ProjectPermissionSchema, @@ -24,6 +25,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.IdentitySpecificPrivilegesV1], description: "Create a permanent or a non expiry specific privilege for identity.", security: [ { @@ -33,17 +36,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F body: z.object({ identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId), projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug), - slug: z - .string() - .min(1) - .max(60) - .trim() - .refine((val) => val.toLowerCase() === val, "Must be lowercase") - .refine((v) => slugify(v) === v, { - message: "Slug must be a valid slug" - }) - .optional() - .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug), + slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug), permissions: ProjectPermissionSchema.array() .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions) .optional(), @@ -77,7 +70,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, ...req.body, - slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)), + slug: req.body.slug ?? slugify(alphaNumericNanoId(12)), isTemporary: false, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore-error this is valid ts @@ -94,6 +87,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.IdentitySpecificPrivilegesV1], description: "Create a temporary or a expiring specific privilege for identity.", security: [ { @@ -103,17 +98,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F body: z.object({ identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId), projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug), - slug: z - .string() - .min(1) - .max(60) - .trim() - .refine((val) => val.toLowerCase() === val, "Must be lowercase") - .refine((v) => slugify(v) === v, { - message: "Slug must be a valid slug" - }) - .optional() - .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug), + slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug), permissions: ProjectPermissionSchema.array() .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions) .optional(), @@ -159,7 +144,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, ...req.body, - slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)), + slug: req.body.slug ?? slugify(alphaNumericNanoId(12)), isTemporary: true, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore-error this is valid ts @@ -176,6 +161,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.IdentitySpecificPrivilegesV1], description: "Update a specific privilege of an identity.", security: [ { @@ -189,16 +176,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.projectSlug), privilegeDetails: z .object({ - slug: z - .string() - .min(1) - .max(60) - .trim() - .refine((val) => val.toLowerCase() === val, "Must be lowercase") - .refine((v) => slugify(v) === v, { - message: "Slug must be a valid slug" - }) - .describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug), + slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug), permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions), privilegePermission: ProjectSpecificPrivilegePermissionSchema.describe( IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.privilegePermission @@ -268,6 +246,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.IdentitySpecificPrivilegesV1], description: "Delete a specific privilege of an identity.", security: [ { @@ -307,6 +287,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.IdentitySpecificPrivilegesV1], description: "Retrieve details of a specific privilege by privilege slug.", security: [ { @@ -347,6 +329,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.IdentitySpecificPrivilegesV1], description: "List of a specific privilege of an identity in a project.", security: [ { diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 5e3a0eafe..2bf85e9c4 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -7,8 +7,11 @@ import { registerCaCrlRouter } from "./certificate-authority-crl-router"; import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router"; import { registerDynamicSecretRouter } from "./dynamic-secret-router"; import { registerExternalKmsRouter } from "./external-kms-router"; +import { registerGatewayRouter } from "./gateway-router"; import { registerGroupRouter } from "./group-router"; import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router"; +import { registerKmipRouter } from "./kmip-router"; +import { registerKmipSpecRouter } from "./kmip-spec-router"; import { registerLdapRouter } from "./ldap-router"; import { registerLicenseRouter } from "./license-router"; import { registerOidcRouter } from "./oidc-router"; @@ -22,9 +25,14 @@ import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-rou import { registerSecretApprovalRequestRouter } from "./secret-approval-request-router"; import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router"; import { registerSecretRotationRouter } from "./secret-rotation-router"; +import { registerSecretRouter } from "./secret-router"; import { registerSecretScanningRouter } from "./secret-scanning-router"; import { registerSecretVersionRouter } from "./secret-version-router"; import { registerSnapshotRouter } from "./snapshot-router"; +import { registerSshCaRouter } from "./ssh-certificate-authority-router"; +import { registerSshCertRouter } from "./ssh-certificate-router"; +import { registerSshCertificateTemplateRouter } from "./ssh-certificate-template-router"; +import { registerSshHostRouter } from "./ssh-host-router"; import { registerTrustedIpRouter } from "./trusted-ip-router"; import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router"; @@ -61,6 +69,8 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { { prefix: "/dynamic-secrets" } ); + await server.register(registerGatewayRouter, { prefix: "/gateways" }); + await server.register( async (pkiRouter) => { await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" }); @@ -68,6 +78,16 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { { prefix: "/pki" } ); + await server.register( + async (sshRouter) => { + await sshRouter.register(registerSshCaRouter, { prefix: "/ca" }); + await sshRouter.register(registerSshCertRouter, { prefix: "/certificates" }); + await sshRouter.register(registerSshCertificateTemplateRouter, { prefix: "/certificate-templates" }); + await sshRouter.register(registerSshHostRouter, { prefix: "/hosts" }); + }, + { prefix: "/ssh" } + ); + await server.register( async (ssoRouter) => { await ssoRouter.register(registerSamlRouter); @@ -80,6 +100,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { await server.register(registerLdapRouter, { prefix: "/ldap" }); await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" }); await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" }); + await server.register(registerSecretRouter, { prefix: "/secrets" }); await server.register(registerSecretVersionRouter, { prefix: "/secret" }); await server.register(registerGroupRouter, { prefix: "/groups" }); await server.register(registerAuditLogStreamRouter, { prefix: "/audit-log-streams" }); @@ -96,4 +117,12 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { }); await server.register(registerProjectTemplateRouter, { prefix: "/project-templates" }); + + await server.register( + async (kmipRouter) => { + await kmipRouter.register(registerKmipRouter); + await kmipRouter.register(registerKmipSpecRouter, { prefix: "/spec" }); + }, + { prefix: "/kmip" } + ); }; diff --git a/backend/src/ee/routes/v1/kmip-router.ts b/backend/src/ee/routes/v1/kmip-router.ts new file mode 100644 index 000000000..b7f5384ff --- /dev/null +++ b/backend/src/ee/routes/v1/kmip-router.ts @@ -0,0 +1,428 @@ +import { z } from "zod"; + +import { KmipClientsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { KmipPermission } from "@app/ee/services/kmip/kmip-enum"; +import { KmipClientOrderBy } from "@app/ee/services/kmip/kmip-types"; +import { ms } from "@app/lib/ms"; +import { OrderByDirection } from "@app/lib/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"; +import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; +import { validateAltNamesField } from "@app/services/certificate-authority/certificate-authority-validators"; + +const KmipClientResponseSchema = KmipClientsSchema.pick({ + projectId: true, + name: true, + id: true, + description: true, + permissions: true +}); + +export const registerKmipRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/clients", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + projectId: z.string(), + name: z.string().trim().min(1), + description: z.string().optional(), + permissions: z.nativeEnum(KmipPermission).array() + }), + response: { + 200: KmipClientResponseSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const kmipClient = await server.services.kmip.createKmipClient({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: kmipClient.projectId, + event: { + type: EventType.CREATE_KMIP_CLIENT, + metadata: { + id: kmipClient.id, + name: kmipClient.name, + permissions: (kmipClient.permissions ?? []) as KmipPermission[] + } + } + }); + + return kmipClient; + } + }); + + server.route({ + method: "PATCH", + url: "/clients/:id", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + id: z.string() + }), + body: z.object({ + name: z.string().trim().min(1), + description: z.string().optional(), + permissions: z.nativeEnum(KmipPermission).array() + }), + response: { + 200: KmipClientResponseSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const kmipClient = await server.services.kmip.updateKmipClient({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.params, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: kmipClient.projectId, + event: { + type: EventType.UPDATE_KMIP_CLIENT, + metadata: { + id: kmipClient.id, + name: kmipClient.name, + permissions: (kmipClient.permissions ?? []) as KmipPermission[] + } + } + }); + + return kmipClient; + } + }); + + server.route({ + method: "DELETE", + url: "/clients/:id", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + id: z.string() + }), + response: { + 200: KmipClientResponseSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const kmipClient = await server.services.kmip.deleteKmipClient({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.params + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: kmipClient.projectId, + event: { + type: EventType.DELETE_KMIP_CLIENT, + metadata: { + id: kmipClient.id + } + } + }); + + return kmipClient; + } + }); + + server.route({ + method: "GET", + url: "/clients/:id", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + id: z.string() + }), + response: { + 200: KmipClientResponseSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const kmipClient = await server.services.kmip.getKmipClient({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.params + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: kmipClient.projectId, + event: { + type: EventType.GET_KMIP_CLIENT, + metadata: { + id: kmipClient.id + } + } + }); + + return kmipClient; + } + }); + + server.route({ + method: "GET", + url: "/clients", + config: { + rateLimit: readLimit + }, + schema: { + description: "List KMIP clients", + querystring: z.object({ + projectId: z.string(), + offset: z.coerce.number().min(0).optional().default(0), + limit: z.coerce.number().min(1).max(100).optional().default(100), + orderBy: z.nativeEnum(KmipClientOrderBy).optional().default(KmipClientOrderBy.Name), + orderDirection: z.nativeEnum(OrderByDirection).optional().default(OrderByDirection.ASC), + search: z.string().trim().optional() + }), + response: { + 200: z.object({ + kmipClients: KmipClientResponseSchema.array(), + totalCount: z.number() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { kmipClients, totalCount } = await server.services.kmip.listKmipClientsByProjectId({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.query + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.query.projectId, + event: { + type: EventType.GET_KMIP_CLIENTS, + metadata: { + ids: kmipClients.map((key) => key.id) + } + } + }); + + return { kmipClients, totalCount }; + } + }); + + server.route({ + method: "POST", + url: "/clients/:id/certificates", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + id: z.string() + }), + body: z.object({ + keyAlgorithm: z.nativeEnum(CertKeyAlgorithm), + ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number") + }), + response: { + 200: z.object({ + serialNumber: z.string(), + certificateChain: z.string(), + certificate: z.string(), + privateKey: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const certificate = await server.services.kmip.createKmipClientCertificate({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + clientId: req.params.id, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: certificate.projectId, + event: { + type: EventType.CREATE_KMIP_CLIENT_CERTIFICATE, + metadata: { + clientId: req.params.id, + serialNumber: certificate.serialNumber, + ttl: req.body.ttl, + keyAlgorithm: req.body.keyAlgorithm + } + } + }); + + return certificate; + } + }); + + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + caKeyAlgorithm: z.nativeEnum(CertKeyAlgorithm) + }), + response: { + 200: z.object({ + serverCertificateChain: z.string(), + clientCertificateChain: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const chains = await server.services.kmip.setupOrgKmip({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.SETUP_KMIP, + metadata: { + keyAlgorithm: req.body.caKeyAlgorithm + } + } + }); + + return chains; + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: z.object({ + serverCertificateChain: z.string(), + clientCertificateChain: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const kmip = await server.services.kmip.getOrgKmip({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.GET_KMIP, + metadata: { + id: kmip.id + } + } + }); + + return kmip; + } + }); + + server.route({ + method: "POST", + url: "/server-registration", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + hostnamesOrIps: validateAltNamesField, + commonName: z.string().trim().min(1).optional(), + keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional().default(CertKeyAlgorithm.RSA_2048), + ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number") + }), + response: { + 200: z.object({ + clientCertificateChain: z.string(), + certificateChain: z.string(), + certificate: z.string(), + privateKey: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const configs = await server.services.kmip.registerServer({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.REGISTER_KMIP_SERVER, + metadata: { + serverCertificateSerialNumber: configs.serverCertificateSerialNumber, + hostnamesOrIps: req.body.hostnamesOrIps, + commonName: req.body.commonName ?? "kmip-server", + keyAlgorithm: req.body.keyAlgorithm, + ttl: req.body.ttl + } + } + }); + + return configs; + } + }); +}; diff --git a/backend/src/ee/routes/v1/kmip-spec-router.ts b/backend/src/ee/routes/v1/kmip-spec-router.ts new file mode 100644 index 000000000..9a1f4902c --- /dev/null +++ b/backend/src/ee/routes/v1/kmip-spec-router.ts @@ -0,0 +1,477 @@ +import z from "zod"; + +import { KmsKeysSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher"; +import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { ActorType, AuthMode } from "@app/services/auth/auth-type"; + +export const registerKmipSpecRouter = async (server: FastifyZodProvider) => { + server.decorateRequest("kmipUser", null); + + server.addHook("onRequest", async (req) => { + const clientId = req.headers["x-kmip-client-id"] as string; + const projectId = req.headers["x-kmip-project-id"] as string; + const clientCertSerialNumber = req.headers["x-kmip-client-certificate-serial-number"] as string; + const serverCertSerialNumber = req.headers["x-kmip-server-certificate-serial-number"] as string; + + if (!serverCertSerialNumber) { + throw new ForbiddenRequestError({ + message: "Missing server certificate serial number from request" + }); + } + + if (!clientCertSerialNumber) { + throw new ForbiddenRequestError({ + message: "Missing client certificate serial number from request" + }); + } + + if (!clientId) { + throw new ForbiddenRequestError({ + message: "Missing client ID from request" + }); + } + + if (!projectId) { + throw new ForbiddenRequestError({ + message: "Missing project ID from request" + }); + } + + // TODO: assert that server certificate used is not revoked + // TODO: assert that client certificate used is not revoked + + const kmipClient = await server.store.kmipClient.findByProjectAndClientId(projectId, clientId); + + if (!kmipClient) { + throw new NotFoundError({ + message: "KMIP client cannot be found." + }); + } + + if (kmipClient.orgId !== req.permission.orgId) { + throw new ForbiddenRequestError({ + message: "Client specified in the request does not belong in the organization" + }); + } + + req.kmipUser = { + projectId, + clientId, + name: kmipClient.name + }; + }); + + server.route({ + method: "POST", + url: "/create", + config: { + rateLimit: writeLimit + }, + schema: { + description: "KMIP endpoint for creating managed objects", + body: z.object({ + algorithm: z.nativeEnum(SymmetricKeyAlgorithm) + }), + response: { + 200: KmsKeysSchema + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const object = await server.services.kmipOperation.create({ + ...req.kmipUser, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + algorithm: req.body.algorithm + }); + + await server.services.auditLog.createAuditLog({ + projectId: req.kmipUser.projectId, + actor: { + type: ActorType.KMIP_CLIENT, + metadata: { + clientId: req.kmipUser.clientId, + name: req.kmipUser.name + } + }, + event: { + type: EventType.KMIP_OPERATION_CREATE, + metadata: { + id: object.id, + algorithm: req.body.algorithm + } + } + }); + + return object; + } + }); + + server.route({ + method: "POST", + url: "/get", + config: { + rateLimit: writeLimit + }, + schema: { + description: "KMIP endpoint for getting managed objects", + body: z.object({ + id: z.string() + }), + response: { + 200: z.object({ + id: z.string(), + value: z.string(), + algorithm: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const object = await server.services.kmipOperation.get({ + ...req.kmipUser, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.body.id + }); + + await server.services.auditLog.createAuditLog({ + projectId: req.kmipUser.projectId, + actor: { + type: ActorType.KMIP_CLIENT, + metadata: { + clientId: req.kmipUser.clientId, + name: req.kmipUser.name + } + }, + event: { + type: EventType.KMIP_OPERATION_GET, + metadata: { + id: object.id + } + } + }); + + return object; + } + }); + + server.route({ + method: "POST", + url: "/get-attributes", + config: { + rateLimit: writeLimit + }, + schema: { + description: "KMIP endpoint for getting attributes of managed object", + body: z.object({ + id: z.string() + }), + response: { + 200: z.object({ + id: z.string(), + algorithm: z.string(), + isActive: z.boolean(), + createdAt: z.date(), + updatedAt: z.date() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const object = await server.services.kmipOperation.getAttributes({ + ...req.kmipUser, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.body.id + }); + + await server.services.auditLog.createAuditLog({ + projectId: req.kmipUser.projectId, + actor: { + type: ActorType.KMIP_CLIENT, + metadata: { + clientId: req.kmipUser.clientId, + name: req.kmipUser.name + } + }, + event: { + type: EventType.KMIP_OPERATION_GET_ATTRIBUTES, + metadata: { + id: object.id + } + } + }); + + return object; + } + }); + + server.route({ + method: "POST", + url: "/destroy", + config: { + rateLimit: writeLimit + }, + schema: { + description: "KMIP endpoint for destroying managed objects", + body: z.object({ + id: z.string() + }), + response: { + 200: z.object({ + id: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const object = await server.services.kmipOperation.destroy({ + ...req.kmipUser, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.body.id + }); + + await server.services.auditLog.createAuditLog({ + projectId: req.kmipUser.projectId, + actor: { + type: ActorType.KMIP_CLIENT, + metadata: { + clientId: req.kmipUser.clientId, + name: req.kmipUser.name + } + }, + event: { + type: EventType.KMIP_OPERATION_DESTROY, + metadata: { + id: object.id + } + } + }); + + return object; + } + }); + + server.route({ + method: "POST", + url: "/activate", + config: { + rateLimit: writeLimit + }, + schema: { + description: "KMIP endpoint for activating managed object", + body: z.object({ + id: z.string() + }), + response: { + 200: z.object({ + id: z.string(), + isActive: z.boolean() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const object = await server.services.kmipOperation.activate({ + ...req.kmipUser, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.body.id + }); + + await server.services.auditLog.createAuditLog({ + projectId: req.kmipUser.projectId, + actor: { + type: ActorType.KMIP_CLIENT, + metadata: { + clientId: req.kmipUser.clientId, + name: req.kmipUser.name + } + }, + event: { + type: EventType.KMIP_OPERATION_ACTIVATE, + metadata: { + id: object.id + } + } + }); + + return object; + } + }); + + server.route({ + method: "POST", + url: "/revoke", + config: { + rateLimit: writeLimit + }, + schema: { + description: "KMIP endpoint for revoking managed object", + body: z.object({ + id: z.string() + }), + response: { + 200: z.object({ + id: z.string(), + updatedAt: z.date() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const object = await server.services.kmipOperation.revoke({ + ...req.kmipUser, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.body.id + }); + + await server.services.auditLog.createAuditLog({ + projectId: req.kmipUser.projectId, + actor: { + type: ActorType.KMIP_CLIENT, + metadata: { + clientId: req.kmipUser.clientId, + name: req.kmipUser.name + } + }, + event: { + type: EventType.KMIP_OPERATION_REVOKE, + metadata: { + id: object.id + } + } + }); + + return object; + } + }); + + server.route({ + method: "POST", + url: "/locate", + config: { + rateLimit: writeLimit + }, + schema: { + description: "KMIP endpoint for locating managed objects", + response: { + 200: z.object({ + objects: z + .object({ + id: z.string(), + name: z.string(), + isActive: z.boolean(), + algorithm: z.string(), + createdAt: z.date(), + updatedAt: z.date() + }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const objects = await server.services.kmipOperation.locate({ + ...req.kmipUser, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + projectId: req.kmipUser.projectId, + actor: { + type: ActorType.KMIP_CLIENT, + metadata: { + clientId: req.kmipUser.clientId, + name: req.kmipUser.name + } + }, + event: { + type: EventType.KMIP_OPERATION_LOCATE, + metadata: { + ids: objects.map((obj) => obj.id) + } + } + }); + + return { + objects + }; + } + }); + + server.route({ + method: "POST", + url: "/register", + config: { + rateLimit: writeLimit + }, + schema: { + description: "KMIP endpoint for registering managed object", + body: z.object({ + key: z.string(), + name: z.string(), + algorithm: z.nativeEnum(SymmetricKeyAlgorithm) + }), + response: { + 200: z.object({ + id: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const object = await server.services.kmipOperation.register({ + ...req.kmipUser, + ...req.body, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + projectId: req.kmipUser.projectId, + actor: { + type: ActorType.KMIP_CLIENT, + metadata: { + clientId: req.kmipUser.clientId, + name: req.kmipUser.name + } + }, + event: { + type: EventType.KMIP_OPERATION_REGISTER, + metadata: { + id: object.id, + algorithm: req.body.algorithm, + name: object.name + } + } + }); + + return object; + } + }); +}; diff --git a/backend/src/ee/routes/v1/ldap-router.ts b/backend/src/ee/routes/v1/ldap-router.ts index 735ba632c..5f80ad02b 100644 --- a/backend/src/ee/routes/v1/ldap-router.ts +++ b/backend/src/ee/routes/v1/ldap-router.ts @@ -14,7 +14,7 @@ import { FastifyRequest } from "fastify"; import LdapStrategy from "passport-ldapauth"; import { z } from "zod"; -import { LdapConfigsSchema, LdapGroupMapsSchema } from "@app/db/schemas"; +import { LdapGroupMapsSchema } from "@app/db/schemas"; import { TLDAPConfig } from "@app/ee/services/ldap-config/ldap-config-types"; import { isValidLdapFilter, searchGroups } from "@app/ee/services/ldap-config/ldap-fns"; import { getConfig } from "@app/lib/config/env"; @@ -22,6 +22,7 @@ import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { SanitizedLdapConfigSchema } from "@app/server/routes/sanitizedSchema/directory-config"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerLdapRouter = async (server: FastifyZodProvider) => { @@ -60,8 +61,8 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { if (ldapConfig.groupSearchBase) { const groupFilter = "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))"; const groupSearchFilter = (ldapConfig.groupSearchFilter || groupFilter) - .replace(/{{\.Username}}/g, user.uid) - .replace(/{{\.UserDN}}/g, user.dn); + .replaceAll("{{.Username}}", user.uid) + .replaceAll("{{.UserDN}}", user.dn); if (!isValidLdapFilter(groupSearchFilter)) { throw new Error("Generated LDAP search filter is invalid."); @@ -187,7 +188,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { caCert: z.string().trim().default("") }), response: { - 200: LdapConfigsSchema + 200: SanitizedLdapConfigSchema } }, handler: async (req) => { @@ -228,7 +229,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { .partial() .merge(z.object({ organizationId: z.string() })), response: { - 200: LdapConfigsSchema + 200: SanitizedLdapConfigSchema } }, handler: async (req) => { diff --git a/backend/src/ee/routes/v1/oidc-router.ts b/backend/src/ee/routes/v1/oidc-router.ts index e675121e9..1bfc4d696 100644 --- a/backend/src/ee/routes/v1/oidc-router.ts +++ b/backend/src/ee/routes/v1/oidc-router.ts @@ -9,19 +9,33 @@ import { Authenticator, Strategy } from "@fastify/passport"; import fastifySession from "@fastify/session"; import RedisStore from "connect-redis"; -import { Redis } from "ioredis"; import { z } from "zod"; -import { OidcConfigsSchema } from "@app/db/schemas/oidc-configs"; -import { OIDCConfigurationType } from "@app/ee/services/oidc/oidc-config-types"; +import { OidcConfigsSchema } from "@app/db/schemas"; +import { OIDCConfigurationType, OIDCJWTSignatureAlgorithm } from "@app/ee/services/oidc/oidc-config-types"; import { getConfig } from "@app/lib/config/env"; import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +const SanitizedOidcConfigSchema = OidcConfigsSchema.pick({ + id: true, + issuer: true, + authorizationEndpoint: true, + configurationType: true, + discoveryURL: true, + jwksUri: true, + tokenEndpoint: true, + userinfoEndpoint: true, + orgId: true, + isActive: true, + allowedEmailDomains: true, + manageGroupMemberships: true, + jwtSignatureAlgorithm: true +}); + export const registerOidcRouter = async (server: FastifyZodProvider) => { const appCfg = getConfig(); - const redis = new Redis(appCfg.REDIS_URL); const passport = new Authenticator({ key: "oidc", userProperty: "passportUser" }); /* @@ -30,7 +44,7 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { - Fastify session <> Redis structure is based on the ff: https://github.com/fastify/session/blob/master/examples/redis.js */ const redisStore = new RedisStore({ - client: redis, + client: server.redis, prefix: "oidc-session:", ttl: 600 // 10 minutes }); @@ -123,11 +137,12 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { url: "/login/error", method: "GET", handler: async (req, res) => { + const failureMessage = req.session.get("messages"); await req.session.destroy(); return res.status(500).send({ error: "Authentication error", - details: req.query + details: failureMessage ?? req.query }); } }); @@ -144,7 +159,7 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { orgSlug: z.string().trim() }), response: { - 200: OidcConfigsSchema.pick({ + 200: SanitizedOidcConfigSchema.pick({ id: true, issuer: true, authorizationEndpoint: true, @@ -155,7 +170,9 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { discoveryURL: true, isActive: true, orgId: true, - allowedEmailDomains: true + allowedEmailDomains: true, + manageGroupMemberships: true, + jwtSignatureAlgorithm: true }).extend({ clientId: z.string(), clientSecret: z.string() @@ -209,12 +226,14 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { userinfoEndpoint: z.string().trim(), clientId: z.string().trim(), clientSecret: z.string().trim(), - isActive: z.boolean() + isActive: z.boolean(), + manageGroupMemberships: z.boolean().optional(), + jwtSignatureAlgorithm: z.nativeEnum(OIDCJWTSignatureAlgorithm).optional() }) .partial() .merge(z.object({ orgSlug: z.string() })), response: { - 200: OidcConfigsSchema.pick({ + 200: SanitizedOidcConfigSchema.pick({ id: true, issuer: true, authorizationEndpoint: true, @@ -225,7 +244,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { userinfoEndpoint: true, orgId: true, allowedEmailDomains: true, - isActive: true + isActive: true, + manageGroupMemberships: true }) } }, @@ -274,7 +294,12 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { clientId: z.string().trim(), clientSecret: z.string().trim(), isActive: z.boolean(), - orgSlug: z.string().trim() + orgSlug: z.string().trim(), + manageGroupMemberships: z.boolean().optional().default(false), + jwtSignatureAlgorithm: z + .nativeEnum(OIDCJWTSignatureAlgorithm) + .optional() + .default(OIDCJWTSignatureAlgorithm.RS256) }) .superRefine((data, ctx) => { if (data.configurationType === OIDCConfigurationType.CUSTOM) { @@ -325,19 +350,7 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { } }), response: { - 200: OidcConfigsSchema.pick({ - id: true, - issuer: true, - authorizationEndpoint: true, - configurationType: true, - discoveryURL: true, - jwksUri: true, - tokenEndpoint: true, - userinfoEndpoint: true, - orgId: true, - isActive: true, - allowedEmailDomains: true - }) + 200: SanitizedOidcConfigSchema } }, @@ -352,4 +365,25 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { return oidc; } }); + + server.route({ + method: "GET", + url: "/manage-group-memberships", + schema: { + querystring: z.object({ + orgId: z.string().trim().min(1, "Org ID is required") + }), + response: { + 200: z.object({ + isEnabled: z.boolean() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const isEnabled = await server.services.oidc.isOidcManageGroupMembershipsEnabled(req.query.orgId, req.permission); + + return { isEnabled }; + } + }); }; diff --git a/backend/src/ee/routes/v1/org-role-router.ts b/backend/src/ee/routes/v1/org-role-router.ts index 232f4b0b5..c8ee03a99 100644 --- a/backend/src/ee/routes/v1/org-role-router.ts +++ b/backend/src/ee/routes/v1/org-role-router.ts @@ -1,8 +1,8 @@ -import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -18,19 +18,13 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { organizationId: z.string().trim() }), body: z.object({ - slug: z - .string() - .min(1) - .trim() - .refine( - (val) => !Object.values(OrgMembershipRole).includes(val as OrgMembershipRole), - "Please choose a different slug, the slug you have entered is reserved" - ) - .refine((v) => slugify(v) === v, { - message: "Slug must be a valid" - }), + slug: slugSchema({ min: 1, max: 64 }).refine( + (val) => !Object.values(OrgMembershipRole).includes(val as OrgMembershipRole), + "Please choose a different slug, the slug you have entered is reserved" + ), name: z.string().trim(), - description: z.string().trim().optional(), + description: z.string().trim().nullish(), + // TODO(scott): once UI refactored permissions: OrgPermissionSchema.array() permissions: z.any().array() }), response: { @@ -94,19 +88,16 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { roleId: z.string().trim() }), body: z.object({ - slug: z - .string() - .trim() - .optional() + // TODO: Switch to slugSchema after verifying correct methods with Akhil - Omar 11/24 + slug: slugSchema({ min: 1, max: 64 }) .refine( - (val) => typeof val !== "undefined" && !Object.keys(OrgMembershipRole).includes(val), + (val) => !Object.keys(OrgMembershipRole).includes(val), "Please choose a different slug, the slug you have entered is reserved." ) - .refine((val) => typeof val === "undefined" || slugify(val) === val, { - message: "Slug must be a valid" - }), + .optional(), name: z.string().trim().optional(), - description: z.string().trim().optional(), + description: z.string().trim().nullish(), + // TODO(scott): once UI refactored permissions: OrgPermissionSchema.array().optional() permissions: z.any().array().optional() }), response: { diff --git a/backend/src/ee/routes/v1/project-role-router.ts b/backend/src/ee/routes/v1/project-role-router.ts index ba2c0aa9f..469460491 100644 --- a/backend/src/ee/routes/v1/project-role-router.ts +++ b/backend/src/ee/routes/v1/project-role-router.ts @@ -1,5 +1,4 @@ import { packRules } from "@casl/ability/extra"; -import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas"; @@ -9,6 +8,7 @@ import { } from "@app/ee/services/permission/project-permission"; import { PROJECT_ROLE } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { SanitizedRoleSchemaV1 } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -32,21 +32,14 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { projectSlug: z.string().trim().describe(PROJECT_ROLE.CREATE.projectSlug) }), body: z.object({ - slug: z - .string() - .toLowerCase() - .trim() - .min(1) + slug: slugSchema({ max: 64 }) .refine( (val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole), "Please choose a different slug, the slug you have entered is reserved" ) - .refine((v) => slugify(v) === v, { - message: "Slug must be a valid" - }) .describe(PROJECT_ROLE.CREATE.slug), name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name), - description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description), + description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description), permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.CREATE.permissions) }), response: { @@ -94,23 +87,15 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId) }), body: z.object({ - slug: z - .string() - .toLowerCase() - .trim() - .optional() - .describe(PROJECT_ROLE.UPDATE.slug) + slug: slugSchema({ max: 64 }) .refine( - (val) => - typeof val === "undefined" || - !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole), + (val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole), "Please choose a different slug, the slug you have entered is reserved" ) - .refine((val) => typeof val === "undefined" || slugify(val) === val, { - message: "Slug must be a valid" - }), + .describe(PROJECT_ROLE.UPDATE.slug) + .optional(), name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name), - description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description), + description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description), permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional() }), response: { diff --git a/backend/src/ee/routes/v1/project-router.ts b/backend/src/ee/routes/v1/project-router.ts index e3956731e..ab9d1be6c 100644 --- a/backend/src/ee/routes/v1/project-router.ts +++ b/backend/src/ee/routes/v1/project-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { AuditLogsSchema, SecretSnapshotsSchema } from "@app/db/schemas"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; -import { AUDIT_LOGS, PROJECTS } from "@app/lib/api-docs"; +import { ApiDocsTags, AUDIT_LOGS, PROJECTS } from "@app/lib/api-docs"; import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -17,6 +17,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Projects], description: "Return project secret snapshots ids", security: [ { diff --git a/backend/src/ee/routes/v1/project-template-router.ts b/backend/src/ee/routes/v1/project-template-router.ts index 5b115ab4e..08d16414b 100644 --- a/backend/src/ee/routes/v1/project-template-router.ts +++ b/backend/src/ee/routes/v1/project-template-router.ts @@ -1,4 +1,3 @@ -import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { ProjectMembershipRole, ProjectTemplatesSchema } from "@app/db/schemas"; @@ -6,24 +5,15 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants"; import { isInfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-fns"; -import { ProjectTemplates } from "@app/lib/api-docs"; +import { ApiDocsTags, ProjectTemplates } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; +import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { AuthMode } from "@app/services/auth/auth-type"; const MAX_JSON_SIZE_LIMIT_IN_BYTES = 32_768; -const SlugSchema = z - .string() - .trim() - .min(1) - .max(32) - .refine((val) => val.toLowerCase() === val, "Must be lowercase") - .refine((v) => slugify(v) === v, { - message: "Must be valid slug format" - }); - const isReservedRoleSlug = (slug: string) => Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole); @@ -34,14 +24,14 @@ const SanitizedProjectTemplateSchema = ProjectTemplatesSchema.extend({ roles: z .object({ name: z.string().trim().min(1), - slug: SlugSchema, + slug: slugSchema(), permissions: UnpackedPermissionSchema.array() }) .array(), environments: z .object({ name: z.string().trim().min(1), - slug: SlugSchema, + slug: slugSchema(), position: z.number().min(1) }) .array() @@ -50,7 +40,7 @@ const SanitizedProjectTemplateSchema = ProjectTemplatesSchema.extend({ const ProjectTemplateRolesSchema = z .object({ name: z.string().trim().min(1), - slug: SlugSchema, + slug: slugSchema(), permissions: ProjectPermissionV2Schema.array() }) .array() @@ -78,7 +68,7 @@ const ProjectTemplateRolesSchema = z const ProjectTemplateEnvironmentsSchema = z .object({ name: z.string().trim().min(1), - slug: SlugSchema, + slug: slugSchema(), position: z.number().min(1) }) .array() @@ -111,6 +101,8 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider) rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectTemplates], description: "List project templates for the current organization.", response: { 200: z.object({ @@ -147,6 +139,8 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider) rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectTemplates], description: "Get a project template by ID.", params: z.object({ templateId: z.string().uuid() @@ -186,11 +180,15 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider) rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectTemplates], description: "Create a project template.", body: z.object({ - name: SlugSchema.refine((val) => !isInfisicalProjectTemplate(val), { - message: `The requested project template name is reserved.` - }).describe(ProjectTemplates.CREATE.name), + name: slugSchema({ field: "name" }) + .refine((val) => !isInfisicalProjectTemplate(val), { + message: `The requested project template name is reserved.` + }) + .describe(ProjectTemplates.CREATE.name), description: z.string().max(256).trim().optional().describe(ProjectTemplates.CREATE.description), roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles), environments: ProjectTemplateEnvironmentsSchema.default(ProjectTemplateDefaultEnvironments).describe( @@ -227,12 +225,15 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider) rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectTemplates], description: "Update a project template.", params: z.object({ templateId: z.string().uuid().describe(ProjectTemplates.UPDATE.templateId) }), body: z.object({ - name: SlugSchema.refine((val) => !isInfisicalProjectTemplate(val), { - message: `The requested project template name is reserved.` - }) + name: slugSchema({ field: "name" }) + .refine((val) => !isInfisicalProjectTemplate(val), { + message: `The requested project template name is reserved.` + }) .optional() .describe(ProjectTemplates.UPDATE.name), description: z.string().max(256).trim().optional().describe(ProjectTemplates.UPDATE.description), @@ -276,6 +277,8 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider) rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectTemplates], description: "Delete a project template.", params: z.object({ templateId: z.string().uuid().describe(ProjectTemplates.DELETE.templateId) }), diff --git a/backend/src/ee/routes/v1/saml-router.ts b/backend/src/ee/routes/v1/saml-router.ts index aaebd9b6f..f2df2fb89 100644 --- a/backend/src/ee/routes/v1/saml-router.ts +++ b/backend/src/ee/routes/v1/saml-router.ts @@ -12,20 +12,20 @@ import { MultiSamlStrategy } from "@node-saml/passport-saml"; import { FastifyRequest } from "fastify"; import { z } from "zod"; -import { SamlConfigsSchema } from "@app/db/schemas"; import { SamlProviders, TGetSamlCfgDTO } from "@app/ee/services/saml-config/saml-config-types"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { SanitizedSamlConfigSchema } from "@app/server/routes/sanitizedSchema/directory-config"; import { AuthMode } from "@app/services/auth/auth-type"; type TSAMLConfig = { callbackUrl: string; entryPoint: string; issuer: string; - cert: string; + idpCert: string; audience: string; wantAuthnResponseSigned?: boolean; wantAssertionsSigned?: boolean; @@ -72,7 +72,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { callbackUrl: `${appCfg.SITE_URL}/api/v1/sso/saml2/${ssoConfig.id}`, entryPoint: ssoConfig.entryPoint, issuer: ssoConfig.issuer, - cert: ssoConfig.cert, + idpCert: ssoConfig.cert, audience: appCfg.SITE_URL || "" }; if (ssoConfig.authProvider === SamlProviders.JUMPCLOUD_SAML) { @@ -84,7 +84,10 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { samlConfig.audience = `spn:${ssoConfig.issuer}`; } } - if (ssoConfig.authProvider === SamlProviders.GOOGLE_SAML) { + if ( + ssoConfig.authProvider === SamlProviders.GOOGLE_SAML || + ssoConfig.authProvider === SamlProviders.AUTH0_SAML + ) { samlConfig.wantAssertionsSigned = false; } @@ -122,6 +125,11 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { }, `email: ${email} firstName: ${profile.firstName as string}` ); + + throw new BadRequestError({ + message: + "Missing email or first name. Please double check your SAML attribute mapping for the selected provider." + }); } const userMetadata = Object.keys(profile.attributes || {}) @@ -215,12 +223,18 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { samlConfigId: z.string().trim() }) }, - preValidation: passport.authenticate("saml", { - session: false, - failureFlash: true, - failureRedirect: "/login/provider/error" - // this is due to zod type difference - }) as any, + preValidation: passport.authenticate( + "saml", + { + session: false + }, + async (req, res, err, user) => { + if (err) { + throw new BadRequestError({ message: `Saml authentication failed. ${err?.message}`, error: err }); + } + req.passportUser = user as { isUserCompleted: boolean; providerAuthToken: string }; + } + ) as any, // this is due to zod type difference handler: (req, res) => { if (req.passportUser.isUserCompleted) { return res.redirect( @@ -290,19 +304,25 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { cert: z.string() }), response: { - 200: SamlConfigsSchema + 200: SanitizedSamlConfigSchema } }, handler: async (req) => { - const saml = await server.services.saml.createSamlCfg({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - orgId: req.body.organizationId, - ...req.body + const { isActive, authProvider, issuer, entryPoint, cert } = req.body; + const { permission } = req; + + return server.services.saml.createSamlCfg({ + isActive, + authProvider, + issuer, + entryPoint, + idpCert: cert, + actor: permission.type, + actorId: permission.id, + actorAuthMethod: permission.authMethod, + actorOrgId: permission.orgId, + orgId: req.body.organizationId }); - return saml; } }); @@ -325,19 +345,25 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { .partial() .merge(z.object({ organizationId: z.string() })), response: { - 200: SamlConfigsSchema + 200: SanitizedSamlConfigSchema } }, handler: async (req) => { - const saml = await server.services.saml.updateSamlCfg({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - orgId: req.body.organizationId, - ...req.body + const { isActive, authProvider, issuer, entryPoint, cert } = req.body; + const { permission } = req; + + return server.services.saml.updateSamlCfg({ + isActive, + authProvider, + issuer, + entryPoint, + idpCert: cert, + actor: permission.type, + actorId: permission.id, + actorAuthMethod: permission.authMethod, + actorOrgId: permission.orgId, + orgId: req.body.organizationId }); - return saml; } }); }; diff --git a/backend/src/ee/routes/v1/secret-approval-policy-router.ts b/backend/src/ee/routes/v1/secret-approval-policy-router.ts index 40f0a71bd..846b60923 100644 --- a/backend/src/ee/routes/v1/secret-approval-policy-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-policy-router.ts @@ -35,7 +35,8 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi .array() .min(1, { message: "At least one approver should be provided" }), approvals: z.number().min(1).default(1), - enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard) + enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard), + allowedSelfApprovals: z.boolean().default(true) }), response: { 200: z.object({ @@ -85,7 +86,8 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi .nullable() .transform((val) => (val ? removeTrailingSlash(val) : val)) .transform((val) => (val === "" ? "/" : val)), - enforcementLevel: z.nativeEnum(EnforcementLevel).optional() + enforcementLevel: z.nativeEnum(EnforcementLevel).optional(), + allowedSelfApprovals: z.boolean().default(true) }), response: { 200: z.object({ diff --git a/backend/src/ee/routes/v1/secret-approval-request-router.ts b/backend/src/ee/routes/v1/secret-approval-request-router.ts index 5fbf784f6..7d2cdcc0c 100644 --- a/backend/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts @@ -1,17 +1,13 @@ import { z } from "zod"; -import { - SecretApprovalRequestsReviewersSchema, - SecretApprovalRequestsSchema, - SecretTagsSchema, - UsersSchema -} from "@app/db/schemas"; +import { SecretApprovalRequestsReviewersSchema, SecretApprovalRequestsSchema, UsersSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { secretRawSchema } from "@app/server/routes/sanitizedSchemas"; +import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; +import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; const approvalRequestUser = z.object({ userId: z.string().nullable().optional() }).merge( UsersSchema.pick({ @@ -52,7 +48,9 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv }) .array(), secretPath: z.string().optional().nullable(), - enforcementLevel: z.string() + enforcementLevel: z.string(), + deletedAt: z.date().nullish(), + allowedSelfApprovals: z.boolean() }), committerUser: approvalRequestUser, commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(), @@ -157,7 +155,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv id: z.string() }), body: z.object({ - status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED]) + status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED]), + comment: z.string().optional() }), response: { 200: z.object({ @@ -173,8 +172,25 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, approvalId: req.params.id, - status: req.body.status + status: req.body.status, + comment: req.body.comment }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: review.projectId, + event: { + type: EventType.SECRET_APPROVAL_REQUEST_REVIEW, + metadata: { + secretApprovalRequestId: review.requestId, + reviewedBy: review.reviewerUserId, + status: review.status as ApprovalStatus, + comment: review.comment || "" + } + } + }); + return { review }; } }); @@ -230,15 +246,6 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv } }); - const tagSchema = SecretTagsSchema.pick({ - id: true, - slug: true, - name: true, - color: true - }) - .array() - .optional(); - server.route({ method: "GET", url: "/:id", @@ -260,18 +267,23 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv approvals: z.number(), approvers: approvalRequestUser.array(), secretPath: z.string().optional().nullable(), - enforcementLevel: z.string() + enforcementLevel: z.string(), + deletedAt: z.date().nullish(), + allowedSelfApprovals: z.boolean() }), environment: z.string(), statusChangedByUser: approvalRequestUser.optional(), committerUser: approvalRequestUser, - reviewers: approvalRequestUser.extend({ status: z.string() }).array(), + reviewers: approvalRequestUser.extend({ status: z.string(), comment: z.string().optional() }).array(), secretPath: z.string(), commits: secretRawSchema - .omit({ _id: true, environment: true, workspace: true, type: true, version: true }) + .omit({ _id: true, environment: true, workspace: true, type: true, version: true, secretValue: true }) .extend({ + secretValue: z.string().optional(), + isRotatedSecret: z.boolean().optional(), op: z.string(), - tags: tagSchema, + tags: SanitizedTagSchema.array().optional(), + secretMetadata: ResourceMetadataSchema.nullish(), secret: z .object({ id: z.string(), @@ -289,7 +301,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv secretKey: z.string(), secretValue: z.string().optional(), secretComment: z.string().optional(), - tags: tagSchema + tags: SanitizedTagSchema.array().optional(), + secretMetadata: ResourceMetadataSchema.nullish() }) .optional() }) diff --git a/backend/src/ee/routes/v1/secret-rotation-provider-router.ts b/backend/src/ee/routes/v1/secret-rotation-provider-router.ts index 58419d3b7..e6a1ac72b 100644 --- a/backend/src/ee/routes/v1/secret-rotation-provider-router.ts +++ b/backend/src/ee/routes/v1/secret-rotation-provider-router.ts @@ -23,7 +23,8 @@ export const registerSecretRotationProviderRouter = async (server: FastifyZodPro title: z.string(), image: z.string().optional(), description: z.string().optional(), - template: z.any() + template: z.any(), + isDeprecated: z.boolean().optional() }) .array() }) diff --git a/backend/src/ee/routes/v1/secret-router.ts b/backend/src/ee/routes/v1/secret-router.ts new file mode 100644 index 000000000..a964eb1b8 --- /dev/null +++ b/backend/src/ee/routes/v1/secret-router.ts @@ -0,0 +1,71 @@ +import z from "zod"; + +import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; +import { RAW_SECRETS } from "@app/lib/api-docs"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +const AccessListEntrySchema = z + .object({ + allowedActions: z.nativeEnum(ProjectPermissionSecretActions).array(), + id: z.string(), + membershipId: z.string(), + name: z.string() + }) + .array(); + +export const registerSecretRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/:secretName/access-list", + config: { + rateLimit: readLimit + }, + schema: { + description: "Get list of users, machine identities, and groups with access to a secret", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + secretName: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.secretName) + }), + querystring: z.object({ + workspaceId: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.workspaceId), + environment: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.environment), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(RAW_SECRETS.GET_ACCESS_LIST.secretPath) + }), + response: { + 200: z.object({ + groups: AccessListEntrySchema, + identities: AccessListEntrySchema, + users: AccessListEntrySchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { secretName } = req.params; + const { secretPath, environment, workspaceId: projectId } = req.query; + + return server.services.secret.getSecretAccessList({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + secretPath, + environment, + projectId, + secretName + }); + } + }); +}; diff --git a/backend/src/ee/routes/v1/secret-scanning-router.ts b/backend/src/ee/routes/v1/secret-scanning-router.ts index 89784600a..f144a6c00 100644 --- a/backend/src/ee/routes/v1/secret-scanning-router.ts +++ b/backend/src/ee/routes/v1/secret-scanning-router.ts @@ -1,9 +1,13 @@ import { z } from "zod"; import { GitAppOrgSchema, SecretScanningGitRisksSchema } from "@app/db/schemas"; -import { SecretScanningRiskStatus } from "@app/ee/services/secret-scanning/secret-scanning-types"; +import { + SecretScanningResolvedStatus, + SecretScanningRiskStatus +} from "@app/ee/services/secret-scanning/secret-scanning-types"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; +import { OrderByDirection } from "@app/lib/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"; @@ -97,6 +101,45 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) = } }); + server.route({ + url: "/organization/:organizationId/risks/export", + method: "GET", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ organizationId: z.string().trim() }), + querystring: z.object({ + repositoryNames: z + .string() + .optional() + .nullable() + .transform((val) => (val ? val.split(",") : undefined)), + resolvedStatus: z.nativeEnum(SecretScanningResolvedStatus).optional() + }), + response: { + 200: z.object({ + risks: SecretScanningGitRisksSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const risks = await server.services.secretScanning.getAllRisksByOrg({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + orgId: req.params.organizationId, + filter: { + repositoryNames: req.query.repositoryNames, + resolvedStatus: req.query.resolvedStatus + } + }); + return { risks }; + } + }); + server.route({ url: "/organization/:organizationId/risks", method: "GET", @@ -105,20 +148,46 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) = }, schema: { params: z.object({ organizationId: z.string().trim() }), + + querystring: z.object({ + offset: z.coerce.number().min(0).default(0), + limit: z.coerce.number().min(1).max(20000).default(100), + orderBy: z.enum(["createdAt", "name"]).default("createdAt"), + orderDirection: z.nativeEnum(OrderByDirection).default(OrderByDirection.DESC), + repositoryNames: z + .string() + .optional() + .nullable() + .transform((val) => (val ? val.split(",") : undefined)), + resolvedStatus: z.nativeEnum(SecretScanningResolvedStatus).optional() + }), + response: { - 200: z.object({ risks: SecretScanningGitRisksSchema.array() }) + 200: z.object({ + risks: SecretScanningGitRisksSchema.array(), + totalCount: z.number(), + repos: z.array(z.string()) + }) } }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { risks } = await server.services.secretScanning.getRisksByOrg({ + const { risks, totalCount, repos } = await server.services.secretScanning.getRisksByOrg({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - orgId: req.params.organizationId + orgId: req.params.organizationId, + filter: { + limit: req.query.limit, + offset: req.query.offset, + orderBy: req.query.orderBy, + orderDirection: req.query.orderDirection, + repositoryNames: req.query.repositoryNames, + resolvedStatus: req.query.resolvedStatus + } }); - return { risks }; + return { risks, totalCount, repos }; } }); diff --git a/backend/src/ee/routes/v1/secret-version-router.ts b/backend/src/ee/routes/v1/secret-version-router.ts index 11443ebfe..a09a05c91 100644 --- a/backend/src/ee/routes/v1/secret-version-router.ts +++ b/backend/src/ee/routes/v1/secret-version-router.ts @@ -22,7 +22,11 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) => }), response: { 200: z.object({ - secretVersions: secretRawSchema.array() + secretVersions: secretRawSchema + .extend({ + secretValueHidden: z.boolean() + }) + .array() }) } }, @@ -37,6 +41,7 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) => offset: req.query.offset, secretId: req.params.secretId }); + return { secretVersions }; } }); diff --git a/backend/src/ee/routes/v1/snapshot-router.ts b/backend/src/ee/routes/v1/snapshot-router.ts index a716aabd7..3ee80adce 100644 --- a/backend/src/ee/routes/v1/snapshot-router.ts +++ b/backend/src/ee/routes/v1/snapshot-router.ts @@ -1,10 +1,10 @@ import { z } from "zod"; -import { SecretSnapshotsSchema, SecretTagsSchema } from "@app/db/schemas"; -import { PROJECTS } from "@app/lib/api-docs"; +import { SecretSnapshotsSchema } from "@app/db/schemas"; +import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { secretRawSchema } from "@app/server/routes/sanitizedSchemas"; +import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerSnapshotRouter = async (server: FastifyZodProvider) => { @@ -31,13 +31,10 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => { secretVersions: secretRawSchema .omit({ _id: true, environment: true, workspace: true, type: true }) .extend({ + secretValueHidden: z.boolean(), secretId: z.string(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - name: true, - color: true - }).array() + tags: SanitizedTagSchema.array(), + isRotatedSecret: z.boolean().optional() }) .array(), folderVersion: z.object({ id: z.string(), name: z.string() }).array(), @@ -56,6 +53,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => { actorOrgId: req.permission.orgId, id: req.params.secretSnapshotId }); + return { secretSnapshot }; } }); @@ -67,6 +65,8 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Projects], description: "Roll back project secrets to those captured in a secret snapshot version.", security: [ { diff --git a/backend/src/ee/routes/v1/ssh-certificate-authority-router.ts b/backend/src/ee/routes/v1/ssh-certificate-authority-router.ts new file mode 100644 index 000000000..e20d49263 --- /dev/null +++ b/backend/src/ee/routes/v1/ssh-certificate-authority-router.ts @@ -0,0 +1,312 @@ +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { normalizeSshPrivateKey } from "@app/ee/services/ssh/ssh-certificate-authority-fns"; +import { sanitizedSshCa } from "@app/ee/services/ssh/ssh-certificate-authority-schema"; +import { SshCaKeySource, SshCaStatus } from "@app/ee/services/ssh/ssh-certificate-authority-types"; +import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types"; +import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema"; +import { ApiDocsTags, SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs"; +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 registerSshCaRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateAuthorities], + description: "Create SSH CA", + body: z + .object({ + projectId: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.projectId), + friendlyName: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.friendlyName), + keyAlgorithm: z + .nativeEnum(SshCertKeyAlgorithm) + .default(SshCertKeyAlgorithm.ED25519) + .describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm), + publicKey: z.string().trim().optional().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.publicKey), + privateKey: z + .string() + .trim() + .optional() + .transform((val) => (val ? normalizeSshPrivateKey(val) : undefined)) + .describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.privateKey), + keySource: z + .nativeEnum(SshCaKeySource) + .default(SshCaKeySource.INTERNAL) + .describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.keySource) + }) + .refine((data) => data.keySource === SshCaKeySource.INTERNAL || (!!data.publicKey && !!data.privateKey), { + message: "publicKey and privateKey are required when keySource is external", + path: ["publicKey"] + }) + .refine((data) => data.keySource === SshCaKeySource.EXTERNAL || !!data.keyAlgorithm, { + message: "keyAlgorithm is required when keySource is internal", + path: ["keyAlgorithm"] + }), + response: { + 200: z.object({ + ca: sanitizedSshCa.extend({ + publicKey: z.string() + }) + }) + } + }, + handler: async (req) => { + const ca = await server.services.sshCertificateAuthority.createSshCa({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.CREATE_SSH_CA, + metadata: { + sshCaId: ca.id, + friendlyName: ca.friendlyName + } + } + }); + + return { + ca + }; + } + }); + + server.route({ + method: "GET", + url: "/:sshCaId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateAuthorities], + description: "Get SSH CA", + params: z.object({ + sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.GET.sshCaId) + }), + response: { + 200: z.object({ + ca: sanitizedSshCa.extend({ + publicKey: z.string() + }) + }) + } + }, + handler: async (req) => { + const ca = await server.services.sshCertificateAuthority.getSshCaById({ + caId: req.params.sshCaId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.GET_SSH_CA, + metadata: { + sshCaId: ca.id, + friendlyName: ca.friendlyName + } + } + }); + + return { + ca + }; + } + }); + + server.route({ + method: "GET", + url: "/:sshCaId/public-key", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateAuthorities], + description: "Get public key of SSH CA", + params: z.object({ + sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.GET_PUBLIC_KEY.sshCaId) + }), + response: { + 200: z.string() + } + }, + handler: async (req) => { + const publicKey = await server.services.sshCertificateAuthority.getSshCaPublicKey({ + caId: req.params.sshCaId + }); + + return publicKey; + } + }); + + server.route({ + method: "PATCH", + url: "/:sshCaId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateAuthorities], + description: "Update SSH CA", + params: z.object({ + sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.UPDATE.sshCaId) + }), + body: z.object({ + friendlyName: z.string().optional().describe(SSH_CERTIFICATE_AUTHORITIES.UPDATE.friendlyName), + status: z.nativeEnum(SshCaStatus).optional().describe(SSH_CERTIFICATE_AUTHORITIES.UPDATE.status) + }), + response: { + 200: z.object({ + ca: sanitizedSshCa.extend({ + publicKey: z.string() + }) + }) + } + }, + handler: async (req) => { + const ca = await server.services.sshCertificateAuthority.updateSshCaById({ + caId: req.params.sshCaId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.UPDATE_SSH_CA, + metadata: { + sshCaId: ca.id, + friendlyName: ca.friendlyName, + status: ca.status as SshCaStatus + } + } + }); + + return { + ca + }; + } + }); + + server.route({ + method: "DELETE", + url: "/:sshCaId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateAuthorities], + description: "Delete SSH CA", + params: z.object({ + sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.DELETE.sshCaId) + }), + response: { + 200: z.object({ + ca: sanitizedSshCa + }) + } + }, + handler: async (req) => { + const ca = await server.services.sshCertificateAuthority.deleteSshCaById({ + caId: req.params.sshCaId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.DELETE_SSH_CA, + metadata: { + sshCaId: ca.id, + friendlyName: ca.friendlyName + } + } + }); + + return { + ca + }; + } + }); + + server.route({ + method: "GET", + url: "/:sshCaId/certificate-templates", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateAuthorities], + description: "Get list of certificate templates for the SSH CA", + params: z.object({ + sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.GET_CERTIFICATE_TEMPLATES.sshCaId) + }), + response: { + 200: z.object({ + certificateTemplates: sanitizedSshCertificateTemplate.array() + }) + } + }, + handler: async (req) => { + const { certificateTemplates, ca } = await server.services.sshCertificateAuthority.getSshCaCertificateTemplates({ + caId: req.params.sshCaId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.GET_SSH_CA_CERTIFICATE_TEMPLATES, + metadata: { + sshCaId: ca.id, + friendlyName: ca.friendlyName + } + } + }); + + return { + certificateTemplates + }; + } + }); +}; diff --git a/backend/src/ee/routes/v1/ssh-certificate-router.ts b/backend/src/ee/routes/v1/ssh-certificate-router.ts new file mode 100644 index 000000000..cb576e496 --- /dev/null +++ b/backend/src/ee/routes/v1/ssh-certificate-router.ts @@ -0,0 +1,190 @@ +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types"; +import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types"; +import { ApiDocsTags, SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs"; +import { ms } from "@app/lib/ms"; +import { writeLimit } from "@app/server/config/rateLimiter"; +import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; + +export const registerSshCertRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/sign", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.SshCertificates], + description: "Sign SSH public key", + body: z.object({ + certificateTemplateId: z + .string() + .trim() + .min(1) + .describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.certificateTemplateId), + publicKey: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.publicKey), + certType: z + .nativeEnum(SshCertType) + .default(SshCertType.USER) + .describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.certType), + principals: z + .array(z.string().transform((val) => val.trim())) + .nonempty("Principals array must not be empty") + .describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.principals), + ttl: z + .string() + .refine((val) => ms(val) > 0, "TTL must be a positive number") + .optional() + .describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.ttl), + keyId: z.string().trim().max(50).optional().describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.keyId) + }), + response: { + 200: z.object({ + serialNumber: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.serialNumber), + signedKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.signedKey) + }) + } + }, + handler: async (req) => { + const { serialNumber, signedPublicKey, certificateTemplate, ttl, keyId } = + await server.services.sshCertificateAuthority.signSshKey({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.SIGN_SSH_KEY, + metadata: { + certificateTemplateId: certificateTemplate.id, + certType: req.body.certType, + principals: req.body.principals, + ttl: String(ttl), + keyId + } + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SignSshKey, + distinctId: getTelemetryDistinctId(req), + properties: { + certificateTemplateId: req.body.certificateTemplateId, + principals: req.body.principals, + ...req.auditLogInfo + } + }); + + return { + serialNumber, + signedKey: signedPublicKey + }; + } + }); + + server.route({ + method: "POST", + url: "/issue", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.SshCertificates], + description: "Issue SSH credentials (certificate + key)", + body: z.object({ + certificateTemplateId: z + .string() + .trim() + .min(1) + .describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.certificateTemplateId), + keyAlgorithm: z + .nativeEnum(SshCertKeyAlgorithm) + .default(SshCertKeyAlgorithm.ED25519) + .describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.keyAlgorithm), + certType: z + .nativeEnum(SshCertType) + .default(SshCertType.USER) + .describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.certType), + principals: z + .array(z.string().transform((val) => val.trim())) + .nonempty("Principals array must not be empty") + .describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.principals), + ttl: z + .string() + .refine((val) => ms(val) > 0, "TTL must be a positive number") + .optional() + .describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.ttl), + keyId: z.string().trim().max(50).optional().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.keyId) + }), + response: { + 200: z.object({ + serialNumber: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.serialNumber), + signedKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.signedKey), + privateKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.privateKey), + publicKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.publicKey), + keyAlgorithm: z + .nativeEnum(SshCertKeyAlgorithm) + .describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.keyAlgorithm) + }) + } + }, + handler: async (req) => { + const { serialNumber, signedPublicKey, privateKey, publicKey, certificateTemplate, ttl, keyId } = + await server.services.sshCertificateAuthority.issueSshCreds({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.ISSUE_SSH_CREDS, + metadata: { + certificateTemplateId: certificateTemplate.id, + keyAlgorithm: req.body.keyAlgorithm, + certType: req.body.certType, + principals: req.body.principals, + ttl: String(ttl), + keyId + } + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.IssueSshCreds, + distinctId: getTelemetryDistinctId(req), + properties: { + certificateTemplateId: req.body.certificateTemplateId, + principals: req.body.principals, + ...req.auditLogInfo + } + }); + + return { + serialNumber, + signedKey: signedPublicKey, + privateKey, + publicKey, + keyAlgorithm: req.body.keyAlgorithm + }; + } + }); +}; diff --git a/backend/src/ee/routes/v1/ssh-certificate-template-router.ts b/backend/src/ee/routes/v1/ssh-certificate-template-router.ts new file mode 100644 index 000000000..e44693643 --- /dev/null +++ b/backend/src/ee/routes/v1/ssh-certificate-template-router.ts @@ -0,0 +1,266 @@ +import slugify from "@sindresorhus/slugify"; +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema"; +import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types"; +import { + isValidHostPattern, + isValidUserPattern +} from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-validators"; +import { ApiDocsTags, SSH_CERTIFICATE_TEMPLATES } from "@app/lib/api-docs"; +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"; + +export const registerSshCertificateTemplateRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/:certificateTemplateId", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateTemplates], + params: z.object({ + certificateTemplateId: z.string().describe(SSH_CERTIFICATE_TEMPLATES.GET.certificateTemplateId) + }), + response: { + 200: sanitizedSshCertificateTemplate + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const certificateTemplate = await server.services.sshCertificateTemplate.getSshCertTemplate({ + id: req.params.certificateTemplateId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: certificateTemplate.projectId, + event: { + type: EventType.GET_SSH_CERTIFICATE_TEMPLATE, + metadata: { + certificateTemplateId: certificateTemplate.id + } + } + }); + + return certificateTemplate; + } + }); + + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateTemplates], + body: z + .object({ + sshCaId: z.string().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.sshCaId), + name: z + .string() + .min(1) + .max(36) + .refine((v) => slugify(v) === v, { + message: "Name must be a valid slug" + }) + .describe(SSH_CERTIFICATE_TEMPLATES.CREATE.name), + ttl: z + .string() + .refine((val) => ms(val) > 0, "TTL must be a positive number") + .default("1h") + .describe(SSH_CERTIFICATE_TEMPLATES.CREATE.ttl), + maxTTL: z + .string() + .refine((val) => ms(val) > 0, "Max TTL must be a positive number") + .default("30d") + .describe(SSH_CERTIFICATE_TEMPLATES.CREATE.maxTTL), + allowedUsers: z + .array(z.string().refine(isValidUserPattern, "Invalid user pattern")) + .describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowedUsers), + allowedHosts: z + .array(z.string().refine(isValidHostPattern, "Invalid host pattern")) + .describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowedHosts), + allowUserCertificates: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowUserCertificates), + allowHostCertificates: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowHostCertificates), + allowCustomKeyIds: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowCustomKeyIds) + }) + .refine((data) => ms(data.maxTTL) >= ms(data.ttl), { + message: "Max TLL must be greater than or equal to TTL", + path: ["maxTTL"] + }), + response: { + 200: sanitizedSshCertificateTemplate + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { certificateTemplate, ca } = await server.services.sshCertificateTemplate.createSshCertTemplate({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.CREATE_SSH_CERTIFICATE_TEMPLATE, + metadata: { + certificateTemplateId: certificateTemplate.id, + sshCaId: ca.id, + name: certificateTemplate.name, + ttl: certificateTemplate.ttl, + maxTTL: certificateTemplate.maxTTL, + allowedUsers: certificateTemplate.allowedUsers, + allowedHosts: certificateTemplate.allowedHosts, + allowUserCertificates: certificateTemplate.allowUserCertificates, + allowHostCertificates: certificateTemplate.allowHostCertificates, + allowCustomKeyIds: certificateTemplate.allowCustomKeyIds + } + } + }); + + return certificateTemplate; + } + }); + + server.route({ + method: "PATCH", + url: "/:certificateTemplateId", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateTemplates], + body: z.object({ + status: z.nativeEnum(SshCertTemplateStatus).optional(), + name: z + .string() + .min(1) + .max(36) + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid slug" + }) + .optional() + .describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.name), + ttl: z + .string() + .refine((val) => ms(val) > 0, "TTL must be a positive number") + .optional() + .describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.ttl), + maxTTL: z + .string() + .refine((val) => ms(val) > 0, "Max TTL must be a positive number") + .optional() + .describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.maxTTL), + allowedUsers: z + .array(z.string().refine(isValidUserPattern, "Invalid user pattern")) + .optional() + .describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.allowedUsers), + allowedHosts: z + .array(z.string().refine(isValidHostPattern, "Invalid host pattern")) + .optional() + .describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.allowedHosts), + allowUserCertificates: z.boolean().optional().describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.allowUserCertificates), + allowHostCertificates: z.boolean().optional().describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.allowHostCertificates), + allowCustomKeyIds: z.boolean().optional().describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.allowCustomKeyIds) + }), + params: z.object({ + certificateTemplateId: z.string().describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.certificateTemplateId) + }), + response: { + 200: sanitizedSshCertificateTemplate + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { certificateTemplate, projectId } = await server.services.sshCertificateTemplate.updateSshCertTemplate({ + ...req.body, + id: req.params.certificateTemplateId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.UPDATE_SSH_CERTIFICATE_TEMPLATE, + metadata: { + status: certificateTemplate.status as SshCertTemplateStatus, + certificateTemplateId: certificateTemplate.id, + sshCaId: certificateTemplate.sshCaId, + name: certificateTemplate.name, + ttl: certificateTemplate.ttl, + maxTTL: certificateTemplate.maxTTL, + allowedUsers: certificateTemplate.allowedUsers, + allowedHosts: certificateTemplate.allowedHosts, + allowUserCertificates: certificateTemplate.allowUserCertificates, + allowHostCertificates: certificateTemplate.allowHostCertificates, + allowCustomKeyIds: certificateTemplate.allowCustomKeyIds + } + } + }); + + return certificateTemplate; + } + }); + + server.route({ + method: "DELETE", + url: "/:certificateTemplateId", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateTemplates], + params: z.object({ + certificateTemplateId: z.string().describe(SSH_CERTIFICATE_TEMPLATES.DELETE.certificateTemplateId) + }), + response: { + 200: sanitizedSshCertificateTemplate + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const certificateTemplate = await server.services.sshCertificateTemplate.deleteSshCertTemplate({ + id: req.params.certificateTemplateId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: certificateTemplate.projectId, + event: { + type: EventType.DELETE_SSH_CERTIFICATE_TEMPLATE, + metadata: { + certificateTemplateId: certificateTemplate.id + } + } + }); + + return certificateTemplate; + } + }); +}; diff --git a/backend/src/ee/routes/v1/ssh-host-router.ts b/backend/src/ee/routes/v1/ssh-host-router.ts new file mode 100644 index 000000000..1dab5dd2f --- /dev/null +++ b/backend/src/ee/routes/v1/ssh-host-router.ts @@ -0,0 +1,444 @@ +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types"; +import { loginMappingSchema, sanitizedSshHost } from "@app/ee/services/ssh-host/ssh-host-schema"; +import { isValidHostname } from "@app/ee/services/ssh-host/ssh-host-validators"; +import { SSH_HOSTS } from "@app/lib/api-docs"; +import { ms } from "@app/lib/ms"; +import { publicSshCaLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; + +export const registerSshHostRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: z.array( + sanitizedSshHost.extend({ + loginMappings: z.array(loginMappingSchema) + }) + ) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const hosts = await server.services.sshHost.listSshHosts({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + return hosts; + } + }); + + server.route({ + method: "GET", + url: "/:sshHostId", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + sshHostId: z.string().describe(SSH_HOSTS.GET.sshHostId) + }), + response: { + 200: sanitizedSshHost.extend({ + loginMappings: z.array(loginMappingSchema) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const host = await server.services.sshHost.getSshHost({ + sshHostId: req.params.sshHostId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: host.projectId, + event: { + type: EventType.GET_SSH_HOST, + metadata: { + sshHostId: host.id, + hostname: host.hostname + } + } + }); + + return host; + } + }); + + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Add an SSH Host", + body: z.object({ + projectId: z.string().describe(SSH_HOSTS.CREATE.projectId), + hostname: z + .string() + .min(1) + .refine((v) => isValidHostname(v), { + message: "Hostname must be a valid hostname" + }) + .describe(SSH_HOSTS.CREATE.hostname), + userCertTtl: z + .string() + .refine((val) => ms(val) > 0, "TTL must be a positive number") + .default("8h") + .describe(SSH_HOSTS.CREATE.userCertTtl), + hostCertTtl: z + .string() + .refine((val) => ms(val) > 0, "TTL must be a positive number") + .default("1y") + .describe(SSH_HOSTS.CREATE.hostCertTtl), + loginMappings: z.array(loginMappingSchema).default([]).describe(SSH_HOSTS.CREATE.loginMappings), + userSshCaId: z.string().describe(SSH_HOSTS.CREATE.userSshCaId).optional(), + hostSshCaId: z.string().describe(SSH_HOSTS.CREATE.hostSshCaId).optional() + }), + response: { + 200: sanitizedSshHost.extend({ + loginMappings: z.array(loginMappingSchema) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const host = await server.services.sshHost.createSshHost({ + ...req.body, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: host.projectId, + event: { + type: EventType.CREATE_SSH_HOST, + metadata: { + sshHostId: host.id, + hostname: host.hostname, + userCertTtl: host.userCertTtl, + hostCertTtl: host.hostCertTtl, + loginMappings: host.loginMappings, + userSshCaId: host.userSshCaId, + hostSshCaId: host.hostSshCaId + } + } + }); + + return host; + } + }); + + server.route({ + method: "PATCH", + url: "/:sshHostId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Update SSH Host", + params: z.object({ + sshHostId: z.string().trim().describe(SSH_HOSTS.UPDATE.sshHostId) + }), + body: z.object({ + hostname: z + .string() + .min(1) + .refine((v) => isValidHostname(v), { + message: "Hostname must be a valid hostname" + }) + .optional() + .describe(SSH_HOSTS.UPDATE.hostname), + userCertTtl: z + .string() + .refine((val) => ms(val) > 0, "TTL must be a positive number") + .optional() + .describe(SSH_HOSTS.UPDATE.userCertTtl), + hostCertTtl: z + .string() + .refine((val) => ms(val) > 0, "TTL must be a positive number") + .optional() + .describe(SSH_HOSTS.UPDATE.hostCertTtl), + loginMappings: z.array(loginMappingSchema).optional().describe(SSH_HOSTS.UPDATE.loginMappings) + }), + response: { + 200: sanitizedSshHost.extend({ + loginMappings: z.array(loginMappingSchema) + }) + } + }, + handler: async (req) => { + const host = await server.services.sshHost.updateSshHost({ + sshHostId: req.params.sshHostId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: host.projectId, + event: { + type: EventType.UPDATE_SSH_HOST, + metadata: { + sshHostId: host.id, + hostname: host.hostname, + userCertTtl: host.userCertTtl, + hostCertTtl: host.hostCertTtl, + loginMappings: host.loginMappings, + userSshCaId: host.userSshCaId, + hostSshCaId: host.hostSshCaId + } + } + }); + + return host; + } + }); + + server.route({ + method: "DELETE", + url: "/:sshHostId", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + sshHostId: z.string().describe(SSH_HOSTS.DELETE.sshHostId) + }), + response: { + 200: sanitizedSshHost.extend({ + loginMappings: z.array(loginMappingSchema) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const host = await server.services.sshHost.deleteSshHost({ + sshHostId: req.params.sshHostId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: host.projectId, + event: { + type: EventType.DELETE_SSH_HOST, + metadata: { + sshHostId: host.id, + hostname: host.hostname + } + } + }); + + return host; + } + }); + + server.route({ + method: "POST", + url: "/:sshHostId/issue-user-cert", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + description: "Issue SSH certificate for user", + params: z.object({ + sshHostId: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.sshHostId) + }), + body: z.object({ + loginUser: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.loginUser) + }), + response: { + 200: z.object({ + serialNumber: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.serialNumber), + signedKey: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.signedKey), + privateKey: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.privateKey), + publicKey: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.publicKey), + keyAlgorithm: z.nativeEnum(SshCertKeyAlgorithm).describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.keyAlgorithm) + }) + } + }, + handler: async (req) => { + const { serialNumber, signedPublicKey, privateKey, publicKey, keyAlgorithm, host, principals } = + await server.services.sshHost.issueSshHostUserCert({ + sshHostId: req.params.sshHostId, + loginUser: req.body.loginUser, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.ISSUE_SSH_HOST_USER_CERT, + metadata: { + sshHostId: req.params.sshHostId, + hostname: host.hostname, + loginUser: req.body.loginUser, + principals, + ttl: host.userCertTtl + } + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.IssueSshHostUserCert, + distinctId: getTelemetryDistinctId(req), + properties: { + sshHostId: req.params.sshHostId, + hostname: host.hostname, + principals, + ...req.auditLogInfo + } + }); + + return { + serialNumber, + signedKey: signedPublicKey, + privateKey, + publicKey, + keyAlgorithm + }; + } + }); + + server.route({ + method: "POST", + url: "/:sshHostId/issue-host-cert", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Issue SSH certificate for host", + params: z.object({ + sshHostId: z.string().describe(SSH_HOSTS.ISSUE_HOST_CERT.sshHostId) + }), + body: z.object({ + publicKey: z.string().describe(SSH_HOSTS.ISSUE_HOST_CERT.publicKey) + }), + response: { + 200: z.object({ + serialNumber: z.string().describe(SSH_HOSTS.ISSUE_HOST_CERT.serialNumber), + signedKey: z.string().describe(SSH_HOSTS.ISSUE_HOST_CERT.signedKey) + }) + } + }, + handler: async (req) => { + const { host, principals, serialNumber, signedPublicKey } = await server.services.sshHost.issueSshHostHostCert({ + sshHostId: req.params.sshHostId, + publicKey: req.body.publicKey, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.ISSUE_SSH_HOST_HOST_CERT, + metadata: { + sshHostId: req.params.sshHostId, + hostname: host.hostname, + principals, + serialNumber, + ttl: host.hostCertTtl + } + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.IssueSshHostHostCert, + distinctId: getTelemetryDistinctId(req), + properties: { + sshHostId: req.params.sshHostId, + hostname: host.hostname, + principals, + ...req.auditLogInfo + } + }); + + return { + serialNumber, + signedKey: signedPublicKey + }; + } + }); + + server.route({ + method: "GET", + url: "/:sshHostId/user-ca-public-key", + config: { + rateLimit: publicSshCaLimit + }, + schema: { + description: "Get public key of the user SSH CA linked to the host", + params: z.object({ + sshHostId: z.string().trim().describe(SSH_HOSTS.GET_USER_CA_PUBLIC_KEY.sshHostId) + }), + response: { + 200: z.string().describe(SSH_HOSTS.GET_USER_CA_PUBLIC_KEY.publicKey) + } + }, + handler: async (req) => { + const publicKey = await server.services.sshHost.getSshHostUserCaPk(req.params.sshHostId); + return publicKey; + } + }); + + server.route({ + method: "GET", + url: "/:sshHostId/host-ca-public-key", + config: { + rateLimit: publicSshCaLimit + }, + schema: { + description: "Get public key of the host SSH CA linked to the host", + params: z.object({ + sshHostId: z.string().trim().describe(SSH_HOSTS.GET_HOST_CA_PUBLIC_KEY.sshHostId) + }), + response: { + 200: z.string().describe(SSH_HOSTS.GET_HOST_CA_PUBLIC_KEY.publicKey) + } + }, + handler: async (req) => { + const publicKey = await server.services.sshHost.getSshHostHostCaPk(req.params.sshHostId); + return publicKey; + } + }); +}; diff --git a/backend/src/ee/routes/v1/user-additional-privilege-router.ts b/backend/src/ee/routes/v1/user-additional-privilege-router.ts index e58a6335b..df8512ada 100644 --- a/backend/src/ee/routes/v1/user-additional-privilege-router.ts +++ b/backend/src/ee/routes/v1/user-additional-privilege-router.ts @@ -1,14 +1,16 @@ import slugify from "@sindresorhus/slugify"; -import ms from "ms"; import { z } from "zod"; +import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types"; import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; +import { ms } from "@app/lib/ms"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { SanitizedUserProjectAdditionalPrivilegeSchema } from "@app/server/routes/santizedSchemas/user-additional-privilege"; +import { SanitizedUserProjectAdditionalPrivilegeSchema } from "@app/server/routes/sanitizedSchema/user-additional-privilege"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => { @@ -21,18 +23,10 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr schema: { body: z.object({ projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId), - slug: z - .string() - .min(1) - .max(60) - .trim() - .refine((v) => v.toLowerCase() === v, "Slug must be lowercase") - .refine((v) => slugify(v) === v, { - message: "Slug must be a valid slug" - }) - .optional() - .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug), - permissions: ProjectPermissionV2Schema.array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions), + slug: slugSchema({ min: 1, max: 60 }).optional().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug), + permissions: ProjectPermissionV2Schema.array() + .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions) + .refine(checkForInvalidPermissionCombination), type: z.discriminatedUnion("isTemporary", [ z.object({ isTemporary: z.literal(false) @@ -87,18 +81,11 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr }), body: z .object({ - slug: z - .string() - .max(60) - .trim() - .refine((v) => v.toLowerCase() === v, "Slug must be lowercase") - .refine((v) => slugify(v) === v, { - message: "Slug must be a valid slug" - }) - .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug), + slug: slugSchema({ min: 1, max: 60 }).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug), permissions: ProjectPermissionV2Schema.array() .optional() - .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions), + .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions) + .refine(checkForInvalidPermissionCombination), type: z.discriminatedUnion("isTemporary", [ z.object({ isTemporary: z.literal(false).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary) }), z.object({ diff --git a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts index 5df03f68d..a6d4459e4 100644 --- a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts +++ b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts @@ -1,14 +1,16 @@ import slugify from "@sindresorhus/slugify"; -import ms from "ms"; import { z } from "zod"; import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types"; +import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; -import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs"; +import { ApiDocsTags, IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs"; +import { ms } from "@app/lib/ms"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { SanitizedIdentityPrivilegeSchema } from "@app/server/routes/santizedSchemas/identitiy-additional-privilege"; +import { SanitizedIdentityPrivilegeSchema } from "@app/server/routes/sanitizedSchema/identitiy-additional-privilege"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => { @@ -19,6 +21,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.IdentitySpecificPrivilegesV2], description: "Add an additional privilege for identity.", security: [ { @@ -28,18 +32,10 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F body: z.object({ identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.identityId), projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.projectId), - slug: z - .string() - .min(1) - .max(60) - .trim() - .refine((val) => val.toLowerCase() === val, "Must be lowercase") - .refine((v) => slugify(v) === v, { - message: "Slug must be a valid slug" - }) - .optional() - .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug), - permissions: ProjectPermissionV2Schema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission), + slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug), + permissions: ProjectPermissionV2Schema.array() + .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission) + .refine(checkForInvalidPermissionCombination), type: z.discriminatedUnion("isTemporary", [ z.object({ isTemporary: z.literal(false) @@ -90,6 +86,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.IdentitySpecificPrivilegesV2], description: "Update a specific identity privilege.", security: [ { @@ -100,19 +98,11 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F id: z.string().trim().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.id) }), body: z.object({ - slug: z - .string() - .min(1) - .max(60) - .trim() - .refine((val) => val.toLowerCase() === val, "Must be lowercase") - .refine((v) => slugify(v) === v, { - message: "Slug must be a valid slug" - }) - .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug), + slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug), permissions: ProjectPermissionV2Schema.array() .optional() - .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission), + .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission) + .refine(checkForInvalidPermissionCombination), type: z.discriminatedUnion("isTemporary", [ z.object({ isTemporary: z.literal(false).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.isTemporary) }), z.object({ @@ -162,6 +152,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.IdentitySpecificPrivilegesV2], description: "Delete the specified identity privilege.", security: [ { @@ -197,6 +189,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.IdentitySpecificPrivilegesV2], description: "Retrieve details of a specific privilege by id.", security: [ { @@ -232,6 +226,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.IdentitySpecificPrivilegesV2], description: "Retrieve details of a specific privilege by slug.", security: [ { @@ -272,6 +268,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.IdentitySpecificPrivilegesV2], description: "List privileges for the specified identity by project.", security: [ { diff --git a/backend/src/ee/routes/v2/index.ts b/backend/src/ee/routes/v2/index.ts index bede5a1cf..70e5005a4 100644 --- a/backend/src/ee/routes/v2/index.ts +++ b/backend/src/ee/routes/v2/index.ts @@ -1,3 +1,8 @@ +import { + registerSecretRotationV2Router, + SECRET_ROTATION_REGISTER_ROUTER_MAP +} from "@app/ee/routes/v2/secret-rotation-v2-routers"; + import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router"; import { registerProjectRoleRouter } from "./project-role-router"; @@ -13,4 +18,17 @@ export const registerV2EERoutes = async (server: FastifyZodProvider) => { await server.register(registerIdentityProjectAdditionalPrivilegeRouter, { prefix: "/identity-project-additional-privilege" }); + + await server.register( + async (secretRotationV2Router) => { + // register generic secret rotation endpoints + await secretRotationV2Router.register(registerSecretRotationV2Router); + + // register service specific secret rotation endpoints (secret-rotations/postgres-credentials, etc.) + for await (const [type, router] of Object.entries(SECRET_ROTATION_REGISTER_ROUTER_MAP)) { + await secretRotationV2Router.register(router, { prefix: `/${type}` }); + } + }, + { prefix: "/secret-rotations" } + ); }; diff --git a/backend/src/ee/routes/v2/project-role-router.ts b/backend/src/ee/routes/v2/project-role-router.ts index 70511ce87..538929316 100644 --- a/backend/src/ee/routes/v2/project-role-router.ts +++ b/backend/src/ee/routes/v2/project-role-router.ts @@ -1,11 +1,12 @@ import { packRules } from "@casl/ability/extra"; -import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas"; +import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; -import { PROJECT_ROLE } from "@app/lib/api-docs"; +import { ApiDocsTags, PROJECT_ROLE } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -19,6 +20,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectRoles], description: "Create a project role", security: [ { @@ -29,22 +32,17 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { projectId: z.string().trim().describe(PROJECT_ROLE.CREATE.projectId) }), body: z.object({ - slug: z - .string() - .toLowerCase() - .trim() - .min(1) + slug: slugSchema({ min: 1, max: 64 }) .refine( (val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole), "Please choose a different slug, the slug you have entered is reserved" ) - .refine((v) => slugify(v) === v, { - message: "Slug must be a valid" - }) .describe(PROJECT_ROLE.CREATE.slug), name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name), - description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description), - permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.CREATE.permissions) + description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description), + permissions: ProjectPermissionV2Schema.array() + .describe(PROJECT_ROLE.CREATE.permissions) + .refine(checkForInvalidPermissionCombination) }), response: { 200: z.object({ @@ -79,6 +77,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectRoles], description: "Update a project role", security: [ { @@ -90,24 +90,19 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId) }), body: z.object({ - slug: z - .string() - .toLowerCase() - .trim() - .optional() - .describe(PROJECT_ROLE.UPDATE.slug) + slug: slugSchema({ min: 1, max: 64 }) .refine( - (val) => - typeof val === "undefined" || - !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole), + (val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole), "Please choose a different slug, the slug you have entered is reserved" ) - .refine((val) => typeof val === "undefined" || slugify(val) === val, { - message: "Slug must be a valid" - }), + .optional() + .describe(PROJECT_ROLE.UPDATE.slug), name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name), - description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description), - permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional() + description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description), + permissions: ProjectPermissionV2Schema.array() + .describe(PROJECT_ROLE.UPDATE.permissions) + .optional() + .superRefine(checkForInvalidPermissionCombination) }), response: { 200: z.object({ @@ -139,6 +134,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectRoles], description: "Delete a project role", security: [ { @@ -175,6 +172,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectRoles], description: "List project role", security: [ { @@ -213,6 +212,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectRoles], params: z.object({ projectId: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.projectId), roleSlug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.roleSlug) diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/auth0-client-secret-rotation-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/auth0-client-secret-rotation-router.ts new file mode 100644 index 000000000..6bcf1ea5e --- /dev/null +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/auth0-client-secret-rotation-router.ts @@ -0,0 +1,19 @@ +import { + Auth0ClientSecretRotationGeneratedCredentialsSchema, + Auth0ClientSecretRotationSchema, + CreateAuth0ClientSecretRotationSchema, + UpdateAuth0ClientSecretRotationSchema +} from "@app/ee/services/secret-rotation-v2/auth0-client-secret"; +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; + +import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints"; + +export const registerAuth0ClientSecretRotationRouter = async (server: FastifyZodProvider) => + registerSecretRotationEndpoints({ + type: SecretRotation.Auth0ClientSecret, + server, + responseSchema: Auth0ClientSecretRotationSchema, + createSchema: CreateAuth0ClientSecretRotationSchema, + updateSchema: UpdateAuth0ClientSecretRotationSchema, + generatedCredentialsSchema: Auth0ClientSecretRotationGeneratedCredentialsSchema + }); diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts new file mode 100644 index 000000000..1dacf1bd2 --- /dev/null +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts @@ -0,0 +1,16 @@ +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; + +import { registerAuth0ClientSecretRotationRouter } from "./auth0-client-secret-rotation-router"; +import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router"; +import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router"; + +export * from "./secret-rotation-v2-router"; + +export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record< + SecretRotation, + (server: FastifyZodProvider) => Promise +> = { + [SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter, + [SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter, + [SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter +}; diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/mssql-credentials-rotation-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/mssql-credentials-rotation-router.ts new file mode 100644 index 000000000..4fea8869b --- /dev/null +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/mssql-credentials-rotation-router.ts @@ -0,0 +1,19 @@ +import { + CreateMsSqlCredentialsRotationSchema, + MsSqlCredentialsRotationSchema, + UpdateMsSqlCredentialsRotationSchema +} from "@app/ee/services/secret-rotation-v2/mssql-credentials"; +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { SqlCredentialsRotationGeneratedCredentialsSchema } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials"; + +import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints"; + +export const registerMsSqlCredentialsRotationRouter = async (server: FastifyZodProvider) => + registerSecretRotationEndpoints({ + type: SecretRotation.MsSqlCredentials, + server, + responseSchema: MsSqlCredentialsRotationSchema, + createSchema: CreateMsSqlCredentialsRotationSchema, + updateSchema: UpdateMsSqlCredentialsRotationSchema, + generatedCredentialsSchema: SqlCredentialsRotationGeneratedCredentialsSchema + }); diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/postgres-credentials-rotation-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/postgres-credentials-rotation-router.ts new file mode 100644 index 000000000..ab5ef5768 --- /dev/null +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/postgres-credentials-rotation-router.ts @@ -0,0 +1,19 @@ +import { + CreatePostgresCredentialsRotationSchema, + PostgresCredentialsRotationSchema, + UpdatePostgresCredentialsRotationSchema +} from "@app/ee/services/secret-rotation-v2/postgres-credentials"; +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { SqlCredentialsRotationGeneratedCredentialsSchema } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials"; + +import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints"; + +export const registerPostgresCredentialsRotationRouter = async (server: FastifyZodProvider) => + registerSecretRotationEndpoints({ + type: SecretRotation.PostgresCredentials, + server, + responseSchema: PostgresCredentialsRotationSchema, + createSchema: CreatePostgresCredentialsRotationSchema, + updateSchema: UpdatePostgresCredentialsRotationSchema, + generatedCredentialsSchema: SqlCredentialsRotationGeneratedCredentialsSchema + }); diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-endpoints.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-endpoints.ts new file mode 100644 index 000000000..17fe14dbf --- /dev/null +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-endpoints.ts @@ -0,0 +1,445 @@ +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { SECRET_ROTATION_NAME_MAP } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps"; +import { + TRotateAtUtc, + TSecretRotationV2, + TSecretRotationV2GeneratedCredentials, + TSecretRotationV2Input +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { ApiDocsTags, SecretRotations } from "@app/lib/api-docs"; +import { startsWithVowel } from "@app/lib/fn"; +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 registerSecretRotationEndpoints = < + T extends TSecretRotationV2, + I extends TSecretRotationV2Input, + C extends TSecretRotationV2GeneratedCredentials +>({ + server, + type, + createSchema, + updateSchema, + responseSchema, + generatedCredentialsSchema +}: { + type: SecretRotation; + server: FastifyZodProvider; + createSchema: z.ZodType<{ + name: string; + environment: string; + secretPath: string; + projectId: string; + connectionId: string; + parameters: I["parameters"]; + secretsMapping: I["secretsMapping"]; + description?: string | null; + isAutoRotationEnabled?: boolean; + rotationInterval: number; + rotateAtUtc?: TRotateAtUtc; + }>; + updateSchema: z.ZodType<{ + connectionId?: string; + name?: string; + environment?: string; + secretPath?: string; + parameters?: I["parameters"]; + secretsMapping?: I["secretsMapping"]; + description?: string | null; + isAutoRotationEnabled?: boolean; + rotationInterval?: number; + rotateAtUtc?: TRotateAtUtc; + }>; + responseSchema: z.ZodTypeAny; + generatedCredentialsSchema: z.ZodTypeAny; +}) => { + const rotationType = SECRET_ROTATION_NAME_MAP[type]; + + server.route({ + method: "GET", + url: `/`, + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretRotations], + description: `List the ${rotationType} Rotations for the specified project.`, + querystring: z.object({ + projectId: z.string().trim().min(1, "Project ID required").describe(SecretRotations.LIST(type).projectId) + }), + response: { + 200: z.object({ secretRotations: responseSchema.array() }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { + query: { projectId } + } = req; + + const secretRotations = (await server.services.secretRotationV2.listSecretRotationsByProjectId( + { projectId, type }, + req.permission + )) as T[]; + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.GET_SECRET_ROTATIONS, + metadata: { + type, + count: secretRotations.length, + rotationIds: secretRotations.map((rotation) => rotation.id) + } + } + }); + + return { secretRotations }; + } + }); + + server.route({ + method: "GET", + url: "/:rotationId", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretRotations], + description: `Get the specified ${rotationType} Rotation by ID.`, + params: z.object({ + rotationId: z.string().uuid().describe(SecretRotations.GET_BY_ID(type).rotationId) + }), + response: { + 200: z.object({ secretRotation: responseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { rotationId } = req.params; + + const secretRotation = (await server.services.secretRotationV2.findSecretRotationById( + { rotationId, type }, + req.permission + )) as T; + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: secretRotation.projectId, + event: { + type: EventType.GET_SECRET_ROTATION, + metadata: { + rotationId, + type, + secretPath: secretRotation.folder.path, + environment: secretRotation.environment.slug + } + } + }); + + return { secretRotation }; + } + }); + + server.route({ + method: "GET", + url: `/rotation-name/:rotationName`, + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretRotations], + description: `Get the specified ${rotationType} Rotation by name, secret path, environment and project ID.`, + params: z.object({ + rotationName: z + .string() + .trim() + .min(1, "Rotation name required") + .describe(SecretRotations.GET_BY_NAME(type).rotationName) + }), + querystring: z.object({ + projectId: z + .string() + .trim() + .min(1, "Project ID required") + .describe(SecretRotations.GET_BY_NAME(type).projectId), + secretPath: z + .string() + .trim() + .min(1, "Secret path required") + .describe(SecretRotations.GET_BY_NAME(type).secretPath), + environment: z + .string() + .trim() + .min(1, "Environment required") + .describe(SecretRotations.GET_BY_NAME(type).environment) + }), + response: { + 200: z.object({ secretRotation: responseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { rotationName } = req.params; + const { projectId, secretPath, environment } = req.query; + + const secretRotation = (await server.services.secretRotationV2.findSecretRotationByName( + { rotationName, projectId, type, secretPath, environment }, + req.permission + )) as T; + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.GET_SECRET_ROTATION, + metadata: { + rotationId: secretRotation.id, + type, + secretPath, + environment + } + } + }); + + return { secretRotation }; + } + }); + + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretRotations], + description: `Create ${ + startsWithVowel(rotationType) ? "an" : "a" + } ${rotationType} Rotation for the specified project.`, + body: createSchema, + response: { + 200: z.object({ secretRotation: responseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const secretRotation = (await server.services.secretRotationV2.createSecretRotation( + { ...req.body, type }, + req.permission + )) as T; + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: secretRotation.projectId, + event: { + type: EventType.CREATE_SECRET_ROTATION, + metadata: { + rotationId: secretRotation.id, + type, + ...req.body + } + } + }); + + return { secretRotation }; + } + }); + + server.route({ + method: "PATCH", + url: "/:rotationId", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretRotations], + description: `Update the specified ${rotationType} Rotation.`, + params: z.object({ + rotationId: z.string().uuid().describe(SecretRotations.UPDATE(type).rotationId) + }), + body: updateSchema, + response: { + 200: z.object({ secretRotation: responseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { rotationId } = req.params; + + const secretRotation = (await server.services.secretRotationV2.updateSecretRotation( + { ...req.body, rotationId, type }, + req.permission + )) as T; + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: secretRotation.projectId, + event: { + type: EventType.UPDATE_SECRET_ROTATION, + metadata: { + rotationId, + type, + ...req.body + } + } + }); + + return { secretRotation }; + } + }); + + server.route({ + method: "DELETE", + url: `/:rotationId`, + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretRotations], + description: `Delete the specified ${rotationType} Rotation.`, + params: z.object({ + rotationId: z.string().uuid().describe(SecretRotations.DELETE(type).rotationId) + }), + querystring: z.object({ + deleteSecrets: z + .enum(["true", "false"]) + .transform((value) => value === "true") + .describe(SecretRotations.DELETE(type).deleteSecrets), + revokeGeneratedCredentials: z + .enum(["true", "false"]) + .transform((value) => value === "true") + .describe(SecretRotations.DELETE(type).revokeGeneratedCredentials) + }), + response: { + 200: z.object({ secretRotation: responseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { rotationId } = req.params; + const { deleteSecrets, revokeGeneratedCredentials } = req.query; + + const secretRotation = (await server.services.secretRotationV2.deleteSecretRotation( + { type, rotationId, deleteSecrets, revokeGeneratedCredentials }, + req.permission + )) as T; + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: secretRotation.projectId, + event: { + type: EventType.DELETE_SECRET_ROTATION, + metadata: { + type, + rotationId, + deleteSecrets, + revokeGeneratedCredentials + } + } + }); + + return { secretRotation }; + } + }); + + server.route({ + method: "GET", + url: "/:rotationId/generated-credentials", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretRotations], + description: `Get the generated credentials for the specified ${rotationType} Rotation.`, + params: z.object({ + rotationId: z.string().uuid().describe(SecretRotations.GET_GENERATED_CREDENTIALS_BY_ID(type).rotationId) + }), + response: { + 200: z.object({ + generatedCredentials: generatedCredentialsSchema, + activeIndex: z.number(), + rotationId: z.string().uuid(), + type: z.literal(type) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { rotationId } = req.params; + + const { + generatedCredentials, + secretRotation: { activeIndex, projectId, folder, environment } + } = await server.services.secretRotationV2.findSecretRotationGeneratedCredentialsById( + { + rotationId, + type + }, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.GET_SECRET_ROTATION_GENERATED_CREDENTIALS, + metadata: { + type, + rotationId, + secretPath: folder.path, + environment: environment.slug + } + } + }); + + return { generatedCredentials: generatedCredentials as C, activeIndex, rotationId, type }; + } + }); + + server.route({ + method: "POST", + url: "/:rotationId/rotate-secrets", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretRotations], + description: `Rotate the generated credentials for the specified ${rotationType} Rotation.`, + params: z.object({ + rotationId: z.string().uuid().describe(SecretRotations.ROTATE(type).rotationId) + }), + response: { + 200: z.object({ secretRotation: responseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { rotationId } = req.params; + + const secretRotation = (await server.services.secretRotationV2.rotateSecretRotation( + { + rotationId, + type, + auditLogInfo: req.auditLogInfo + }, + req.permission + )) as T; + + return { secretRotation }; + } + }); +}; diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts new file mode 100644 index 000000000..c1a2cb69d --- /dev/null +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts @@ -0,0 +1,87 @@ +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { Auth0ClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret"; +import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials"; +import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials"; +import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema"; +import { ApiDocsTags, SecretRotations } from "@app/lib/api-docs"; +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [ + PostgresCredentialsRotationListItemSchema, + MsSqlCredentialsRotationListItemSchema, + Auth0ClientSecretRotationListItemSchema +]); + +export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/options", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretRotations], + description: "List the available Secret Rotation Options.", + response: { + 200: z.object({ + secretRotationOptions: SecretRotationV2OptionsSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: () => { + const secretRotationOptions = server.services.secretRotationV2.listSecretRotationOptions(); + return { secretRotationOptions }; + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretRotations], + description: "List all the Secret Rotations for the specified project.", + querystring: z.object({ + projectId: z.string().trim().min(1, "Project ID required").describe(SecretRotations.LIST().projectId) + }), + response: { + 200: z.object({ secretRotations: SecretRotationV2Schema.array() }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { + query: { projectId }, + permission + } = req; + + const secretRotations = await server.services.secretRotationV2.listSecretRotationsByProjectId( + { projectId }, + permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.GET_SECRET_ROTATIONS, + metadata: { + rotationIds: secretRotations.map((sync) => sync.id), + count: secretRotations.length + } + } + }); + + return { secretRotations }; + } + }); +}; diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts index 220701410..e14451498 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts @@ -139,5 +139,10 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => { } }; - return { ...accessApprovalPolicyOrm, find, findById }; + const softDeleteById = async (policyId: string, tx?: Knex) => { + const softDeletedPolicy = await accessApprovalPolicyOrm.updateById(policyId, { deletedAt: new Date() }, tx); + return softDeletedPolicy; + }; + + return { ...accessApprovalPolicyOrm, find, findById, softDeleteById }; }; diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index ee7cf2572..6b5014acc 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; @@ -8,7 +9,11 @@ import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TUserDALFactory } from "@app/services/user/user-dal"; +import { TAccessApprovalRequestDALFactory } from "../access-approval-request/access-approval-request-dal"; +import { TAccessApprovalRequestReviewerDALFactory } from "../access-approval-request/access-approval-request-reviewer-dal"; +import { ApprovalStatus } from "../access-approval-request/access-approval-request-types"; import { TGroupDALFactory } from "../group/group-dal"; +import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal"; import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal"; import { @@ -21,7 +26,7 @@ import { TUpdateAccessApprovalPolicy } from "./access-approval-policy-types"; -type TSecretApprovalPolicyServiceFactoryDep = { +type TAccessApprovalPolicyServiceFactoryDep = { projectDAL: TProjectDALFactory; permissionService: Pick; accessApprovalPolicyDAL: TAccessApprovalPolicyDALFactory; @@ -30,6 +35,9 @@ type TSecretApprovalPolicyServiceFactoryDep = { projectMembershipDAL: Pick; groupDAL: TGroupDALFactory; userDAL: Pick; + accessApprovalRequestDAL: Pick; + additionalPrivilegeDAL: Pick; + accessApprovalRequestReviewerDAL: Pick; }; export type TAccessApprovalPolicyServiceFactory = ReturnType; @@ -41,8 +49,11 @@ export const accessApprovalPolicyServiceFactory = ({ permissionService, projectEnvDAL, projectDAL, - userDAL -}: TSecretApprovalPolicyServiceFactoryDep) => { + userDAL, + accessApprovalRequestDAL, + additionalPrivilegeDAL, + accessApprovalRequestReviewerDAL +}: TAccessApprovalPolicyServiceFactoryDep) => { const createAccessApprovalPolicy = async ({ name, actor, @@ -54,7 +65,8 @@ export const accessApprovalPolicyServiceFactory = ({ approvers, projectSlug, environment, - enforcementLevel + enforcementLevel, + allowedSelfApprovals }: TCreateAccessApprovalPolicy) => { const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); @@ -76,13 +88,15 @@ export const accessApprovalPolicyServiceFactory = ({ if (!groupApprovers && approvals > userApprovers.length + userApproverNames.length) throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - project.id, + projectId: project.id, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval @@ -140,7 +154,8 @@ export const accessApprovalPolicyServiceFactory = ({ approvals, secretPath, name, - enforcementLevel + enforcementLevel, + allowedSelfApprovals }, tx ); @@ -180,16 +195,16 @@ export const accessApprovalPolicyServiceFactory = ({ if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); // Anyone in the project should be able to get the policies. - /* const { permission } = */ await permissionService.getProjectPermission( + await permissionService.getProjectPermission({ actor, actorId, - project.id, + projectId: project.id, actorAuthMethod, - actorOrgId - ); - // ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); - const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id }); + const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id, deletedAt: null }); return accessApprovalPolicies; }; @@ -203,7 +218,8 @@ export const accessApprovalPolicyServiceFactory = ({ actorOrgId, actorAuthMethod, approvals, - enforcementLevel + enforcementLevel, + allowedSelfApprovals }: TUpdateAccessApprovalPolicy) => { const groupApprovers = approvers .filter((approver) => approver.type === ApproverType.Group) @@ -231,13 +247,14 @@ export const accessApprovalPolicyServiceFactory = ({ if (!accessApprovalPolicy) { throw new NotFoundError({ message: `Secret approval policy with ID '${policyId}' not found` }); } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - accessApprovalPolicy.projectId, + projectId: accessApprovalPolicy.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); @@ -248,7 +265,8 @@ export const accessApprovalPolicyServiceFactory = ({ approvals, secretPath, name, - enforcementLevel + enforcementLevel, + allowedSelfApprovals }, tx ); @@ -314,19 +332,42 @@ export const accessApprovalPolicyServiceFactory = ({ const policy = await accessApprovalPolicyDAL.findById(policyId); if (!policy) throw new NotFoundError({ message: `Secret approval policy with ID '${policyId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - policy.projectId, + projectId: policy.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval ); - await accessApprovalPolicyDAL.deleteById(policyId); + await accessApprovalPolicyDAL.transaction(async (tx) => { + await accessApprovalPolicyDAL.softDeleteById(policyId, tx); + const allAccessApprovalRequests = await accessApprovalRequestDAL.find({ policyId }); + + if (allAccessApprovalRequests.length) { + const accessApprovalRequestsIds = allAccessApprovalRequests.map((request) => request.id); + + const privilegeIdsArray = allAccessApprovalRequests + .map((request) => request.privilegeId) + .filter((id): id is string => id != null); + + if (privilegeIdsArray.length) { + await additionalPrivilegeDAL.delete({ $in: { id: privilegeIdsArray } }, tx); + } + + await accessApprovalRequestReviewerDAL.update( + { $in: { id: accessApprovalRequestsIds }, status: ApprovalStatus.PENDING }, + { status: ApprovalStatus.REJECTED }, + tx + ); + } + }); + return policy; }; @@ -342,13 +383,14 @@ export const accessApprovalPolicyServiceFactory = ({ if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - const { membership } = await permissionService.getProjectPermission( + const { membership } = await permissionService.getProjectPermission({ actor, actorId, - project.id, + projectId: project.id, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); if (!membership) { throw new ForbiddenRequestError({ message: "You are not a member of this project" }); } @@ -356,7 +398,11 @@ export const accessApprovalPolicyServiceFactory = ({ const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug }); if (!environment) throw new NotFoundError({ message: `Environment with slug '${envSlug}' not found` }); - const policies = await accessApprovalPolicyDAL.find({ envId: environment.id, projectId: project.id }); + const policies = await accessApprovalPolicyDAL.find({ + envId: environment.id, + projectId: project.id, + deletedAt: null + }); if (!policies) throw new NotFoundError({ message: `No policies found in environment with slug '${envSlug}'` }); return { count: policies.length }; @@ -377,13 +423,14 @@ export const accessApprovalPolicyServiceFactory = ({ }); } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - policy.projectId, + projectId: policy.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts index a42c89e7a..dde8ffbea 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts @@ -26,6 +26,7 @@ export type TCreateAccessApprovalPolicy = { projectSlug: string; name: string; enforcementLevel: EnforcementLevel; + allowedSelfApprovals: boolean; } & Omit; export type TUpdateAccessApprovalPolicy = { @@ -35,6 +36,7 @@ export type TUpdateAccessApprovalPolicy = { secretPath?: string; name?: string; enforcementLevel?: EnforcementLevel; + allowedSelfApprovals: boolean; } & Omit; export type TDeleteAccessApprovalPolicy = { diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts index 8784d05e2..e2075af0a 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts @@ -61,7 +61,9 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => { db.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"), db.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"), db.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"), - db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId") + db.ref("allowedSelfApprovals").withSchema(TableName.AccessApprovalPolicy).as("policyAllowedSelfApprovals"), + db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId"), + db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt") ) .select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover)) @@ -118,7 +120,9 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => { approvals: doc.policyApprovals, secretPath: doc.policySecretPath, enforcementLevel: doc.policyEnforcementLevel, - envId: doc.policyEnvId + allowedSelfApprovals: doc.policyAllowedSelfApprovals, + envId: doc.policyEnvId, + deletedAt: doc.policyDeletedAt }, requestedByUser: { userId: doc.requestedByUserId, @@ -141,7 +145,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => { } : null, - isApproved: !!doc.privilegeId + isApproved: !!doc.policyDeletedAt || !!doc.privilegeId }), childrenMapper: [ { @@ -252,7 +256,9 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => { tx.ref("slug").withSchema(TableName.Environment).as("environment"), tx.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"), tx.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"), - tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals") + tx.ref("allowedSelfApprovals").withSchema(TableName.AccessApprovalPolicy).as("policyAllowedSelfApprovals"), + tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"), + tx.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt") ); const findById = async (id: string, tx?: Knex) => { @@ -271,7 +277,9 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => { name: el.policyName, approvals: el.policyApprovals, secretPath: el.policySecretPath, - enforcementLevel: el.policyEnforcementLevel + enforcementLevel: el.policyEnforcementLevel, + allowedSelfApprovals: el.policyAllowedSelfApprovals, + deletedAt: el.policyDeletedAt }, requestedByUser: { userId: el.requestedByUserId, @@ -363,6 +371,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => { ) .where(`${TableName.Environment}.projectId`, projectId) + .where(`${TableName.AccessApprovalPolicy}.deletedAt`, null) .select(selectAllTableCols(TableName.AccessApprovalRequest)) .select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus")) .select(db.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId")); diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index 14accff41..3606b4bdc 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -1,9 +1,10 @@ import slugify from "@sindresorhus/slugify"; -import ms from "ms"; +import msFn from "ms"; -import { ProjectMembershipRole } from "@app/db/schemas"; +import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { ms } from "@app/lib/ms"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TProjectDALFactory } from "@app/services/project/project-dal"; @@ -93,20 +94,22 @@ export const accessApprovalRequestServiceFactory = ({ actor, actorOrgId, actorAuthMethod, - projectSlug + projectSlug, + note }: TCreateAccessApprovalRequestDTO) => { const cfg = getConfig(); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); // Anyone can create an access approval request. - const { membership } = await permissionService.getProjectPermission( + const { membership } = await permissionService.getProjectPermission({ actor, actorId, - project.id, + projectId: project.id, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); if (!membership) { throw new ForbiddenRequestError({ message: "You are not a member of this project" }); } @@ -130,6 +133,9 @@ export const accessApprovalRequestServiceFactory = ({ message: `No policy in environment with slug '${environment.slug}' and with secret path '${secretPath}' was found.` }); } + if (policy.deletedAt) { + throw new BadRequestError({ message: "The policy linked to this request has been deleted" }); + } const approverIds: string[] = []; const approverGroupIds: string[] = []; @@ -204,13 +210,14 @@ export const accessApprovalRequestServiceFactory = ({ requestedByUserId: actorId, temporaryRange: temporaryRange || null, permissions: JSON.stringify(requestedPermissions), - isTemporary + isTemporary, + note: note || null }, tx ); const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`; - const approvalUrl = `${cfg.SITE_URL}/project/${project.id}/approval`; + const approvalUrl = `${cfg.SITE_URL}/secret-manager/${project.id}/approval`; await triggerSlackNotification({ projectId: project.id, @@ -227,7 +234,8 @@ export const accessApprovalRequestServiceFactory = ({ secretPath, environment: envSlug, permissions: accessTypes, - approvalUrl + approvalUrl, + note } } }); @@ -242,12 +250,13 @@ export const accessApprovalRequestServiceFactory = ({ requesterEmail: requestedByUser.email, isTemporary, ...(isTemporary && { - expiresIn: ms(ms(temporaryRange || ""), { long: true }) + expiresIn: msFn(ms(temporaryRange || ""), { long: true }) }), secretPath, environment: envSlug, permissions: accessTypes, - approvalUrl + approvalUrl, + note }, template: SmtpTemplates.AccessApprovalRequest }); @@ -270,13 +279,14 @@ export const accessApprovalRequestServiceFactory = ({ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - const { membership } = await permissionService.getProjectPermission( + const { membership } = await permissionService.getProjectPermission({ actor, actorId, - project.id, + projectId: project.id, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); if (!membership) { throw new ForbiddenRequestError({ message: "You are not a member of this project" }); } @@ -309,13 +319,25 @@ export const accessApprovalRequestServiceFactory = ({ } const { policy } = accessApprovalRequest; - const { membership, hasRole } = await permissionService.getProjectPermission( + if (policy.deletedAt) { + throw new BadRequestError({ + message: "The policy associated with this access request has been deleted." + }); + } + if (!policy.allowedSelfApprovals && actorId === accessApprovalRequest.requestedByUserId) { + throw new BadRequestError({ + message: "Failed to review access approval request. Users are not authorized to review their own request." + }); + } + + const { membership, hasRole } = await permissionService.getProjectPermission({ actor, actorId, - accessApprovalRequest.projectId, + projectId: accessApprovalRequest.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); if (!membership) { throw new ForbiddenRequestError({ message: "You are not a member of this project" }); @@ -413,13 +435,14 @@ export const accessApprovalRequestServiceFactory = ({ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - const { membership } = await permissionService.getProjectPermission( + const { membership } = await permissionService.getProjectPermission({ actor, actorId, - project.id, + projectId: project.id, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); if (!membership) { throw new ForbiddenRequestError({ message: "You are not a member of this project" }); } diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-types.ts b/backend/src/ee/services/access-approval-request/access-approval-request-types.ts index e11ca58d5..51a5e0ca2 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-types.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-types.ts @@ -24,6 +24,7 @@ export type TCreateAccessApprovalRequestDTO = { permissions: unknown; isTemporary: boolean; temporaryRange?: string; + note?: string; } & Omit; export type TListApprovalRequestsDTO = { diff --git a/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts b/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts index 4f080dac3..c5a562a18 100644 --- a/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts +++ b/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts @@ -45,7 +45,6 @@ export const auditLogStreamServiceFactory = ({ }: TCreateAuditLogStreamDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" }); - const appCfg = getConfig(); const plan = await licenseService.getPlan(actorOrgId); if (!plan.auditLogStreams) { throw new BadRequestError({ @@ -62,9 +61,8 @@ export const auditLogStreamServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); - if (appCfg.isCloud) { - blockLocalAndPrivateIpAddresses(url); - } + const appCfg = getConfig(); + if (appCfg.isCloud) await blockLocalAndPrivateIpAddresses(url); const totalStreams = await auditLogStreamDAL.find({ orgId: actorOrgId }); if (totalStreams.length >= plan.auditLogStreamLimit) { @@ -93,7 +91,7 @@ export const auditLogStreamServiceFactory = ({ } ) .catch((err) => { - throw new Error(`Failed to connect with the source ${(err as Error)?.message}`); + throw new BadRequestError({ message: `Failed to connect with upstream source: ${(err as Error)?.message}` }); }); const encryptedHeaders = headers ? infisicalSymmetricEncypt(JSON.stringify(headers)) : undefined; const logStream = await auditLogStreamDAL.create({ @@ -135,9 +133,8 @@ export const auditLogStreamServiceFactory = ({ const { orgId } = logStream; const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); - const appCfg = getConfig(); - if (url && appCfg.isCloud) blockLocalAndPrivateIpAddresses(url); + if (url && appCfg.isCloud) await blockLocalAndPrivateIpAddresses(url); // testing connection first const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" }; diff --git a/backend/src/ee/services/audit-log/audit-log-dal.ts b/backend/src/ee/services/audit-log/audit-log-dal.ts index b2c80aa0b..ad6b72e33 100644 --- a/backend/src/ee/services/audit-log/audit-log-dal.ts +++ b/backend/src/ee/services/audit-log/audit-log-dal.ts @@ -9,13 +9,14 @@ import { logger } from "@app/lib/logger"; import { QueueName } from "@app/queue"; import { ActorType } from "@app/services/auth/auth-type"; -import { EventType } from "./audit-log-types"; +import { EventType, filterableSecretEvents } from "./audit-log-types"; export type TAuditLogDALFactory = ReturnType; type TFindQuery = { actor?: string; projectId?: string; + environment?: string; orgId?: string; eventType?: string; startDate?: string; @@ -32,6 +33,7 @@ export const auditLogDALFactory = (db: TDbClient) => { { orgId, projectId, + environment, userAgentType, startDate, endDate, @@ -39,11 +41,15 @@ export const auditLogDALFactory = (db: TDbClient) => { offset = 0, actorId, actorType, + secretPath, + secretKey, eventType, eventMetadata }: Omit & { actorId?: string; actorType?: ActorType; + secretPath?: string; + secretKey?: string; eventType?: EventType[]; eventMetadata?: Record; }, @@ -88,6 +94,31 @@ export const auditLogDALFactory = (db: TDbClient) => { }); } + const eventIsSecretType = !eventType?.length || eventType.some((event) => filterableSecretEvents.includes(event)); + // We only want to filter for environment/secretPath/secretKey if the user is either checking for all event types + + // ? Note(daniel): use the `eventMetadata" @> ?::jsonb` approach to properly use our GIN index + if (projectId && eventIsSecretType) { + if (environment || secretPath) { + // Handle both environment and secret path together to only use the GIN index once + void sqlQuery.whereRaw(`"eventMetadata" @> ?::jsonb`, [ + JSON.stringify({ + ...(environment && { environment }), + ...(secretPath && { secretPath }) + }) + ]); + } + + // Handle secret key separately to include the OR condition + if (secretKey) { + void sqlQuery.whereRaw( + `("eventMetadata" @> ?::jsonb + OR "eventMetadata"->'secrets' @> ?::jsonb)`, + [JSON.stringify({ secretKey }), JSON.stringify([{ secretKey }])] + ); + } + } + // Filter by actor type if (actorType) { void sqlQuery.where("actor", actorType); @@ -100,10 +131,10 @@ export const auditLogDALFactory = (db: TDbClient) => { // Filter by date range if (startDate) { - void sqlQuery.where(`${TableName.AuditLog}.createdAt`, ">=", startDate); + void sqlQuery.whereRaw(`"${TableName.AuditLog}"."createdAt" >= ?::timestamptz`, [startDate]); } if (endDate) { - void sqlQuery.where(`${TableName.AuditLog}.createdAt`, "<=", endDate); + void sqlQuery.whereRaw(`"${TableName.AuditLog}"."createdAt" <= ?::timestamptz`, [endDate]); } // we timeout long running queries to prevent DB resource issues (2 minutes) diff --git a/backend/src/ee/services/audit-log/audit-log-queue.ts b/backend/src/ee/services/audit-log/audit-log-queue.ts index 83a2fafa6..e312c3886 100644 --- a/backend/src/ee/services/audit-log/audit-log-queue.ts +++ b/backend/src/ee/services/audit-log/audit-log-queue.ts @@ -1,6 +1,7 @@ import { RawAxiosRequestHeaders } from "axios"; import { SecretKeyEncoding } from "@app/db/schemas"; +import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; @@ -20,27 +21,130 @@ type TAuditLogQueueServiceFactoryDep = { licenseService: Pick; }; -export type TAuditLogQueueServiceFactory = ReturnType; +export type TAuditLogQueueServiceFactory = Awaited>; // keep this timeout 5s it must be fast because else the queue will take time to finish // audit log is a crowded queue thus needs to be fast export const AUDIT_LOG_STREAM_TIMEOUT = 5 * 1000; -export const auditLogQueueServiceFactory = ({ + +export const auditLogQueueServiceFactory = async ({ auditLogDAL, queueService, projectDAL, licenseService, auditLogStreamDAL }: TAuditLogQueueServiceFactoryDep) => { + const appCfg = getConfig(); + const pushToLog = async (data: TCreateAuditLogDTO) => { - await queueService.queue(QueueName.AuditLog, QueueJobs.AuditLog, data, { - removeOnFail: { - count: 3 - }, - removeOnComplete: true - }); + if (appCfg.USE_PG_QUEUE && appCfg.SHOULD_INIT_PG_QUEUE) { + await queueService.queuePg(QueueJobs.AuditLog, data, { + retryLimit: 10, + retryBackoff: true + }); + } else { + await queueService.queue(QueueName.AuditLog, QueueJobs.AuditLog, data, { + removeOnFail: { + count: 3 + }, + removeOnComplete: true + }); + } }; + if (appCfg.SHOULD_INIT_PG_QUEUE) { + await queueService.startPg( + QueueJobs.AuditLog, + async ([job]) => { + const { actor, event, ipAddress, projectId, userAgent, userAgentType } = job.data; + let { orgId } = job.data; + const MS_IN_DAY = 24 * 60 * 60 * 1000; + let project; + + if (!orgId) { + // it will never be undefined for both org and project id + // TODO(akhilmhdh): use caching here in dal to avoid db calls + project = await projectDAL.findById(projectId as string); + orgId = project.orgId; + } + + const plan = await licenseService.getPlan(orgId); + if (plan.auditLogsRetentionDays === 0) { + // skip inserting if audit log retention is 0 meaning its not supported + return; + } + + // For project actions, set TTL to project-level audit log retention config + // This condition ensures that the plan's audit log retention days cannot be bypassed + const ttlInDays = + project?.auditLogsRetentionDays && project.auditLogsRetentionDays < plan.auditLogsRetentionDays + ? project.auditLogsRetentionDays + : plan.auditLogsRetentionDays; + + const ttl = ttlInDays * MS_IN_DAY; + + const auditLog = await auditLogDAL.create({ + actor: actor.type, + actorMetadata: actor.metadata, + userAgent, + projectId, + projectName: project?.name, + ipAddress, + orgId, + eventType: event.type, + expiresAt: new Date(Date.now() + ttl), + eventMetadata: event.metadata, + userAgentType + }); + + const logStreams = orgId ? await auditLogStreamDAL.find({ orgId }) : []; + await Promise.allSettled( + logStreams.map( + async ({ + url, + encryptedHeadersTag, + encryptedHeadersIV, + encryptedHeadersKeyEncoding, + encryptedHeadersCiphertext + }) => { + const streamHeaders = + encryptedHeadersIV && encryptedHeadersCiphertext && encryptedHeadersTag + ? (JSON.parse( + infisicalSymmetricDecrypt({ + keyEncoding: encryptedHeadersKeyEncoding as SecretKeyEncoding, + iv: encryptedHeadersIV, + tag: encryptedHeadersTag, + ciphertext: encryptedHeadersCiphertext + }) + ) as LogStreamHeaders[]) + : []; + + const headers: RawAxiosRequestHeaders = { "Content-Type": "application/json" }; + + if (streamHeaders.length) + streamHeaders.forEach(({ key, value }) => { + headers[key] = value; + }); + + return request.post(url, auditLog, { + headers, + // request timeout + timeout: AUDIT_LOG_STREAM_TIMEOUT, + // connection timeout + signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) + }); + } + ) + ); + }, + { + batchSize: 1, + workerCount: 30, + pollingIntervalSeconds: 0.5 + } + ); + } + queueService.start(QueueName.AuditLog, async (job) => { const { actor, event, ipAddress, projectId, userAgent, userAgentType } = job.data; let { orgId } = job.data; diff --git a/backend/src/ee/services/audit-log/audit-log-service.ts b/backend/src/ee/services/audit-log/audit-log-service.ts index 747c53c1a..ce6689fe9 100644 --- a/backend/src/ee/services/audit-log/audit-log-service.ts +++ b/backend/src/ee/services/audit-log/audit-log-service.ts @@ -1,7 +1,10 @@ import { ForbiddenError } from "@casl/ability"; +import { requestContext } from "@fastify/request-context"; +import { ActionProjectType } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; +import { ActorType } from "@app/services/auth/auth-type"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TPermissionServiceFactory } from "../permission/permission-service"; @@ -26,13 +29,14 @@ export const auditLogServiceFactory = ({ const listAuditLogs = async ({ actorAuthMethod, actorId, actorOrgId, actor, filter }: TListProjectAuditLogDTO) => { // Filter logs for specific project if (filter.projectId) { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - filter.projectId, + projectId: filter.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); } else { // Organization-wide logs @@ -44,10 +48,6 @@ export const auditLogServiceFactory = ({ actorOrgId ); - /** - * NOTE (dangtony98): Update this to organization-level audit log permission check once audit logs are moved - * to the organization level ✅ - */ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs); } @@ -62,6 +62,9 @@ export const auditLogServiceFactory = ({ actorId: filter.auditLogActorId, actorType: filter.actorType, eventMetadata: filter.eventMetadata, + secretPath: filter.secretPath, + secretKey: filter.secretKey, + environment: filter.environment, ...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId }) }); @@ -79,10 +82,15 @@ export const auditLogServiceFactory = ({ } // add all cases in which project id or org id cannot be added if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) { - if (!data.projectId && !data.orgId) throw new BadRequestError({ message: "Must either project id or org id" }); + if (!data.projectId && !data.orgId) + throw new BadRequestError({ message: "Must specify either project id or org id" }); } - - return auditLogQueue.pushToLog(data); + const el = { ...data }; + if (el.actor.type === ActorType.USER || el.actor.type === ActorType.IDENTITY) { + const permissionMetadata = requestContext.get("identityPermissionMetadata"); + el.actor.metadata.permission = permissionMetadata; + } + return auditLogQueue.pushToLog(el); }; return { 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 51090e594..a31200a1b 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -2,12 +2,36 @@ import { TCreateProjectTemplateDTO, TUpdateProjectTemplateDTO } from "@app/ee/services/project-template/project-template-types"; -import { SymmetricEncryption } from "@app/lib/crypto/cipher"; +import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { + TCreateSecretRotationV2DTO, + TDeleteSecretRotationV2DTO, + TSecretRotationV2Raw, + TUpdateSecretRotationV2DTO +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types"; +import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types"; +import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types"; +import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher"; +import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign/types"; import { TProjectPermission } from "@app/lib/types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types"; import { ActorType } from "@app/services/auth/auth-type"; +import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; import { PkiItemType } from "@app/services/pki-collection/pki-collection-types"; +import { SecretSync, SecretSyncImportBehavior } from "@app/services/secret-sync/secret-sync-enums"; +import { + TCreateSecretSyncDTO, + TDeleteSecretSyncDTO, + TSecretSyncRaw, + TUpdateSecretSyncDTO +} from "@app/services/secret-sync/secret-sync-types"; + +import { KmipPermission } from "../kmip/kmip-enum"; +import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types"; export type TListProjectAuditLogDTO = { filter: { @@ -18,19 +42,31 @@ export type TListProjectAuditLogDTO = { endDate?: string; startDate?: string; projectId?: string; + environment?: string; auditLogActorId?: string; actorType?: ActorType; + secretPath?: string; + secretKey?: string; eventMetadata?: Record; }; } & Omit; export type TCreateAuditLogDTO = { event: Event; - actor: UserActor | IdentityActor | ServiceActor | ScimClientActor | PlatformActor; + actor: + | UserActor + | IdentityActor + | ServiceActor + | ScimClientActor + | PlatformActor + | UnknownUserActor + | KmipClientActor; orgId?: string; projectId?: string; } & BaseAuthData; +export type AuditLogInfo = Pick; + interface BaseAuthData { ipAddress?: string; userAgent?: string; @@ -60,6 +96,7 @@ export enum EventType { DELETE_SECRETS = "delete-secrets", GET_WORKSPACE_KEY = "get-workspace-key", AUTHORIZE_INTEGRATION = "authorize-integration", + UPDATE_INTEGRATION_AUTH = "update-integration-auth", UNAUTHORIZE_INTEGRATION = "unauthorize-integration", CREATE_INTEGRATION = "create-integration", DELETE_INTEGRATION = "delete-integration", @@ -94,6 +131,11 @@ export enum EventType { UPDATE_IDENTITY_OIDC_AUTH = "update-identity-oidc-auth", GET_IDENTITY_OIDC_AUTH = "get-identity-oidc-auth", REVOKE_IDENTITY_OIDC_AUTH = "revoke-identity-oidc-auth", + LOGIN_IDENTITY_JWT_AUTH = "login-identity-jwt-auth", + ADD_IDENTITY_JWT_AUTH = "add-identity-jwt-auth", + UPDATE_IDENTITY_JWT_AUTH = "update-identity-jwt-auth", + GET_IDENTITY_JWT_AUTH = "get-identity-jwt-auth", + REVOKE_IDENTITY_JWT_AUTH = "revoke-identity-jwt-auth", CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret", REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret", GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret", @@ -137,6 +179,24 @@ export enum EventType { SECRET_APPROVAL_REQUEST = "secret-approval-request", SECRET_APPROVAL_CLOSED = "secret-approval-closed", SECRET_APPROVAL_REOPENED = "secret-approval-reopened", + SECRET_APPROVAL_REQUEST_REVIEW = "secret-approval-request-review", + SIGN_SSH_KEY = "sign-ssh-key", + ISSUE_SSH_CREDS = "issue-ssh-creds", + CREATE_SSH_CA = "create-ssh-certificate-authority", + GET_SSH_CA = "get-ssh-certificate-authority", + UPDATE_SSH_CA = "update-ssh-certificate-authority", + DELETE_SSH_CA = "delete-ssh-certificate-authority", + GET_SSH_CA_CERTIFICATE_TEMPLATES = "get-ssh-certificate-authority-certificate-templates", + CREATE_SSH_CERTIFICATE_TEMPLATE = "create-ssh-certificate-template", + UPDATE_SSH_CERTIFICATE_TEMPLATE = "update-ssh-certificate-template", + DELETE_SSH_CERTIFICATE_TEMPLATE = "delete-ssh-certificate-template", + GET_SSH_CERTIFICATE_TEMPLATE = "get-ssh-certificate-template", + CREATE_SSH_HOST = "create-ssh-host", + UPDATE_SSH_HOST = "update-ssh-host", + DELETE_SSH_HOST = "delete-ssh-host", + GET_SSH_HOST = "get-ssh-host", + ISSUE_SSH_HOST_USER_CERT = "issue-ssh-host-user-cert", + ISSUE_SSH_HOST_HOST_CERT = "issue-ssh-host-host-cert", CREATE_CA = "create-certificate-authority", GET_CA = "get-certificate-authority", UPDATE_CA = "update-certificate-authority", @@ -174,6 +234,7 @@ export enum EventType { GET_PROJECT_KMS_BACKUP = "get-project-kms-backup", LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup", ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project", + ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso", CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template", UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template", DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template", @@ -193,8 +254,14 @@ export enum EventType { UPDATE_CMEK = "update-cmek", DELETE_CMEK = "delete-cmek", GET_CMEKS = "get-cmeks", + GET_CMEK = "get-cmek", CMEK_ENCRYPT = "cmek-encrypt", CMEK_DECRYPT = "cmek-decrypt", + CMEK_SIGN = "cmek-sign", + CMEK_VERIFY = "cmek-verify", + CMEK_LIST_SIGNING_ALGORITHMS = "cmek-list-signing-algorithms", + CMEK_GET_PUBLIC_KEY = "cmek-get-public-key", + UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping", GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping", GET_PROJECT_TEMPLATES = "get-project-templates", @@ -202,13 +269,73 @@ export enum EventType { CREATE_PROJECT_TEMPLATE = "create-project-template", UPDATE_PROJECT_TEMPLATE = "update-project-template", DELETE_PROJECT_TEMPLATE = "delete-project-template", - APPLY_PROJECT_TEMPLATE = "apply-project-template" + APPLY_PROJECT_TEMPLATE = "apply-project-template", + GET_APP_CONNECTIONS = "get-app-connections", + GET_AVAILABLE_APP_CONNECTIONS_DETAILS = "get-available-app-connections-details", + GET_APP_CONNECTION = "get-app-connection", + CREATE_APP_CONNECTION = "create-app-connection", + UPDATE_APP_CONNECTION = "update-app-connection", + DELETE_APP_CONNECTION = "delete-app-connection", + CREATE_SHARED_SECRET = "create-shared-secret", + CREATE_SECRET_REQUEST = "create-secret-request", + DELETE_SHARED_SECRET = "delete-shared-secret", + READ_SHARED_SECRET = "read-shared-secret", + GET_SECRET_SYNCS = "get-secret-syncs", + GET_SECRET_SYNC = "get-secret-sync", + CREATE_SECRET_SYNC = "create-secret-sync", + UPDATE_SECRET_SYNC = "update-secret-sync", + DELETE_SECRET_SYNC = "delete-secret-sync", + SECRET_SYNC_SYNC_SECRETS = "secret-sync-sync-secrets", + SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets", + SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets", + OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER = "oidc-group-membership-mapping-assign-user", + OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user", + CREATE_KMIP_CLIENT = "create-kmip-client", + UPDATE_KMIP_CLIENT = "update-kmip-client", + DELETE_KMIP_CLIENT = "delete-kmip-client", + GET_KMIP_CLIENT = "get-kmip-client", + GET_KMIP_CLIENTS = "get-kmip-clients", + CREATE_KMIP_CLIENT_CERTIFICATE = "create-kmip-client-certificate", + + SETUP_KMIP = "setup-kmip", + GET_KMIP = "get-kmip", + REGISTER_KMIP_SERVER = "register-kmip-server", + + KMIP_OPERATION_CREATE = "kmip-operation-create", + KMIP_OPERATION_GET = "kmip-operation-get", + KMIP_OPERATION_DESTROY = "kmip-operation-destroy", + KMIP_OPERATION_GET_ATTRIBUTES = "kmip-operation-get-attributes", + KMIP_OPERATION_ACTIVATE = "kmip-operation-activate", + KMIP_OPERATION_REVOKE = "kmip-operation-revoke", + KMIP_OPERATION_LOCATE = "kmip-operation-locate", + KMIP_OPERATION_REGISTER = "kmip-operation-register", + + GET_SECRET_ROTATIONS = "get-secret-rotations", + GET_SECRET_ROTATION = "get-secret-rotation", + GET_SECRET_ROTATION_GENERATED_CREDENTIALS = "get-secret-rotation-generated-credentials", + CREATE_SECRET_ROTATION = "create-secret-rotation", + UPDATE_SECRET_ROTATION = "update-secret-rotation", + DELETE_SECRET_ROTATION = "delete-secret-rotation", + SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets", + + PROJECT_ACCESS_REQUEST = "project-access-request" } +export const filterableSecretEvents: EventType[] = [ + EventType.GET_SECRET, + EventType.DELETE_SECRETS, + EventType.CREATE_SECRETS, + EventType.UPDATE_SECRETS, + EventType.CREATE_SECRET, + EventType.UPDATE_SECRET, + EventType.DELETE_SECRET +]; + interface UserActorMetadata { userId: string; email?: string | null; username: string; + permission?: Record; } interface ServiceActorMetadata { @@ -219,12 +346,20 @@ interface ServiceActorMetadata { interface IdentityActorMetadata { identityId: string; name: string; + permission?: Record; } interface ScimClientActorMetadata {} interface PlatformActorMetadata {} +interface KmipClientActorMetadata { + clientId: string; + name: string; +} + +interface UnknownUserActorMetadata {} + export interface UserActor { type: ActorType.USER; metadata: UserActorMetadata; @@ -240,6 +375,16 @@ export interface PlatformActor { metadata: PlatformActorMetadata; } +export interface KmipClientActor { + type: ActorType.KMIP_CLIENT; + metadata: KmipClientActorMetadata; +} + +export interface UnknownUserActor { + type: ActorType.UNKNOWN_USER; + metadata: UnknownUserActorMetadata; +} + export interface IdentityActor { type: ActorType.IDENTITY; metadata: IdentityActorMetadata; @@ -250,7 +395,7 @@ export interface ScimClientActor { metadata: ScimClientActorMetadata; } -export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor | PlatformActor; +export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor | PlatformActor | KmipClientActor; interface GetSecretsEvent { type: EventType.GET_SECRETS; @@ -261,6 +406,8 @@ interface GetSecretsEvent { }; } +type TSecretMetadata = { key: string; value: string }[]; + interface GetSecretEvent { type: EventType.GET_SECRET; metadata: { @@ -269,6 +416,7 @@ interface GetSecretEvent { secretId: string; secretKey: string; secretVersion: number; + secretMetadata?: TSecretMetadata; }; } @@ -280,6 +428,7 @@ interface CreateSecretEvent { secretId: string; secretKey: string; secretVersion: number; + secretMetadata?: TSecretMetadata; }; } @@ -288,7 +437,13 @@ interface CreateSecretBatchEvent { metadata: { environment: string; secretPath: string; - secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>; + secrets: Array<{ + secretId: string; + secretKey: string; + secretPath?: string; + secretVersion: number; + secretMetadata?: TSecretMetadata; + }>; }; } @@ -300,6 +455,7 @@ interface UpdateSecretEvent { secretId: string; secretKey: string; secretVersion: number; + secretMetadata?: TSecretMetadata; }; } @@ -307,8 +463,14 @@ interface UpdateSecretBatchEvent { type: EventType.UPDATE_SECRETS; metadata: { environment: string; - secretPath: string; - secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>; + secretPath?: string; + secrets: Array<{ + secretId: string; + secretKey: string; + secretVersion: number; + secretMetadata?: TSecretMetadata; + secretPath?: string; + }>; }; } @@ -357,6 +519,13 @@ interface AuthorizeIntegrationEvent { }; } +interface UpdateIntegrationAuthEvent { + type: EventType.UPDATE_INTEGRATION_AUTH; + metadata: { + integration: string; + }; +} + interface UnauthorizeIntegrationEvent { type: EventType.UNAUTHORIZE_INTEGRATION; metadata: { @@ -699,9 +868,9 @@ interface AddIdentityGcpAuthEvent { metadata: { identityId: string; type: string; - allowedServiceAccounts: string; - allowedProjects: string; - allowedZones: string; + allowedServiceAccounts?: string | null; + allowedProjects?: string | null; + allowedZones?: string | null; accessTokenTTL: number; accessTokenMaxTTL: number; accessTokenNumUsesLimit: number; @@ -721,9 +890,9 @@ interface UpdateIdentityGcpAuthEvent { metadata: { identityId: string; type?: string; - allowedServiceAccounts?: string; - allowedProjects?: string; - allowedZones?: string; + allowedServiceAccounts?: string | null; + allowedProjects?: string | null; + allowedZones?: string | null; accessTokenTTL?: number; accessTokenMaxTTL?: number; accessTokenNumUsesLimit?: number; @@ -844,6 +1013,7 @@ interface LoginIdentityOidcAuthEvent { identityId: string; identityOidcAuthId: string; identityAccessTokenId: string; + oidcClaimsReceived: Record; }; } @@ -856,6 +1026,7 @@ interface AddIdentityOidcAuthEvent { boundIssuer: string; boundAudiences: string; boundClaims: Record; + claimMetadataMapping: Record; boundSubject: string; accessTokenTTL: number; accessTokenMaxTTL: number; @@ -880,6 +1051,7 @@ interface UpdateIdentityOidcAuthEvent { boundIssuer?: string; boundAudiences?: string; boundClaims?: Record; + claimMetadataMapping?: Record; boundSubject?: string; accessTokenTTL?: number; accessTokenMaxTTL?: number; @@ -895,6 +1067,67 @@ interface GetIdentityOidcAuthEvent { }; } +interface LoginIdentityJwtAuthEvent { + type: EventType.LOGIN_IDENTITY_JWT_AUTH; + metadata: { + identityId: string; + identityJwtAuthId: string; + identityAccessTokenId: string; + }; +} + +interface AddIdentityJwtAuthEvent { + type: EventType.ADD_IDENTITY_JWT_AUTH; + metadata: { + identityId: string; + configurationType: string; + jwksUrl?: string; + jwksCaCert: string; + publicKeys: string[]; + boundIssuer: string; + boundAudiences: string; + boundClaims: Record; + boundSubject: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: Array; + }; +} + +interface UpdateIdentityJwtAuthEvent { + type: EventType.UPDATE_IDENTITY_JWT_AUTH; + metadata: { + identityId: string; + configurationType?: string; + jwksUrl?: string; + jwksCaCert?: string; + publicKeys?: string[]; + boundIssuer?: string; + boundAudiences?: string; + boundClaims?: Record; + boundSubject?: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: Array; + }; +} + +interface DeleteIdentityJwtAuthEvent { + type: EventType.REVOKE_IDENTITY_JWT_AUTH; + metadata: { + identityId: string; + }; +} + +interface GetIdentityJwtAuthEvent { + type: EventType.GET_IDENTITY_JWT_AUTH; + metadata: { + identityId: string; + }; +} + interface CreateEnvironmentEvent { type: EventType.CREATE_ENVIRONMENT; metadata: { @@ -961,6 +1194,7 @@ interface CreateFolderEvent { folderId: string; folderName: string; folderPath: string; + description?: string; }; } @@ -1132,6 +1366,201 @@ interface SecretApprovalRequest { }; } +interface SecretApprovalRequestReview { + type: EventType.SECRET_APPROVAL_REQUEST_REVIEW; + metadata: { + secretApprovalRequestId: string; + reviewedBy: string; + status: ApprovalStatus; + comment: string; + }; +} + +interface SignSshKey { + type: EventType.SIGN_SSH_KEY; + metadata: { + certificateTemplateId: string; + certType: SshCertType; + principals: string[]; + ttl: string; + keyId: string; + }; +} + +interface IssueSshCreds { + type: EventType.ISSUE_SSH_CREDS; + metadata: { + certificateTemplateId: string; + keyAlgorithm: SshCertKeyAlgorithm; + certType: SshCertType; + principals: string[]; + ttl: string; + keyId: string; + }; +} + +interface CreateSshCa { + type: EventType.CREATE_SSH_CA; + metadata: { + sshCaId: string; + friendlyName: string; + }; +} + +interface GetSshCa { + type: EventType.GET_SSH_CA; + metadata: { + sshCaId: string; + friendlyName: string; + }; +} + +interface UpdateSshCa { + type: EventType.UPDATE_SSH_CA; + metadata: { + sshCaId: string; + friendlyName: string; + status: SshCaStatus; + }; +} + +interface DeleteSshCa { + type: EventType.DELETE_SSH_CA; + metadata: { + sshCaId: string; + friendlyName: string; + }; +} + +interface GetSshCaCertificateTemplates { + type: EventType.GET_SSH_CA_CERTIFICATE_TEMPLATES; + metadata: { + sshCaId: string; + friendlyName: string; + }; +} + +interface CreateSshCertificateTemplate { + type: EventType.CREATE_SSH_CERTIFICATE_TEMPLATE; + metadata: { + certificateTemplateId: string; + sshCaId: string; + name: string; + ttl: string; + maxTTL: string; + allowedUsers: string[]; + allowedHosts: string[]; + allowUserCertificates: boolean; + allowHostCertificates: boolean; + allowCustomKeyIds: boolean; + }; +} + +interface GetSshCertificateTemplate { + type: EventType.GET_SSH_CERTIFICATE_TEMPLATE; + metadata: { + certificateTemplateId: string; + }; +} + +interface UpdateSshCertificateTemplate { + type: EventType.UPDATE_SSH_CERTIFICATE_TEMPLATE; + metadata: { + certificateTemplateId: string; + sshCaId: string; + name: string; + status: SshCertTemplateStatus; + ttl: string; + maxTTL: string; + allowedUsers: string[]; + allowedHosts: string[]; + allowUserCertificates: boolean; + allowHostCertificates: boolean; + allowCustomKeyIds: boolean; + }; +} + +interface DeleteSshCertificateTemplate { + type: EventType.DELETE_SSH_CERTIFICATE_TEMPLATE; + metadata: { + certificateTemplateId: string; + }; +} + +interface CreateSshHost { + type: EventType.CREATE_SSH_HOST; + metadata: { + sshHostId: string; + hostname: string; + userCertTtl: string; + hostCertTtl: string; + loginMappings: { + loginUser: string; + allowedPrincipals: { + usernames: string[]; + }; + }[]; + userSshCaId: string; + hostSshCaId: string; + }; +} + +interface UpdateSshHost { + type: EventType.UPDATE_SSH_HOST; + metadata: { + sshHostId: string; + hostname?: string; + userCertTtl?: string; + hostCertTtl?: string; + loginMappings?: { + loginUser: string; + allowedPrincipals: { + usernames: string[]; + }; + }[]; + userSshCaId?: string; + hostSshCaId?: string; + }; +} + +interface DeleteSshHost { + type: EventType.DELETE_SSH_HOST; + metadata: { + sshHostId: string; + hostname: string; + }; +} + +interface GetSshHost { + type: EventType.GET_SSH_HOST; + metadata: { + sshHostId: string; + hostname: string; + }; +} + +interface IssueSshHostUserCert { + type: EventType.ISSUE_SSH_HOST_USER_CERT; + metadata: { + sshHostId: string; + hostname: string; + loginUser: string; + principals: string[]; + ttl: string; + }; +} + +interface IssueSshHostHostCert { + type: EventType.ISSUE_SSH_HOST_HOST_CERT; + metadata: { + sshHostId: string; + hostname: string; + serialNumber: string; + principals: string[]; + ttl: string; + }; +} + interface CreateCa { type: EventType.CREATE_CA; metadata: { @@ -1479,6 +1908,11 @@ interface OrgAdminAccessProjectEvent { }; // no metadata yet } +interface OrgAdminBypassSSOEvent { + type: EventType.ORG_ADMIN_BYPASS_SSO; + metadata: Record; // no metadata yet +} + interface CreateCertificateTemplateEstConfig { type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG; metadata: { @@ -1575,7 +2009,7 @@ interface CreateCmekEvent { keyId: string; name: string; description?: string; - encryptionAlgorithm: SymmetricEncryption; + encryptionAlgorithm: SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm; }; } @@ -1602,6 +2036,13 @@ interface GetCmeksEvent { }; } +interface GetCmekEvent { + type: EventType.GET_CMEK; + metadata: { + keyId: string; + }; +} + interface CmekEncryptEvent { type: EventType.CMEK_ENCRYPT; metadata: { @@ -1616,6 +2057,39 @@ interface CmekDecryptEvent { }; } +interface CmekSignEvent { + type: EventType.CMEK_SIGN; + metadata: { + keyId: string; + signingAlgorithm: SigningAlgorithm; + signature: string; + }; +} + +interface CmekVerifyEvent { + type: EventType.CMEK_VERIFY; + metadata: { + keyId: string; + signingAlgorithm: SigningAlgorithm; + signature: string; + signatureValid: boolean; + }; +} + +interface CmekListSigningAlgorithmsEvent { + type: EventType.CMEK_LIST_SIGNING_ALGORITHMS; + metadata: { + keyId: string; + }; +} + +interface CmekGetPublicKeyEvent { + type: EventType.CMEK_GET_PUBLIC_KEY; + metadata: { + keyId: string; + }; +} + interface GetExternalGroupOrgRoleMappingsEvent { type: EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS; metadata?: Record; // not needed, based off orgId @@ -1668,6 +2142,377 @@ interface ApplyProjectTemplateEvent { }; } +interface GetAppConnectionsEvent { + type: EventType.GET_APP_CONNECTIONS; + metadata: { + app?: AppConnection; + count: number; + connectionIds: string[]; + }; +} + +interface GetAvailableAppConnectionsDetailsEvent { + type: EventType.GET_AVAILABLE_APP_CONNECTIONS_DETAILS; + metadata: { + app?: AppConnection; + count: number; + connectionIds: string[]; + }; +} + +interface GetAppConnectionEvent { + type: EventType.GET_APP_CONNECTION; + metadata: { + connectionId: string; + }; +} + +interface CreateAppConnectionEvent { + type: EventType.CREATE_APP_CONNECTION; + metadata: Omit & { connectionId: string }; +} + +interface UpdateAppConnectionEvent { + type: EventType.UPDATE_APP_CONNECTION; + metadata: Omit & { connectionId: string; credentialsUpdated: boolean }; +} + +interface DeleteAppConnectionEvent { + type: EventType.DELETE_APP_CONNECTION; + metadata: { + connectionId: string; + }; +} + +interface CreateSharedSecretEvent { + type: EventType.CREATE_SHARED_SECRET; + metadata: { + id: string; + accessType: string; + name?: string; + expiresAfterViews?: number; + usingPassword: boolean; + expiresAt: string; + }; +} + +interface CreateSecretRequestEvent { + type: EventType.CREATE_SECRET_REQUEST; + metadata: { + id: string; + accessType: string; + name?: string; + }; +} + +interface DeleteSharedSecretEvent { + type: EventType.DELETE_SHARED_SECRET; + metadata: { + id: string; + name?: string; + }; +} + +interface ReadSharedSecretEvent { + type: EventType.READ_SHARED_SECRET; + metadata: { + id: string; + name?: string; + accessType: string; + }; +} + +interface GetSecretSyncsEvent { + type: EventType.GET_SECRET_SYNCS; + metadata: { + destination?: SecretSync; + count: number; + syncIds: string[]; + }; +} + +interface GetSecretSyncEvent { + type: EventType.GET_SECRET_SYNC; + metadata: { + destination: SecretSync; + syncId: string; + }; +} + +interface CreateSecretSyncEvent { + type: EventType.CREATE_SECRET_SYNC; + metadata: Omit & { syncId: string }; +} + +interface UpdateSecretSyncEvent { + type: EventType.UPDATE_SECRET_SYNC; + metadata: TUpdateSecretSyncDTO; +} + +interface DeleteSecretSyncEvent { + type: EventType.DELETE_SECRET_SYNC; + metadata: TDeleteSecretSyncDTO; +} + +interface SecretSyncSyncSecretsEvent { + type: EventType.SECRET_SYNC_SYNC_SECRETS; + metadata: Pick< + TSecretSyncRaw, + "syncOptions" | "destinationConfig" | "destination" | "syncStatus" | "connectionId" | "folderId" + > & { + syncId: string; + syncMessage: string | null; + jobId: string; + jobRanAt: Date; + }; +} + +interface SecretSyncImportSecretsEvent { + type: EventType.SECRET_SYNC_IMPORT_SECRETS; + metadata: Pick< + TSecretSyncRaw, + "syncOptions" | "destinationConfig" | "destination" | "importStatus" | "connectionId" | "folderId" + > & { + syncId: string; + importMessage: string | null; + jobId: string; + jobRanAt: Date; + importBehavior: SecretSyncImportBehavior; + }; +} + +interface SecretSyncRemoveSecretsEvent { + type: EventType.SECRET_SYNC_REMOVE_SECRETS; + metadata: Pick< + TSecretSyncRaw, + "syncOptions" | "destinationConfig" | "destination" | "removeStatus" | "connectionId" | "folderId" + > & { + syncId: string; + removeMessage: string | null; + jobId: string; + jobRanAt: Date; + }; +} + +interface OidcGroupMembershipMappingAssignUserEvent { + type: EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER; + metadata: { + assignedToGroups: { id: string; name: string }[]; + userId: string; + userEmail: string; + userGroupsClaim: string[]; + }; +} + +interface OidcGroupMembershipMappingRemoveUserEvent { + type: EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER; + metadata: { + removedFromGroups: { id: string; name: string }[]; + userId: string; + userEmail: string; + userGroupsClaim: string[]; + }; +} + +interface CreateKmipClientEvent { + type: EventType.CREATE_KMIP_CLIENT; + metadata: { + name: string; + id: string; + permissions: KmipPermission[]; + }; +} + +interface UpdateKmipClientEvent { + type: EventType.UPDATE_KMIP_CLIENT; + metadata: { + name: string; + id: string; + permissions: KmipPermission[]; + }; +} + +interface DeleteKmipClientEvent { + type: EventType.DELETE_KMIP_CLIENT; + metadata: { + id: string; + }; +} + +interface GetKmipClientEvent { + type: EventType.GET_KMIP_CLIENT; + metadata: { + id: string; + }; +} + +interface GetKmipClientsEvent { + type: EventType.GET_KMIP_CLIENTS; + metadata: { + ids: string[]; + }; +} + +interface CreateKmipClientCertificateEvent { + type: EventType.CREATE_KMIP_CLIENT_CERTIFICATE; + metadata: { + clientId: string; + ttl: string; + keyAlgorithm: string; + serialNumber: string; + }; +} + +interface KmipOperationGetEvent { + type: EventType.KMIP_OPERATION_GET; + metadata: { + id: string; + }; +} + +interface KmipOperationDestroyEvent { + type: EventType.KMIP_OPERATION_DESTROY; + metadata: { + id: string; + }; +} + +interface KmipOperationCreateEvent { + type: EventType.KMIP_OPERATION_CREATE; + metadata: { + id: string; + algorithm: string; + }; +} + +interface KmipOperationGetAttributesEvent { + type: EventType.KMIP_OPERATION_GET_ATTRIBUTES; + metadata: { + id: string; + }; +} + +interface KmipOperationActivateEvent { + type: EventType.KMIP_OPERATION_ACTIVATE; + metadata: { + id: string; + }; +} + +interface KmipOperationRevokeEvent { + type: EventType.KMIP_OPERATION_REVOKE; + metadata: { + id: string; + }; +} + +interface KmipOperationLocateEvent { + type: EventType.KMIP_OPERATION_LOCATE; + metadata: { + ids: string[]; + }; +} + +interface KmipOperationRegisterEvent { + type: EventType.KMIP_OPERATION_REGISTER; + metadata: { + id: string; + algorithm: string; + name: string; + }; +} + +interface ProjectAccessRequestEvent { + type: EventType.PROJECT_ACCESS_REQUEST; + metadata: { + projectId: string; + requesterId: string; + requesterEmail: string; + }; +} + +interface SetupKmipEvent { + type: EventType.SETUP_KMIP; + metadata: { + keyAlgorithm: CertKeyAlgorithm; + }; +} + +interface GetKmipEvent { + type: EventType.GET_KMIP; + metadata: { + id: string; + }; +} + +interface RegisterKmipServerEvent { + type: EventType.REGISTER_KMIP_SERVER; + metadata: { + serverCertificateSerialNumber: string; + hostnamesOrIps: string; + commonName: string; + keyAlgorithm: CertKeyAlgorithm; + ttl: string; + }; +} + +interface GetSecretRotationsEvent { + type: EventType.GET_SECRET_ROTATIONS; + metadata: { + type?: SecretRotation; + count: number; + rotationIds: string[]; + secretPath?: string; + environment?: string; + }; +} + +interface GetSecretRotationEvent { + type: EventType.GET_SECRET_ROTATION; + metadata: { + type: SecretRotation; + rotationId: string; + secretPath: string; + environment: string; + }; +} + +interface GetSecretRotationCredentialsEvent { + type: EventType.GET_SECRET_ROTATION_GENERATED_CREDENTIALS; + metadata: { + type: SecretRotation; + rotationId: string; + secretPath: string; + environment: string; + }; +} + +interface CreateSecretRotationEvent { + type: EventType.CREATE_SECRET_ROTATION; + metadata: Omit & { rotationId: string }; +} + +interface UpdateSecretRotationEvent { + type: EventType.UPDATE_SECRET_ROTATION; + metadata: TUpdateSecretRotationV2DTO; +} + +interface DeleteSecretRotationEvent { + type: EventType.DELETE_SECRET_ROTATION; + metadata: TDeleteSecretRotationV2DTO; +} + +interface RotateSecretRotationEvent { + type: EventType.SECRET_ROTATION_ROTATE_SECRETS; + metadata: Pick & { + status: SecretRotationStatus; + rotationId: string; + jobId?: string | undefined; + occurredAt: Date; + message?: string | null | undefined; + }; +} + export type Event = | GetSecretsEvent | GetSecretEvent @@ -1680,6 +2525,7 @@ export type Event = | DeleteSecretBatchEvent | GetWorkspaceKeyEvent | AuthorizeIntegrationEvent + | UpdateIntegrationAuthEvent | UnauthorizeIntegrationEvent | CreateIntegrationEvent | DeleteIntegrationEvent @@ -1733,6 +2579,11 @@ export type Event = | DeleteIdentityOidcAuthEvent | UpdateIdentityOidcAuthEvent | GetIdentityOidcAuthEvent + | LoginIdentityJwtAuthEvent + | AddIdentityJwtAuthEvent + | UpdateIdentityJwtAuthEvent + | GetIdentityJwtAuthEvent + | DeleteIdentityJwtAuthEvent | CreateEnvironmentEvent | GetEnvironmentEvent | UpdateEnvironmentEvent @@ -1757,6 +2608,23 @@ export type Event = | SecretApprovalClosed | SecretApprovalRequest | SecretApprovalReopened + | SignSshKey + | IssueSshCreds + | CreateSshCa + | GetSshCa + | UpdateSshCa + | DeleteSshCa + | GetSshCaCertificateTemplates + | CreateSshCertificateTemplate + | UpdateSshCertificateTemplate + | GetSshCertificateTemplate + | DeleteSshCertificateTemplate + | CreateSshHost + | UpdateSshHost + | DeleteSshHost + | GetSshHost + | IssueSshHostUserCert + | IssueSshHostHostCert | CreateCa | GetCa | UpdateCa @@ -1794,6 +2662,7 @@ export type Event = | GetProjectKmsBackupEvent | LoadProjectKmsBackupEvent | OrgAdminAccessProjectEvent + | OrgAdminBypassSSOEvent | CreateCertificateTemplate | UpdateCertificateTemplate | GetCertificateTemplate @@ -1812,9 +2681,14 @@ export type Event = | CreateCmekEvent | UpdateCmekEvent | DeleteCmekEvent + | GetCmekEvent | GetCmeksEvent | CmekEncryptEvent | CmekDecryptEvent + | CmekSignEvent + | CmekVerifyEvent + | CmekListSigningAlgorithmsEvent + | CmekGetPublicKeyEvent | GetExternalGroupOrgRoleMappingsEvent | UpdateExternalGroupOrgRoleMappingsEvent | GetProjectTemplatesEvent @@ -1822,4 +2696,50 @@ export type Event = | CreateProjectTemplateEvent | UpdateProjectTemplateEvent | DeleteProjectTemplateEvent - | ApplyProjectTemplateEvent; + | ApplyProjectTemplateEvent + | GetAppConnectionsEvent + | GetAvailableAppConnectionsDetailsEvent + | GetAppConnectionEvent + | CreateAppConnectionEvent + | UpdateAppConnectionEvent + | DeleteAppConnectionEvent + | CreateSharedSecretEvent + | DeleteSharedSecretEvent + | ReadSharedSecretEvent + | GetSecretSyncsEvent + | GetSecretSyncEvent + | CreateSecretSyncEvent + | UpdateSecretSyncEvent + | DeleteSecretSyncEvent + | SecretSyncSyncSecretsEvent + | SecretSyncImportSecretsEvent + | SecretSyncRemoveSecretsEvent + | OidcGroupMembershipMappingAssignUserEvent + | OidcGroupMembershipMappingRemoveUserEvent + | CreateKmipClientEvent + | UpdateKmipClientEvent + | DeleteKmipClientEvent + | GetKmipClientEvent + | GetKmipClientsEvent + | CreateKmipClientCertificateEvent + | SetupKmipEvent + | GetKmipEvent + | RegisterKmipServerEvent + | KmipOperationGetEvent + | KmipOperationDestroyEvent + | KmipOperationCreateEvent + | KmipOperationGetAttributesEvent + | KmipOperationActivateEvent + | KmipOperationRevokeEvent + | KmipOperationLocateEvent + | KmipOperationRegisterEvent + | ProjectAccessRequestEvent + | CreateSecretRequestEvent + | SecretApprovalRequestReview + | GetSecretRotationsEvent + | GetSecretRotationEvent + | GetSecretRotationCredentialsEvent + | CreateSecretRotationEvent + | UpdateSecretRotationEvent + | DeleteSecretRotationEvent + | RotateSecretRotationEvent; diff --git a/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts index 7282b0a29..b8f4ce663 100644 --- a/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts +++ b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; +import { ActionProjectType } from "@app/db/schemas"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -66,13 +67,14 @@ export const certificateAuthorityCrlServiceFactory = ({ const ca = await certificateAuthorityDAL.findById(caId); if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - ca.projectId, + projectId: ca.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, diff --git a/backend/src/ee/services/certificate-est/certificate-est-service.ts b/backend/src/ee/services/certificate-est/certificate-est-service.ts index ce3821ae0..627cc58c6 100644 --- a/backend/src/ee/services/certificate-est/certificate-est-service.ts +++ b/backend/src/ee/services/certificate-est/certificate-est-service.ts @@ -1,5 +1,6 @@ import * as x509 from "@peculiar/x509"; +import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate"; import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { isCertChainValid } from "@app/services/certificate/certificate-fns"; import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal"; @@ -67,9 +68,7 @@ export const certificateEstServiceFactory = ({ const certTemplate = await certificateTemplateDAL.findById(certificateTemplateId); - const leafCertificate = decodeURIComponent(sslClientCert).match( - /-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g - )?.[0]; + const leafCertificate = extractX509CertFromChain(decodeURIComponent(sslClientCert))?.[0]; if (!leafCertificate) { throw new UnauthorizedError({ message: "Missing client certificate" }); @@ -88,10 +87,7 @@ export const certificateEstServiceFactory = ({ const verifiedChains = await Promise.all( caCertChains.map((chain) => { const caCert = new x509.X509Certificate(chain.certificate); - const caChain = - chain.certificateChain - .match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g) - ?.map((c) => new x509.X509Certificate(c)) || []; + const caChain = extractX509CertFromChain(chain.certificateChain)?.map((c) => new x509.X509Certificate(c)) || []; return isCertChainValid([cert, caCert, ...caChain]); }) @@ -171,27 +167,25 @@ export const certificateEstServiceFactory = ({ }); } - const caCerts = estConfig.caChain - .match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g) - ?.map((cert) => { + if (!estConfig.disableBootstrapCertValidation) { + const caCerts = extractX509CertFromChain(estConfig.caChain)?.map((cert) => { return new x509.X509Certificate(cert); }); - if (!caCerts) { - throw new BadRequestError({ message: "Failed to parse certificate chain" }); - } + if (!caCerts) { + throw new BadRequestError({ message: "Failed to parse certificate chain" }); + } - const leafCertificate = decodeURIComponent(sslClientCert).match( - /-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g - )?.[0]; + const leafCertificate = extractX509CertFromChain(decodeURIComponent(sslClientCert))?.[0]; - if (!leafCertificate) { - throw new BadRequestError({ message: "Missing client certificate" }); - } + if (!leafCertificate) { + throw new BadRequestError({ message: "Missing client certificate" }); + } - const certObj = new x509.X509Certificate(leafCertificate); - if (!(await isCertChainValid([certObj, ...caCerts]))) { - throw new BadRequestError({ message: "Invalid certificate chain" }); + const certObj = new x509.X509Certificate(leafCertificate); + if (!(await isCertChainValid([certObj, ...caCerts]))) { + throw new BadRequestError({ message: "Invalid certificate chain" }); + } } const { certificate } = await certificateAuthorityService.signCertFromCa({ @@ -248,13 +242,7 @@ export const certificateEstServiceFactory = ({ kmsService }); - const certificates = caCertChain - .match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g) - ?.map((cert) => new x509.X509Certificate(cert)); - - if (!certificates) { - throw new BadRequestError({ message: "Failed to parse certificate chain" }); - } + const certificates = extractX509CertFromChain(caCertChain).map((cert) => new x509.X509Certificate(cert)); const caCertificate = new x509.X509Certificate(caCert); return convertRawCertsToPkcs7([caCertificate.rawData, ...certificates.map((cert) => cert.rawData)]); diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts index 810628030..e9f00f401 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts @@ -37,11 +37,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { db.ref("type").withSchema(TableName.DynamicSecret).as("dynType"), db.ref("defaultTTL").withSchema(TableName.DynamicSecret).as("dynDefaultTTL"), db.ref("maxTTL").withSchema(TableName.DynamicSecret).as("dynMaxTTL"), - db.ref("inputIV").withSchema(TableName.DynamicSecret).as("dynInputIV"), - db.ref("inputTag").withSchema(TableName.DynamicSecret).as("dynInputTag"), - db.ref("inputCiphertext").withSchema(TableName.DynamicSecret).as("dynInputCiphertext"), - db.ref("algorithm").withSchema(TableName.DynamicSecret).as("dynAlgorithm"), - db.ref("keyEncoding").withSchema(TableName.DynamicSecret).as("dynKeyEncoding"), + db.ref("encryptedInput").withSchema(TableName.DynamicSecret).as("dynEncryptedInput"), db.ref("folderId").withSchema(TableName.DynamicSecret).as("dynFolderId"), db.ref("status").withSchema(TableName.DynamicSecret).as("dynStatus"), db.ref("statusDetails").withSchema(TableName.DynamicSecret).as("dynStatusDetails"), @@ -59,11 +55,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { type: doc.dynType, defaultTTL: doc.dynDefaultTTL, maxTTL: doc.dynMaxTTL, - inputIV: doc.dynInputIV, - inputTag: doc.dynInputTag, - inputCiphertext: doc.dynInputCiphertext, - algorithm: doc.dynAlgorithm, - keyEncoding: doc.dynKeyEncoding, + encryptedInput: doc.dynEncryptedInput, folderId: doc.dynFolderId, status: doc.dynStatus, statusDetails: doc.dynStatusDetails, diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts index 9bdb1c24e..fa1a80ac3 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts @@ -1,8 +1,10 @@ -import { SecretKeyEncoding } from "@app/db/schemas"; import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue"; -import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal"; import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types"; @@ -14,6 +16,8 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = { dynamicSecretLeaseDAL: Pick; dynamicSecretDAL: Pick; dynamicSecretProviders: Record; + kmsService: Pick; + folderDAL: Pick; }; export type TDynamicSecretLeaseQueueServiceFactory = ReturnType; @@ -22,7 +26,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ queueService, dynamicSecretDAL, dynamicSecretProviders, - dynamicSecretLeaseDAL + dynamicSecretLeaseDAL, + kmsService, + folderDAL }: TDynamicSecretLeaseQueueServiceFactoryDep) => { const pruneDynamicSecret = async (dynamicSecretCfgId: string) => { await queueService.queue( @@ -76,15 +82,21 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId); if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" }); + const folder = await folderDAL.findById(dynamicSecretLease.dynamicSecret.folderId); + if (!folder) + throw new NotFoundError({ + message: `Failed to find folder with ${dynamicSecretLease.dynamicSecret.folderId}` + }); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: folder.projectId + }); + const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ - keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, - ciphertext: dynamicSecretCfg.inputCiphertext, - tag: dynamicSecretCfg.inputTag, - iv: dynamicSecretCfg.inputIV - }) + secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() ) as object; await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId); @@ -100,16 +112,22 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting) throw new DisableRotationErrors({ message: "Document not deleted" }); + const folder = await folderDAL.findById(dynamicSecretCfg.folderId); + if (!folder) + throw new NotFoundError({ + message: `Failed to find folder with ${dynamicSecretCfg.folderId}` + }); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: folder.projectId + }); + const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfgId }); if (dynamicSecretLeases.length) { const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ - keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, - ciphertext: dynamicSecretCfg.inputCiphertext, - tag: dynamicSecretCfg.inputTag, - iv: dynamicSecretCfg.inputIV - }) + secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() ) as object; await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id))); diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts index 38d7d1abd..88f2d90f1 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts @@ -1,7 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; -import ms from "ms"; -import { SecretKeyEncoding } from "@app/db/schemas"; +import { ActionProjectType } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { @@ -9,9 +8,11 @@ import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; -import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; +import { ms } from "@app/lib/ms"; +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 { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; @@ -37,6 +38,7 @@ type TDynamicSecretLeaseServiceFactoryDep = { folderDAL: Pick; permissionService: Pick; projectDAL: Pick; + kmsService: Pick; }; export type TDynamicSecretLeaseServiceFactory = ReturnType; @@ -49,7 +51,8 @@ export const dynamicSecretLeaseServiceFactory = ({ permissionService, dynamicSecretQueueService, projectDAL, - licenseService + licenseService, + kmsService }: TDynamicSecretLeaseServiceFactoryDep) => { const create = async ({ environmentSlug, @@ -67,17 +70,14 @@ export const dynamicSecretLeaseServiceFactory = ({ if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); const projectId = project.id; - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.Lease, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); const plan = await licenseService.getPlan(actorOrgId); if (!plan?.dynamicSecret) { @@ -98,21 +98,31 @@ export const dynamicSecretLeaseServiceFactory = ({ message: `Dynamic secret with name '${name}' in folder with path '${path}' not found` }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionDynamicSecretActions.Lease, + subject(ProjectPermissionSub.DynamicSecrets, { + environment: environmentSlug, + secretPath: path, + metadata: dynamicSecretCfg.metadata + }) + ); + const totalLeasesTaken = await dynamicSecretLeaseDAL.countLeasesForDynamicSecret(dynamicSecretCfg.id); if (totalLeasesTaken >= appCfg.MAX_LEASE_LIMIT) throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` }); const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ - keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, - ciphertext: dynamicSecretCfg.inputCiphertext, - tag: dynamicSecretCfg.inputTag, - iv: dynamicSecretCfg.inputIV - }) + secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString() ) as object; - const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL; + const selectedTTL = ttl || dynamicSecretCfg.defaultTTL; const { maxTTL } = dynamicSecretCfg; const expireAt = new Date(new Date().getTime() + ms(selectedTTL)); if (maxTTL) { @@ -146,17 +156,19 @@ export const dynamicSecretLeaseServiceFactory = ({ if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); const projectId = project.id; - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.Lease, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); const plan = await licenseService.getPlan(actorOrgId); if (!plan?.dynamicSecret) { @@ -172,22 +184,35 @@ export const dynamicSecretLeaseServiceFactory = ({ }); const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId); - if (!dynamicSecretLease) { + if (!dynamicSecretLease || dynamicSecretLease.dynamicSecret.folderId !== folder.id) { throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` }); } - const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; + const dynamicSecretCfg = await dynamicSecretDAL.findOne({ + id: dynamicSecretLease.dynamicSecretId, + folderId: folder.id + }); + + if (!dynamicSecretCfg) + throw new NotFoundError({ + message: `Dynamic secret with ID '${dynamicSecretLease.dynamicSecretId}' not found` + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionDynamicSecretActions.Lease, + subject(ProjectPermissionSub.DynamicSecrets, { + environment: environmentSlug, + secretPath: path, + metadata: dynamicSecretCfg.metadata + }) + ); + const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ - keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, - ciphertext: dynamicSecretCfg.inputCiphertext, - tag: dynamicSecretCfg.inputTag, - iv: dynamicSecretCfg.inputIV - }) + secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString() ) as object; - const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL; + const selectedTTL = ttl || dynamicSecretCfg.defaultTTL; const { maxTTL } = dynamicSecretCfg; const expireAt = new Date(dynamicSecretLease.expireAt.getTime() + ms(selectedTTL)); if (maxTTL) { @@ -225,17 +250,19 @@ export const dynamicSecretLeaseServiceFactory = ({ if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); const projectId = project.id; - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.Lease, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); if (!folder) @@ -244,18 +271,31 @@ export const dynamicSecretLeaseServiceFactory = ({ }); const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId); - if (!dynamicSecretLease) + if (!dynamicSecretLease || dynamicSecretLease.dynamicSecret.folderId !== folder.id) throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` }); - const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; + const dynamicSecretCfg = await dynamicSecretDAL.findOne({ + id: dynamicSecretLease.dynamicSecretId, + folderId: folder.id + }); + + if (!dynamicSecretCfg) + throw new NotFoundError({ + message: `Dynamic secret with ID '${dynamicSecretLease.dynamicSecretId}' not found` + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionDynamicSecretActions.Lease, + subject(ProjectPermissionSub.DynamicSecrets, { + environment: environmentSlug, + secretPath: path, + metadata: dynamicSecretCfg.metadata + }) + ); + const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ - keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, - ciphertext: dynamicSecretCfg.inputCiphertext, - tag: dynamicSecretCfg.inputTag, - iv: dynamicSecretCfg.inputIV - }) + secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString() ) as object; const revokeResponse = await selectedProvider @@ -294,17 +334,14 @@ export const dynamicSecretLeaseServiceFactory = ({ if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); const projectId = project.id; - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.Lease, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); if (!folder) @@ -318,6 +355,15 @@ export const dynamicSecretLeaseServiceFactory = ({ message: `Dynamic secret with name '${name}' in folder with path '${path}' not found` }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionDynamicSecretActions.Lease, + subject(ProjectPermissionSub.DynamicSecrets, { + environment: environmentSlug, + secretPath: path, + metadata: dynamicSecretCfg.metadata + }) + ); + const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id }); return dynamicSecretLeases; }; @@ -336,17 +382,14 @@ export const dynamicSecretLeaseServiceFactory = ({ if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); const projectId = project.id; - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.Lease, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); if (!folder) throw new NotFoundError({ message: `Folder with path '${path}' not found` }); @@ -355,6 +398,25 @@ export const dynamicSecretLeaseServiceFactory = ({ if (!dynamicSecretLease) throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` }); + const dynamicSecretCfg = await dynamicSecretDAL.findOne({ + id: dynamicSecretLease.dynamicSecretId, + folderId: folder.id + }); + + if (!dynamicSecretCfg) + throw new NotFoundError({ + message: `Dynamic secret with ID '${dynamicSecretLease.dynamicSecretId}' not found` + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionDynamicSecretActions.Lease, + subject(ProjectPermissionSub.DynamicSecrets, { + environment: environmentSlug, + secretPath: path, + metadata: dynamicSecretCfg.metadata + }) + ); + return dynamicSecretLease; }; diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-dal.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-dal.ts index e47d9102d..d7f78c3b1 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-dal.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-dal.ts @@ -1,9 +1,17 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; +import { TableName, TDynamicSecrets } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { + buildFindFilter, + ormify, + prependTableNameToFindFilter, + selectAllTableCols, + sqlNestRelationships, + TFindFilter, + TFindOpt +} from "@app/lib/knex"; import { OrderByDirection } from "@app/lib/types"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; @@ -12,6 +20,86 @@ export type TDynamicSecretDALFactory = ReturnType { const orm = ormify(db, TableName.DynamicSecret); + const findOne = async (filter: TFindFilter, tx?: Knex) => { + const query = (tx || db.replicaNode())(TableName.DynamicSecret) + .leftJoin( + TableName.ResourceMetadata, + `${TableName.ResourceMetadata}.dynamicSecretId`, + `${TableName.DynamicSecret}.id` + ) + .select(selectAllTableCols(TableName.DynamicSecret)) + .select( + db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue") + ) + .where(prependTableNameToFindFilter(TableName.DynamicSecret, filter)); + + const docs = sqlNestRelationships({ + data: await query, + key: "id", + parentMapper: (el) => el, + childrenMapper: [ + { + key: "metadataId", + label: "metadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) + } + ] + }); + + return docs[0]; + }; + + const findWithMetadata = async ( + filter: TFindFilter, + { offset, limit, sort, tx }: TFindOpt = {} + ) => { + const query = (tx || db.replicaNode())(TableName.DynamicSecret) + .leftJoin( + TableName.ResourceMetadata, + `${TableName.ResourceMetadata}.dynamicSecretId`, + `${TableName.DynamicSecret}.id` + ) + .select(selectAllTableCols(TableName.DynamicSecret)) + .select( + db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue") + ) + // eslint-disable-next-line @typescript-eslint/no-misused-promises + .where(buildFindFilter(filter)); + + if (limit) void query.limit(limit); + if (offset) void query.offset(offset); + if (sort) { + void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); + } + + const docs = sqlNestRelationships({ + data: await query, + key: "id", + parentMapper: (el) => el, + childrenMapper: [ + { + key: "metadataId", + label: "metadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) + } + ] + }); + + return docs; + }; + // find dynamic secrets for multiple environments (folder IDs are cross env, thus need to rank for pagination) const listDynamicSecretsByFolderIds = async ( { @@ -39,18 +127,27 @@ export const dynamicSecretDALFactory = (db: TDbClient) => { void bd.whereILike(`${TableName.DynamicSecret}.name`, `%${search}%`); } }) + .leftJoin( + TableName.ResourceMetadata, + `${TableName.ResourceMetadata}.dynamicSecretId`, + `${TableName.DynamicSecret}.id` + ) .leftJoin(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`) .leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) .select( selectAllTableCols(TableName.DynamicSecret), db.ref("slug").withSchema(TableName.Environment).as("environment"), - db.raw(`DENSE_RANK() OVER (ORDER BY ${TableName.DynamicSecret}."name" ${orderDirection}) as rank`) + db.raw(`DENSE_RANK() OVER (ORDER BY ${TableName.DynamicSecret}."name" ${orderDirection}) as rank`), + db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue") ) .orderBy(`${TableName.DynamicSecret}.${orderBy}`, orderDirection); + let queryWithLimit; if (limit) { const rankOffset = offset + 1; - return await (tx || db) + queryWithLimit = (tx || db.replicaNode()) .with("w", query) .select("*") .from[number]>("w") @@ -58,7 +155,22 @@ export const dynamicSecretDALFactory = (db: TDbClient) => { .andWhere("w.rank", "<", rankOffset + limit); } - const dynamicSecrets = await query; + const dynamicSecrets = sqlNestRelationships({ + data: await (queryWithLimit || query), + key: "id", + parentMapper: (el) => el, + childrenMapper: [ + { + key: "metadataId", + label: "metadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) + } + ] + }); return dynamicSecrets; } catch (error) { @@ -66,5 +178,5 @@ export const dynamicSecretDALFactory = (db: TDbClient) => { } }; - return { ...orm, listDynamicSecretsByFolderIds }; + return { ...orm, listDynamicSecretsByFolderIds, findOne, findWithMetadata }; }; diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts index 04aeb3950..05d492240 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts @@ -1,20 +1,53 @@ +import dns from "node:dns/promises"; +import net from "node:net"; + import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; +import { isPrivateIp } from "@app/lib/ip/ipRange"; import { getDbConnectionHost } from "@app/lib/knex"; -export const verifyHostInputValidity = (host: string) => { +export const verifyHostInputValidity = async (host: string, isGateway = false) => { const appCfg = getConfig(); - const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI); - if ( - appCfg.isCloud && - // localhost - // internal ips - (host === "host.docker.internal" || host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/)) - ) - throw new BadRequestError({ message: "Invalid db host" }); + if (appCfg.isDevelopmentMode) return [host]; - if (host === "localhost" || host === "127.0.0.1" || dbHost === host) { - throw new BadRequestError({ message: "Invalid db host" }); + const reservedHosts = [appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI)].concat( + (appCfg.DB_READ_REPLICAS || []).map((el) => getDbConnectionHost(el.DB_CONNECTION_URI)), + getDbConnectionHost(appCfg.REDIS_URL), + getDbConnectionHost(appCfg.AUDIT_LOGS_DB_CONNECTION_URI) + ); + + // get host db ip + const exclusiveIps: string[] = []; + for await (const el of reservedHosts) { + if (el) { + if (net.isIPv4(el)) { + exclusiveIps.push(el); + } else { + const resolvedIps = await dns.resolve4(el); + exclusiveIps.push(...resolvedIps); + } + } } + + const normalizedHost = host.split(":")[0]; + const inputHostIps: string[] = []; + if (net.isIPv4(host)) { + inputHostIps.push(host); + } else { + if (normalizedHost === "localhost" || normalizedHost === "host.docker.internal") { + throw new BadRequestError({ message: "Invalid db host" }); + } + const resolvedIps = await dns.resolve4(host); + inputHostIps.push(...resolvedIps); + } + + if (!isGateway && !(appCfg.DYNAMIC_SECRET_ALLOW_INTERNAL_IP || appCfg.ALLOW_INTERNAL_IP_CONNECTIONS)) { + const isInternalIp = inputHostIps.some((el) => isPrivateIp(el)); + if (isInternalIp) throw new BadRequestError({ message: "Invalid db host" }); + } + + const isAppUsedIps = inputHostIps.some((el) => exclusiveIps.includes(el)); + if (isAppUsedIps) throw new BadRequestError({ message: "Invalid db host" }); + return inputHostIps; }; diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts index 5eff1cdcf..44c18b001 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -1,20 +1,23 @@ import { ForbiddenError, subject } from "@casl/ability"; -import { SecretKeyEncoding } from "@app/db/schemas"; +import { ActionProjectType } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionDynamicSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { OrderByDirection, OrgServiceActor } from "@app/lib/types"; +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 { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal"; import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue"; +import { TProjectGatewayDALFactory } from "../gateway/project-gateway-dal"; import { TDynamicSecretDALFactory } from "./dynamic-secret-dal"; import { DynamicSecretStatus, @@ -42,6 +45,9 @@ type TDynamicSecretServiceFactoryDep = { folderDAL: Pick; projectDAL: Pick; permissionService: Pick; + kmsService: Pick; + projectGatewayDAL: Pick; + resourceMetadataDAL: Pick; }; export type TDynamicSecretServiceFactory = ReturnType; @@ -54,7 +60,10 @@ export const dynamicSecretServiceFactory = ({ dynamicSecretProviders, permissionService, dynamicSecretQueueService, - projectDAL + projectDAL, + kmsService, + projectGatewayDAL, + resourceMetadataDAL }: TDynamicSecretServiceFactoryDep) => { const create = async ({ path, @@ -67,22 +76,25 @@ export const dynamicSecretServiceFactory = ({ projectSlug, actorOrgId, defaultTTL, - actorAuthMethod + actorAuthMethod, + metadata }: TCreateDynamicSecretDTO) => { const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); const projectId = project.id; - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionDynamicSecretActions.CreateRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) + subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path, metadata }) ); const plan = await licenseService.getPlan(actorOrgId); @@ -104,24 +116,56 @@ export const dynamicSecretServiceFactory = ({ const selectedProvider = dynamicSecretProviders[provider.type]; const inputs = await selectedProvider.validateProviderInputs(provider.inputs); + let selectedGatewayId: string | null = null; + if (inputs && typeof inputs === "object" && "projectGatewayId" in inputs && inputs.projectGatewayId) { + const projectGatewayId = inputs.projectGatewayId as string; + + const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId }); + if (!projectGateway) + throw new NotFoundError({ + message: `Project gateway with ${projectGatewayId} not found` + }); + selectedGatewayId = projectGateway.id; + } + const isConnected = await selectedProvider.validateConnection(provider.inputs); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); - const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs)); - - const dynamicSecretCfg = await dynamicSecretDAL.create({ - type: provider.type, - version: 1, - inputIV: encryptedInput.iv, - inputTag: encryptedInput.tag, - inputCiphertext: encryptedInput.ciphertext, - algorithm: encryptedInput.algorithm, - keyEncoding: encryptedInput.encoding, - maxTTL, - defaultTTL, - folderId: folder.id, - name + const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId }); + + const dynamicSecretCfg = await dynamicSecretDAL.transaction(async (tx) => { + const cfg = await dynamicSecretDAL.create( + { + type: provider.type, + version: 1, + encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob, + maxTTL, + defaultTTL, + folderId: folder.id, + name, + projectGatewayId: selectedGatewayId + }, + tx + ); + + if (metadata) { + await resourceMetadataDAL.insertMany( + metadata.map(({ key, value }) => ({ + key, + value, + dynamicSecretId: cfg.id, + orgId: actorOrgId + })), + tx + ); + } + + return cfg; + }); + return dynamicSecretCfg; }; @@ -137,24 +181,22 @@ export const dynamicSecretServiceFactory = ({ actorId, newName, actorOrgId, - actorAuthMethod + actorAuthMethod, + metadata }: TUpdateDynamicSecretDTO) => { const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); const projectId = project.id; - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.EditRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); const plan = await licenseService.getPlan(actorOrgId); if (!plan?.dynamicSecret) { @@ -173,39 +215,100 @@ export const dynamicSecretServiceFactory = ({ message: `Dynamic secret with name '${name}' in folder '${folder.path}' not found` }); } + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionDynamicSecretActions.EditRootCredential, + subject(ProjectPermissionSub.DynamicSecrets, { + environment: environmentSlug, + secretPath: path, + metadata: dynamicSecretCfg.metadata + }) + ); + + if (metadata) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionDynamicSecretActions.EditRootCredential, + subject(ProjectPermissionSub.DynamicSecrets, { + environment: environmentSlug, + secretPath: path, + metadata + }) + ); + } + if (newName) { const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id }); if (existingDynamicSecret) throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" }); } + const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = + await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ - keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, - ciphertext: dynamicSecretCfg.inputCiphertext, - tag: dynamicSecretCfg.inputTag, - iv: dynamicSecretCfg.inputIV - }) + secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() ) as object; const newInput = { ...decryptedStoredInput, ...(inputs || {}) }; const updatedInput = await selectedProvider.validateProviderInputs(newInput); + let selectedGatewayId: string | null = null; + if ( + updatedInput && + typeof updatedInput === "object" && + "projectGatewayId" in updatedInput && + updatedInput?.projectGatewayId + ) { + const projectGatewayId = updatedInput.projectGatewayId as string; + + const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId }); + if (!projectGateway) + throw new NotFoundError({ + message: `Project gateway with ${projectGatewayId} not found` + }); + selectedGatewayId = projectGateway.id; + } + const isConnected = await selectedProvider.validateConnection(newInput); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); - const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(updatedInput)); - const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, { - inputIV: encryptedInput.iv, - inputTag: encryptedInput.tag, - inputCiphertext: encryptedInput.ciphertext, - algorithm: encryptedInput.algorithm, - keyEncoding: encryptedInput.encoding, - maxTTL, - defaultTTL, - name: newName ?? name, - status: null, - statusDetails: null + const updatedDynamicCfg = await dynamicSecretDAL.transaction(async (tx) => { + const cfg = await dynamicSecretDAL.updateById( + dynamicSecretCfg.id, + { + encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(updatedInput)) }) + .cipherTextBlob, + maxTTL, + defaultTTL, + name: newName ?? name, + status: null, + projectGatewayId: selectedGatewayId + }, + tx + ); + + if (metadata) { + await resourceMetadataDAL.delete( + { + dynamicSecretId: cfg.id + }, + tx + ); + + await resourceMetadataDAL.insertMany( + metadata.map(({ key, value }) => ({ + key, + value, + dynamicSecretId: cfg.id, + orgId: actorOrgId + })), + tx + ); + } + + return cfg; }); return updatedDynamicCfg; @@ -227,17 +330,14 @@ export const dynamicSecretServiceFactory = ({ const projectId = project.id; - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.DeleteRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); if (!folder) @@ -248,6 +348,15 @@ export const dynamicSecretServiceFactory = ({ throw new NotFoundError({ message: `Dynamic secret with name '${name}' in folder '${folder.path}' not found` }); } + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionDynamicSecretActions.DeleteRootCredential, + subject(ProjectPermissionSub.DynamicSecrets, { + environment: environmentSlug, + secretPath: path, + metadata: dynamicSecretCfg.metadata + }) + ); + const leases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id }); // when not forced we check with the external system to first remove the things // we introduce a forced concept because consider the external lease got deleted by some other external like a human or another system @@ -287,21 +396,14 @@ export const dynamicSecretServiceFactory = ({ if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); const projectId = project.id; - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.ReadRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) - ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.EditRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); if (!folder) @@ -311,16 +413,36 @@ export const dynamicSecretServiceFactory = ({ if (!dynamicSecretCfg) { throw new NotFoundError({ message: `Dynamic secret with name '${name} in folder '${path}' not found` }); } - const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ - keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, - ciphertext: dynamicSecretCfg.inputCiphertext, - tag: dynamicSecretCfg.inputTag, - iv: dynamicSecretCfg.inputIV + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionDynamicSecretActions.ReadRootCredential, + subject(ProjectPermissionSub.DynamicSecrets, { + environment: environmentSlug, + secretPath: path, + metadata: dynamicSecretCfg.metadata }) + ); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionDynamicSecretActions.EditRootCredential, + subject(ProjectPermissionSub.DynamicSecrets, { + environment: environmentSlug, + secretPath: path, + metadata: dynamicSecretCfg.metadata + }) + ); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + + const decryptedStoredInput = JSON.parse( + secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() ) as object; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object; + return { ...dynamicSecretCfg, inputs: providerInputs }; }; @@ -337,13 +459,14 @@ export const dynamicSecretServiceFactory = ({ isInternal }: TListDynamicSecretsMultiEnvDTO) => { if (!isInternal) { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); // verify user has access to each env in request environmentSlugs.forEach((environmentSlug) => @@ -380,16 +503,17 @@ export const dynamicSecretServiceFactory = ({ search, projectId }: TGetDynamicSecretsCountDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionDynamicSecretActions.ReadRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionSub.DynamicSecrets ); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); @@ -428,23 +552,20 @@ export const dynamicSecretServiceFactory = ({ projectId = project.id; } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.ReadRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); if (!folder) throw new NotFoundError({ message: `Folder with path '${path}' in environment '${environmentSlug}' not found` }); - const dynamicSecretCfg = await dynamicSecretDAL.find( + const dynamicSecretCfg = await dynamicSecretDAL.findWithMetadata( { folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined }, { limit, @@ -452,20 +573,31 @@ export const dynamicSecretServiceFactory = ({ sort: orderBy ? [[orderBy, orderDirection]] : undefined } ); - return dynamicSecretCfg; + + return dynamicSecretCfg.filter((dynamicSecret) => { + return permission.can( + ProjectPermissionDynamicSecretActions.ReadRootCredential, + subject(ProjectPermissionSub.DynamicSecrets, { + environment: environmentSlug, + secretPath: path, + metadata: dynamicSecret.metadata + }) + ); + }); }; const listDynamicSecretsByFolderIds = async ( { folderMappings, filters, projectId }: TListDynamicSecretsByFolderMappingsDTO, actor: OrgServiceActor ) => { - const { permission } = await permissionService.getProjectPermission( - actor.type, - actor.id, + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, projectId, - actor.authMethod, - actor.orgId - ); + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager + }); const userAccessibleFolderMappings = folderMappings.filter(({ path, environment }) => permission.can( @@ -503,23 +635,14 @@ export const dynamicSecretServiceFactory = ({ isInternal, ...params }: TListDynamicSecretsMultiEnvDTO) => { - if (!isInternal) { - const { permission } = await permissionService.getProjectPermission( - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId - ); - - // verify user has access to each env in request - environmentSlugs.forEach((environmentSlug) => - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.ReadRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) - ) - ); - } + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path); if (!folders.length) @@ -532,7 +655,16 @@ export const dynamicSecretServiceFactory = ({ ...params }); - return dynamicSecretCfg; + return dynamicSecretCfg.filter((dynamicSecret) => { + return permission.can( + ProjectPermissionDynamicSecretActions.ReadRootCredential, + subject(ProjectPermissionSub.DynamicSecrets, { + environment: dynamicSecret.environment, + secretPath: path, + metadata: dynamicSecret.metadata + }) + ); + }); }; const fetchAzureEntraIdUsers = async ({ diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-types.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-types.ts index 957d884c8..58fdc2143 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-types.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-types.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { OrderByDirection, TProjectPermission } from "@app/lib/types"; +import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { DynamicSecretProviderSchema } from "./providers/models"; @@ -20,6 +21,7 @@ export type TCreateDynamicSecretDTO = { environmentSlug: string; name: string; projectSlug: string; + metadata?: ResourceMetadataDTO; } & Omit; export type TUpdateDynamicSecretDTO = { @@ -31,6 +33,7 @@ export type TUpdateDynamicSecretDTO = { environmentSlug: string; inputs?: TProvider["inputs"]; projectSlug: string; + metadata?: ResourceMetadataDTO; } & Omit; export type TDeleteDynamicSecretDTO = { diff --git a/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts b/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts index 2cb862029..f2907f7dc 100644 --- a/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts +++ b/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts @@ -13,6 +13,7 @@ import { customAlphabet } from "nanoid"; import { z } from "zod"; import { BadRequestError } from "@app/lib/errors"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { DynamicSecretAwsElastiCacheSchema, TDynamicProviderFns } from "./models"; @@ -80,7 +81,7 @@ const ElastiCacheUserManager = (credentials: TBasicAWSCredentials, region: strin } }; - const addUserToInfisicalGroup = async (userId: string) => { + const $addUserToInfisicalGroup = async (userId: string) => { // figure out if the default user is already in the group, if it is, then we shouldn't add it again const addUserToGroupCommand = new ModifyUserGroupCommand({ @@ -96,7 +97,7 @@ const ElastiCacheUserManager = (credentials: TBasicAWSCredentials, region: strin await ensureInfisicalGroupExists(clusterName); await elastiCache.send(new CreateUserCommand(creationInput)); // First create the user - await addUserToInfisicalGroup(creationInput.UserId); // Then add the user to the group. We know the group is already a part of the cluster because of ensureInfisicalGroupExists() + await $addUserToInfisicalGroup(creationInput.UserId); // Then add the user to the group. We know the group is already a part of the cluster because of ensureInfisicalGroupExists() return { userId: creationInput.UserId, @@ -127,7 +128,7 @@ const ElastiCacheUserManager = (credentials: TBasicAWSCredentials, region: strin }; const generatePassword = () => { - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#"; + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*"; return customAlphabet(charset, 64)(); }; @@ -144,6 +145,14 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => { // We can't return the parsed statements here because we need to use the handlebars template to generate the username and password, before we can use the parsed statements. CreateElastiCacheUserSchema.parse(JSON.parse(providerInputs.creationStatement)); DeleteElasticCacheUserSchema.parse(JSON.parse(providerInputs.revocationStatement)); + validateHandlebarTemplate("AWS ElastiCache creation", providerInputs.creationStatement, { + allowedExpressions: (val) => ["username", "password", "expiration"].includes(val) + }); + if (providerInputs.revocationStatement) { + validateHandlebarTemplate("AWS ElastiCache revoke", providerInputs.revocationStatement, { + allowedExpressions: (val) => ["username"].includes(val) + }); + } return providerInputs; }; @@ -211,8 +220,8 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => { return { entityId }; }; - const renew = async (inputs: unknown, entityId: string) => { - // Do nothing + const renew = async (_inputs: unknown, entityId: string) => { + // No renewal necessary return { entityId }; }; diff --git a/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts b/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts index 3feafa534..64ea6a02e 100644 --- a/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts +++ b/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts @@ -33,7 +33,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => { return providerInputs; }; - const getClient = async (providerInputs: z.infer) => { + const $getClient = async (providerInputs: z.infer) => { const client = new IAMClient({ region: providerInputs.region, credentials: { @@ -47,7 +47,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const isConnected = await client.send(new GetUserCommand({})).then(() => true); return isConnected; @@ -55,7 +55,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => { const create = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const username = generateUsername(); const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs; @@ -118,7 +118,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => { const revoke = async (inputs: unknown, entityId: string) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const username = entityId; @@ -179,9 +179,8 @@ export const AwsIamProvider = (): TDynamicProviderFns => { }; const renew = async (_inputs: unknown, entityId: string) => { - // do nothing - const username = entityId; - return { entityId: username }; + // No renewal necessary + return { entityId }; }; return { diff --git a/backend/src/ee/services/dynamic-secret/providers/azure-entra-id.ts b/backend/src/ee/services/dynamic-secret/providers/azure-entra-id.ts index e2dfe2d4b..17f644601 100644 --- a/backend/src/ee/services/dynamic-secret/providers/azure-entra-id.ts +++ b/backend/src/ee/services/dynamic-secret/providers/azure-entra-id.ts @@ -9,7 +9,7 @@ const MSFT_GRAPH_API_URL = "https://graph.microsoft.com/v1.0/"; const MSFT_LOGIN_URL = "https://login.microsoftonline.com"; const generatePassword = () => { - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#"; + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*"; return customAlphabet(charset, 64)(); }; @@ -23,7 +23,7 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & { return providerInputs; }; - const getToken = async ( + const $getToken = async ( tenantId: string, applicationId: string, clientSecret: string @@ -51,18 +51,13 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret); + const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret); return data.success; }; - const renew = async (inputs: unknown, entityId: string) => { - // Do nothing - return { entityId }; - }; - const create = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret); + const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret); if (!data.success) { throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" }); } @@ -98,7 +93,7 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & { }; const fetchAzureEntraIdUsers = async (tenantId: string, applicationId: string, clientSecret: string) => { - const data = await getToken(tenantId, applicationId, clientSecret); + const data = await $getToken(tenantId, applicationId, clientSecret); if (!data.success) { throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" }); } @@ -127,6 +122,11 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & { return users; }; + const renew = async (_inputs: unknown, entityId: string) => { + // No renewal necessary + return { entityId }; + }; + return { validateProviderInputs, validateConnection, diff --git a/backend/src/ee/services/dynamic-secret/providers/cassandra.ts b/backend/src/ee/services/dynamic-secret/providers/cassandra.ts index aea0b9c99..0b6d50146 100644 --- a/backend/src/ee/services/dynamic-secret/providers/cassandra.ts +++ b/backend/src/ee/services/dynamic-secret/providers/cassandra.ts @@ -3,13 +3,14 @@ import handlebars from "handlebars"; import { customAlphabet } from "nanoid"; import { z } from "zod"; -import { BadRequestError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; +import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { DynamicSecretCassandraSchema, TDynamicProviderFns } from "./models"; const generatePassword = (size = 48) => { - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#"; + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*"; return customAlphabet(charset, 48)(size); }; @@ -20,14 +21,28 @@ const generateUsername = () => { export const CassandraProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretCassandraSchema.parseAsync(inputs); - if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1") { - throw new BadRequestError({ message: "Invalid db host" }); + const hostIps = await Promise.all( + providerInputs.host + .split(",") + .filter(Boolean) + .map((el) => verifyHostInputValidity(el).then((ip) => ip[0])) + ); + validateHandlebarTemplate("Cassandra creation", providerInputs.creationStatement, { + allowedExpressions: (val) => ["username", "password", "expiration", "keyspace"].includes(val) + }); + if (providerInputs.renewStatement) { + validateHandlebarTemplate("Cassandra renew", providerInputs.renewStatement, { + allowedExpressions: (val) => ["username", "expiration", "keyspace"].includes(val) + }); } + validateHandlebarTemplate("Cassandra revoke", providerInputs.revocationStatement, { + allowedExpressions: (val) => ["username"].includes(val) + }); - return providerInputs; + return { ...providerInputs, hostIps }; }; - const getClient = async (providerInputs: z.infer) => { + const $getClient = async (providerInputs: z.infer & { hostIps: string[] }) => { const sslOptions = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined; const client = new cassandra.Client({ sslOptions, @@ -40,14 +55,14 @@ export const CassandraProvider = (): TDynamicProviderFns => { }, keyspace: providerInputs.keyspace, localDataCenter: providerInputs?.localDataCenter, - contactPoints: providerInputs.host.split(",").filter(Boolean) + contactPoints: providerInputs.hostIps }); return client; }; const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const isConnected = await client.execute("SELECT * FROM system_schema.keyspaces").then(() => true); await client.shutdown(); @@ -56,7 +71,7 @@ export const CassandraProvider = (): TDynamicProviderFns => { const create = async (inputs: unknown, expireAt: number) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const username = generateUsername(); const password = generatePassword(); @@ -82,7 +97,7 @@ export const CassandraProvider = (): TDynamicProviderFns => { const revoke = async (inputs: unknown, entityId: string) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const username = entityId; const { keyspace } = providerInputs; @@ -99,20 +114,24 @@ export const CassandraProvider = (): TDynamicProviderFns => { const renew = async (inputs: unknown, entityId: string, expireAt: number) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + if (!providerInputs.renewStatement) return { entityId }; + + const client = await $getClient(providerInputs); - const username = entityId; const expiration = new Date(expireAt).toISOString(); const { keyspace } = providerInputs; - const renewStatement = handlebars.compile(providerInputs.revocationStatement)({ username, keyspace, expiration }); + const renewStatement = handlebars.compile(providerInputs.renewStatement)({ + username: entityId, + keyspace, + expiration + }); const queries = renewStatement.toString().split(";").filter(Boolean); - for (const query of queries) { - // eslint-disable-next-line + for await (const query of queries) { await client.execute(query); } await client.shutdown(); - return { entityId: username }; + return { entityId }; }; return { 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 bfe0ac443..6c1affa39 100644 --- a/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts +++ b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts @@ -8,7 +8,7 @@ import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { DynamicSecretElasticSearchSchema, ElasticSearchAuthTypes, TDynamicProviderFns } from "./models"; const generatePassword = () => { - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#"; + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*"; return customAlphabet(charset, 64)(); }; @@ -19,15 +19,14 @@ const generateUsername = () => { export const ElasticSearchProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretElasticSearchSchema.parseAsync(inputs); - verifyHostInputValidity(providerInputs.host); - - return providerInputs; + const [hostIp] = await verifyHostInputValidity(providerInputs.host); + return { ...providerInputs, hostIp }; }; - const getClient = async (providerInputs: z.infer) => { + const $getClient = async (providerInputs: z.infer & { hostIp: string }) => { const connection = new ElasticSearchClient({ node: { - url: new URL(`${providerInputs.host}:${providerInputs.port}`), + url: new URL(`${providerInputs.hostIp}:${providerInputs.port}`), ...(providerInputs.ca && { ssl: { rejectUnauthorized: false, @@ -55,7 +54,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const connection = await getClient(providerInputs); + const connection = await $getClient(providerInputs); const infoResponse = await connection .info() @@ -67,7 +66,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => { const create = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const connection = await getClient(providerInputs); + const connection = await $getClient(providerInputs); const username = generateUsername(); const password = generatePassword(); @@ -85,7 +84,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => { const revoke = async (inputs: unknown, entityId: string) => { const providerInputs = await validateProviderInputs(inputs); - const connection = await getClient(providerInputs); + const connection = await $getClient(providerInputs); await connection.security.deleteUser({ username: entityId @@ -95,8 +94,8 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => { return { entityId }; }; - const renew = async (inputs: unknown, entityId: string) => { - // Do nothing + const renew = async (_inputs: unknown, entityId: string) => { + // No renewal necessary return { entityId }; }; diff --git a/backend/src/ee/services/dynamic-secret/providers/index.ts b/backend/src/ee/services/dynamic-secret/providers/index.ts index f70985379..faa671980 100644 --- a/backend/src/ee/services/dynamic-secret/providers/index.ts +++ b/backend/src/ee/services/dynamic-secret/providers/index.ts @@ -1,21 +1,30 @@ import { SnowflakeProvider } from "@app/ee/services/dynamic-secret/providers/snowflake"; +import { TGatewayServiceFactory } from "../../gateway/gateway-service"; import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache"; import { AwsIamProvider } from "./aws-iam"; import { AzureEntraIDProvider } from "./azure-entra-id"; import { CassandraProvider } from "./cassandra"; import { ElasticSearchProvider } from "./elastic-search"; import { LdapProvider } from "./ldap"; -import { DynamicSecretProviders } from "./models"; +import { DynamicSecretProviders, TDynamicProviderFns } from "./models"; import { MongoAtlasProvider } from "./mongo-atlas"; import { MongoDBProvider } from "./mongo-db"; import { RabbitMqProvider } from "./rabbit-mq"; import { RedisDatabaseProvider } from "./redis"; +import { SapAseProvider } from "./sap-ase"; import { SapHanaProvider } from "./sap-hana"; import { SqlDatabaseProvider } from "./sql-database"; +import { TotpProvider } from "./totp"; -export const buildDynamicSecretProviders = () => ({ - [DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(), +type TBuildDynamicSecretProviderDTO = { + gatewayService: Pick; +}; + +export const buildDynamicSecretProviders = ({ + gatewayService +}: TBuildDynamicSecretProviderDTO): Record => ({ + [DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider({ gatewayService }), [DynamicSecretProviders.Cassandra]: CassandraProvider(), [DynamicSecretProviders.AwsIam]: AwsIamProvider(), [DynamicSecretProviders.Redis]: RedisDatabaseProvider(), @@ -27,5 +36,7 @@ export const buildDynamicSecretProviders = () => ({ [DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(), [DynamicSecretProviders.Ldap]: LdapProvider(), [DynamicSecretProviders.SapHana]: SapHanaProvider(), - [DynamicSecretProviders.Snowflake]: SnowflakeProvider() + [DynamicSecretProviders.Snowflake]: SnowflakeProvider(), + [DynamicSecretProviders.Totp]: TotpProvider(), + [DynamicSecretProviders.SapAse]: SapAseProvider() }); diff --git a/backend/src/ee/services/dynamic-secret/providers/ldap.ts b/backend/src/ee/services/dynamic-secret/providers/ldap.ts index f94e61629..cc68304e0 100644 --- a/backend/src/ee/services/dynamic-secret/providers/ldap.ts +++ b/backend/src/ee/services/dynamic-secret/providers/ldap.ts @@ -2,6 +2,7 @@ import handlebars from "handlebars"; import ldapjs from "ldapjs"; import ldif from "ldif"; import { customAlphabet } from "nanoid"; +import RE2 from "re2"; import { z } from "zod"; import { BadRequestError } from "@app/lib/errors"; @@ -52,7 +53,7 @@ export const LdapProvider = (): TDynamicProviderFns => { return providerInputs; }; - const getClient = async (providerInputs: z.infer): Promise => { + const $getClient = async (providerInputs: z.infer): Promise => { return new Promise((resolve, reject) => { const client = ldapjs.createClient({ url: providerInputs.url, @@ -83,7 +84,7 @@ export const LdapProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); return client.connected; }; @@ -191,10 +192,11 @@ export const LdapProvider = (): TDynamicProviderFns => { const create = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); if (providerInputs.credentialType === LdapCredentialType.Static) { - const dnMatch = providerInputs.rotationLdif.match(/^dn:\s*(.+)/m); + const dnRegex = new RE2("^dn:\\s*(.+)", "m"); + const dnMatch = dnRegex.exec(providerInputs.rotationLdif); if (dnMatch) { const username = dnMatch[1]; @@ -235,10 +237,11 @@ export const LdapProvider = (): TDynamicProviderFns => { const revoke = async (inputs: unknown, entityId: string) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); if (providerInputs.credentialType === LdapCredentialType.Static) { - const dnMatch = providerInputs.rotationLdif.match(/^dn:\s*(.+)/m); + const dnRegex = new RE2("^dn:\\s*(.+)", "m"); + const dnMatch = dnRegex.exec(providerInputs.rotationLdif); if (dnMatch) { const username = dnMatch[1]; @@ -268,7 +271,7 @@ export const LdapProvider = (): TDynamicProviderFns => { }; const renew = async (inputs: unknown, entityId: string) => { - // Do nothing + // No renewal necessary return { entityId }; }; diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index d98215fd4..449f6d8f6 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -1,10 +1,22 @@ import { z } from "zod"; +export type PasswordRequirements = { + length: number; + required: { + lowercase: number; + uppercase: number; + digits: number; + symbols: number; + }; + allowedSymbols?: string; +}; + export enum SqlProviders { Postgres = "postgres", MySQL = "mysql2", Oracle = "oracledb", - MsSQL = "mssql" + MsSQL = "mssql", + SapAse = "sap-ase" } export enum ElasticSearchAuthTypes { @@ -17,6 +29,17 @@ export enum LdapCredentialType { Static = "static" } +export enum TotpConfigType { + URL = "url", + MANUAL = "manual" +} + +export enum TotpAlgorithm { + SHA1 = "sha1", + SHA256 = "sha256", + SHA512 = "sha512" +} + export const DynamicSecretRedisDBSchema = z.object({ host: z.string().trim().toLowerCase(), port: z.number(), @@ -88,10 +111,33 @@ export const DynamicSecretSqlDBSchema = z.object({ database: z.string().trim(), username: z.string().trim(), password: z.string().trim(), + passwordRequirements: z + .object({ + length: z.number().min(1).max(250), + required: z + .object({ + lowercase: z.number().min(0), + uppercase: z.number().min(0), + digits: z.number().min(0), + symbols: z.number().min(0) + }) + .refine((data) => { + const total = Object.values(data).reduce((sum, count) => sum + count, 0); + return total <= 250; + }, "Sum of required characters cannot exceed 250"), + allowedSymbols: z.string().optional() + }) + .refine((data) => { + const total = Object.values(data.required).reduce((sum, count) => sum + count, 0); + return total <= data.length; + }, "Sum of required characters cannot exceed the total length") + .optional() + .describe("Password generation requirements"), creationStatement: z.string().trim(), revocationStatement: z.string().trim(), renewStatement: z.string().trim().optional(), - ca: z.string().optional() + ca: z.string().optional(), + projectGatewayId: z.string().nullable().optional() }); export const DynamicSecretCassandraSchema = z.object({ @@ -107,6 +153,16 @@ export const DynamicSecretCassandraSchema = z.object({ ca: z.string().optional() }); +export const DynamicSecretSapAseSchema = z.object({ + host: z.string().trim().toLowerCase(), + port: z.number(), + database: z.string().trim(), + username: z.string().trim(), + password: z.string().trim(), + creationStatement: z.string().trim(), + revocationStatement: z.string().trim() +}); + export const DynamicSecretAwsIamSchema = z.object({ accessKey: z.string().trim().min(1), secretAccessKey: z.string().trim().min(1), @@ -221,6 +277,34 @@ export const LdapSchema = z.union([ }) ]); +export const DynamicSecretTotpSchema = z.discriminatedUnion("configType", [ + z.object({ + configType: z.literal(TotpConfigType.URL), + url: z + .string() + .url() + .trim() + .min(1) + .refine((val) => { + const urlObj = new URL(val); + const secret = urlObj.searchParams.get("secret"); + + return Boolean(secret); + }, "OTP URL must contain secret field") + }), + z.object({ + configType: z.literal(TotpConfigType.MANUAL), + secret: z + .string() + .trim() + .min(1) + .transform((val) => val.replace(/\s+/g, "")), + period: z.number().optional(), + algorithm: z.nativeEnum(TotpAlgorithm).optional(), + digits: z.number().optional() + }) +]); + export enum DynamicSecretProviders { SqlDatabase = "sql-database", Cassandra = "cassandra", @@ -234,12 +318,15 @@ export enum DynamicSecretProviders { AzureEntraID = "azure-entra-id", Ldap = "ldap", SapHana = "sap-hana", - Snowflake = "snowflake" + Snowflake = "snowflake", + Totp = "totp", + SapAse = "sap-ase" } export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema }), z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema }), + z.object({ type: z.literal(DynamicSecretProviders.SapAse), inputs: DynamicSecretSapAseSchema }), z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema }), z.object({ type: z.literal(DynamicSecretProviders.Redis), inputs: DynamicSecretRedisDBSchema }), z.object({ type: z.literal(DynamicSecretProviders.SapHana), inputs: DynamicSecretSapHanaSchema }), @@ -250,7 +337,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }), z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }), z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema }), - z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema }) + z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema }), + z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema }) ]); export type TDynamicProviderFns = { diff --git a/backend/src/ee/services/dynamic-secret/providers/mongo-atlas.ts b/backend/src/ee/services/dynamic-secret/providers/mongo-atlas.ts index 69f54ce77..6cb414d10 100644 --- a/backend/src/ee/services/dynamic-secret/providers/mongo-atlas.ts +++ b/backend/src/ee/services/dynamic-secret/providers/mongo-atlas.ts @@ -8,7 +8,7 @@ import { alphaNumericNanoId } from "@app/lib/nanoid"; import { DynamicSecretMongoAtlasSchema, TDynamicProviderFns } from "./models"; const generatePassword = (size = 48) => { - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#"; + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*"; return customAlphabet(charset, 48)(size); }; @@ -22,7 +22,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => { return providerInputs; }; - const getClient = async (providerInputs: z.infer) => { + const $getClient = async (providerInputs: z.infer) => { const client = axios.create({ baseURL: "https://cloud.mongodb.com/api/atlas", headers: { @@ -40,7 +40,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const isConnected = await client({ method: "GET", @@ -59,7 +59,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => { const create = async (inputs: unknown, expireAt: number) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const username = generateUsername(); const password = generatePassword(); @@ -87,7 +87,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => { const revoke = async (inputs: unknown, entityId: string) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const username = entityId; const isExisting = await client({ @@ -114,7 +114,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => { const renew = async (inputs: unknown, entityId: string, expireAt: number) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const username = entityId; const expiration = new Date(expireAt).toISOString(); diff --git a/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts b/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts index b824f5aa8..bee29bfc4 100644 --- a/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts +++ b/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts @@ -8,7 +8,7 @@ import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { DynamicSecretMongoDBSchema, TDynamicProviderFns } from "./models"; const generatePassword = (size = 48) => { - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#"; + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*"; return customAlphabet(charset, 48)(size); }; @@ -19,15 +19,15 @@ const generateUsername = () => { export const MongoDBProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretMongoDBSchema.parseAsync(inputs); - verifyHostInputValidity(providerInputs.host); - return providerInputs; + const [hostIp] = await verifyHostInputValidity(providerInputs.host); + return { ...providerInputs, hostIp }; }; - const getClient = async (providerInputs: z.infer) => { + const $getClient = async (providerInputs: z.infer & { hostIp: string }) => { const isSrv = !providerInputs.port; const uri = isSrv - ? `mongodb+srv://${providerInputs.host}` - : `mongodb://${providerInputs.host}:${providerInputs.port}`; + ? `mongodb+srv://${providerInputs.hostIp}` + : `mongodb://${providerInputs.hostIp}:${providerInputs.port}`; const client = new MongoClient(uri, { auth: { @@ -42,7 +42,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const isConnected = await client .db(providerInputs.database) @@ -55,7 +55,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => { const create = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const username = generateUsername(); const password = generatePassword(); @@ -74,7 +74,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => { const revoke = async (inputs: unknown, entityId: string) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const username = entityId; @@ -88,6 +88,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => { }; const renew = async (_inputs: unknown, entityId: string) => { + // No renewal necessary return { entityId }; }; diff --git a/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts b/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts index 00d3b538f..f6c73ba54 100644 --- a/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts +++ b/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts @@ -3,7 +3,6 @@ import https from "https"; import { customAlphabet } from "nanoid"; import { z } from "zod"; -import { removeTrailingSlash } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; @@ -11,7 +10,7 @@ import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { DynamicSecretRabbitMqSchema, TDynamicProviderFns } from "./models"; const generatePassword = () => { - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#"; + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*"; return customAlphabet(charset, 64)(); }; @@ -79,14 +78,13 @@ async function deleteRabbitMqUser({ axiosInstance, usernameToDelete }: TDeleteRa export const RabbitMqProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretRabbitMqSchema.parseAsync(inputs); - verifyHostInputValidity(providerInputs.host); - - return providerInputs; + const [hostIp] = await verifyHostInputValidity(providerInputs.host); + return { ...providerInputs, hostIp }; }; - const getClient = async (providerInputs: z.infer) => { + const $getClient = async (providerInputs: z.infer & { hostIp: string }) => { const axiosInstance = axios.create({ - baseURL: `${removeTrailingSlash(providerInputs.host)}:${providerInputs.port}/api`, + baseURL: `${providerInputs.hostIp}:${providerInputs.port}/api`, auth: { username: providerInputs.username, password: providerInputs.password @@ -105,7 +103,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const connection = await getClient(providerInputs); + const connection = await $getClient(providerInputs); const infoResponse = await connection.get("/whoami").then(() => true); @@ -114,7 +112,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => { const create = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const connection = await getClient(providerInputs); + const connection = await $getClient(providerInputs); const username = generateUsername(); const password = generatePassword(); @@ -134,15 +132,15 @@ export const RabbitMqProvider = (): TDynamicProviderFns => { const revoke = async (inputs: unknown, entityId: string) => { const providerInputs = await validateProviderInputs(inputs); - const connection = await getClient(providerInputs); + const connection = await $getClient(providerInputs); await deleteRabbitMqUser({ axiosInstance: connection, usernameToDelete: entityId }); return { entityId }; }; - const renew = async (inputs: unknown, entityId: string) => { - // Do nothing + const renew = async (_inputs: unknown, entityId: string) => { + // No renewal necessary return { entityId }; }; diff --git a/backend/src/ee/services/dynamic-secret/providers/redis.ts b/backend/src/ee/services/dynamic-secret/providers/redis.ts index 0e7ae99a0..f180dd607 100644 --- a/backend/src/ee/services/dynamic-secret/providers/redis.ts +++ b/backend/src/ee/services/dynamic-secret/providers/redis.ts @@ -5,12 +5,13 @@ import { z } from "zod"; import { BadRequestError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { DynamicSecretRedisDBSchema, TDynamicProviderFns } from "./models"; const generatePassword = () => { - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#"; + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*"; return customAlphabet(charset, 64)(); }; @@ -51,16 +52,28 @@ const executeTransactions = async (connection: Redis, commands: string[]): Promi export const RedisDatabaseProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretRedisDBSchema.parseAsync(inputs); - verifyHostInputValidity(providerInputs.host); - return providerInputs; + const [hostIp] = await verifyHostInputValidity(providerInputs.host); + validateHandlebarTemplate("Redis creation", providerInputs.creationStatement, { + allowedExpressions: (val) => ["username", "password", "expiration"].includes(val) + }); + if (providerInputs.renewStatement) { + validateHandlebarTemplate("Redis renew", providerInputs.renewStatement, { + allowedExpressions: (val) => ["username", "expiration"].includes(val) + }); + } + validateHandlebarTemplate("Redis revoke", providerInputs.revocationStatement, { + allowedExpressions: (val) => ["username"].includes(val) + }); + + return { ...providerInputs, hostIp }; }; - const getClient = async (providerInputs: z.infer) => { + const $getClient = async (providerInputs: z.infer & { hostIp: string }) => { let connection: Redis | null = null; try { connection = new Redis({ username: providerInputs.username, - host: providerInputs.host, + host: providerInputs.hostIp, port: providerInputs.port, password: providerInputs.password, ...(providerInputs.ca && { @@ -92,7 +105,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const connection = await getClient(providerInputs); + const connection = await $getClient(providerInputs); const pingResponse = await connection .ping() @@ -104,7 +117,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => { const create = async (inputs: unknown, expireAt: number) => { const providerInputs = await validateProviderInputs(inputs); - const connection = await getClient(providerInputs); + const connection = await $getClient(providerInputs); const username = generateUsername(); const password = generatePassword(); @@ -126,7 +139,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => { const revoke = async (inputs: unknown, entityId: string) => { const providerInputs = await validateProviderInputs(inputs); - const connection = await getClient(providerInputs); + const connection = await $getClient(providerInputs); const username = entityId; @@ -141,7 +154,9 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => { const renew = async (inputs: unknown, entityId: string, expireAt: number) => { const providerInputs = await validateProviderInputs(inputs); - const connection = await getClient(providerInputs); + if (!providerInputs.renewStatement) return { entityId }; + + const connection = await $getClient(providerInputs); const username = entityId; const expiration = new Date(expireAt).toISOString(); diff --git a/backend/src/ee/services/dynamic-secret/providers/sap-ase.ts b/backend/src/ee/services/dynamic-secret/providers/sap-ase.ts new file mode 100644 index 000000000..c832e9867 --- /dev/null +++ b/backend/src/ee/services/dynamic-secret/providers/sap-ase.ts @@ -0,0 +1,157 @@ +import handlebars from "handlebars"; +import { customAlphabet } from "nanoid"; +import odbc from "odbc"; +import { z } from "zod"; + +import { BadRequestError } from "@app/lib/errors"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; + +import { verifyHostInputValidity } from "../dynamic-secret-fns"; +import { DynamicSecretSapAseSchema, TDynamicProviderFns } from "./models"; + +const generatePassword = (size = 48) => { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + return customAlphabet(charset, 48)(size); +}; + +const generateUsername = () => { + return alphaNumericNanoId(25); +}; + +enum SapCommands { + CreateLogin = "sp_addlogin", + DropLogin = "sp_droplogin" +} + +export const SapAseProvider = (): TDynamicProviderFns => { + const validateProviderInputs = async (inputs: unknown) => { + const providerInputs = await DynamicSecretSapAseSchema.parseAsync(inputs); + + const [hostIp] = await verifyHostInputValidity(providerInputs.host); + validateHandlebarTemplate("SAP ASE creation", providerInputs.creationStatement, { + allowedExpressions: (val) => ["username", "password"].includes(val) + }); + if (providerInputs.revocationStatement) { + validateHandlebarTemplate("SAP ASE revoke", providerInputs.revocationStatement, { + allowedExpressions: (val) => ["username"].includes(val) + }); + } + return { ...providerInputs, hostIp }; + }; + + const $getClient = async ( + providerInputs: z.infer & { hostIp: string }, + useMaster?: boolean + ) => { + const connectionString = + `DRIVER={FreeTDS};` + + `SERVER=${providerInputs.hostIp};` + + `PORT=${providerInputs.port};` + + `DATABASE=${useMaster ? "master" : providerInputs.database};` + + `UID=${providerInputs.username};` + + `PWD=${providerInputs.password};` + + `TDS_VERSION=5.0`; + + const client = await odbc.connect(connectionString); + + return client; + }; + + const validateConnection = async (inputs: unknown) => { + const providerInputs = await validateProviderInputs(inputs); + const masterClient = await $getClient(providerInputs, true); + const client = await $getClient(providerInputs); + + const [resultFromMasterDatabase] = await masterClient.query<{ version: string }>("SELECT @@VERSION AS version"); + const [resultFromSelectedDatabase] = await client.query<{ version: string }>("SELECT @@VERSION AS version"); + + if (!resultFromSelectedDatabase.version) { + throw new BadRequestError({ + message: "Failed to validate SAP ASE connection, version query failed" + }); + } + + if (resultFromMasterDatabase.version !== resultFromSelectedDatabase.version) { + throw new BadRequestError({ + message: "Failed to validate SAP ASE connection (master), version mismatch" + }); + } + + return true; + }; + + const create = async (inputs: unknown) => { + const providerInputs = await validateProviderInputs(inputs); + + const username = `inf_${generateUsername()}`; + const password = `${generatePassword()}`; + + const client = await $getClient(providerInputs); + const masterClient = await $getClient(providerInputs, true); + + const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({ + username, + password + }); + + const queries = creationStatement.trim().replaceAll("\n", "").split(";").filter(Boolean); + + for await (const query of queries) { + // If it's an adduser query, we need to first call sp_addlogin on the MASTER database. + // If not done, then the newly created user won't be able to authenticate. + await (query.startsWith(SapCommands.CreateLogin) ? masterClient : client).query(query); + } + + await masterClient.close(); + await client.close(); + + return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } }; + }; + + const revoke = async (inputs: unknown, username: string) => { + const providerInputs = await validateProviderInputs(inputs); + + const revokeStatement = handlebars.compile(providerInputs.revocationStatement, { noEscape: true })({ + username + }); + + const queries = revokeStatement.trim().replaceAll("\n", "").split(";").filter(Boolean); + + const client = await $getClient(providerInputs); + const masterClient = await $getClient(providerInputs, true); + + // Get all processes for this login and kill them. If there are active connections to the database when drop login happens, it will throw an error. + const result = await masterClient.query<{ spid?: string }>(`sp_who '${username}'`); + + if (result && result.length > 0) { + for await (const row of result) { + if (row.spid) { + await masterClient.query(`KILL ${row.spid.trim()}`); + } + } + } + + for await (const query of queries) { + await (query.startsWith(SapCommands.DropLogin) ? masterClient : client).query(query); + } + + await masterClient.close(); + await client.close(); + + return { entityId: username }; + }; + + const renew = async (_: unknown, username: string) => { + // No need for renewal + return { entityId: username }; + }; + + return { + validateProviderInputs, + validateConnection, + create, + revoke, + renew + }; +}; diff --git a/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts b/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts index d120cf4fe..1ad24473c 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts @@ -11,6 +11,7 @@ import { z } from "zod"; import { BadRequestError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { DynamicSecretSapHanaSchema, TDynamicProviderFns } from "./models"; @@ -28,13 +29,24 @@ export const SapHanaProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretSapHanaSchema.parseAsync(inputs); - verifyHostInputValidity(providerInputs.host); - return providerInputs; + const [hostIp] = await verifyHostInputValidity(providerInputs.host); + validateHandlebarTemplate("SAP Hana creation", providerInputs.creationStatement, { + allowedExpressions: (val) => ["username", "password", "expiration"].includes(val) + }); + if (providerInputs.renewStatement) { + validateHandlebarTemplate("SAP Hana renew", providerInputs.renewStatement, { + allowedExpressions: (val) => ["username", "expiration"].includes(val) + }); + } + validateHandlebarTemplate("SAP Hana revoke", providerInputs.revocationStatement, { + allowedExpressions: (val) => ["username"].includes(val) + }); + return { ...providerInputs, hostIp }; }; - const getClient = async (providerInputs: z.infer) => { + const $getClient = async (providerInputs: z.infer & { hostIp: string }) => { const client = hdb.createClient({ - host: providerInputs.host, + host: providerInputs.hostIp, port: providerInputs.port, user: providerInputs.username, password: providerInputs.password, @@ -64,9 +76,9 @@ export const SapHanaProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); - const testResult: boolean = await new Promise((resolve, reject) => { + const testResult = await new Promise((resolve, reject) => { client.exec("SELECT 1 FROM DUMMY;", (err: any) => { if (err) { reject(); @@ -86,7 +98,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => { const password = generatePassword(); const expiration = new Date(expireAt).toISOString(); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({ username, password, @@ -114,7 +126,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => { const revoke = async (inputs: unknown, username: string) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username }); const queries = revokeStatement.toString().split(";").filter(Boolean); for await (const query of queries) { @@ -135,13 +147,15 @@ export const SapHanaProvider = (): TDynamicProviderFns => { return { entityId: username }; }; - const renew = async (inputs: unknown, username: string, expireAt: number) => { + const renew = async (inputs: unknown, entityId: string, expireAt: number) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + if (!providerInputs.renewStatement) return { entityId }; + + const client = await $getClient(providerInputs); try { const expiration = new Date(expireAt).toISOString(); - const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration }); + const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username: entityId, expiration }); const queries = renewStatement.toString().split(";").filter(Boolean); for await (const query of queries) { await new Promise((resolve, reject) => { @@ -161,7 +175,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => { client.disconnect(); } - return { entityId: username }; + return { entityId }; }; return { diff --git a/backend/src/ee/services/dynamic-secret/providers/snowflake.ts b/backend/src/ee/services/dynamic-secret/providers/snowflake.ts index 27ac3f49c..bea7eca89 100644 --- a/backend/src/ee/services/dynamic-secret/providers/snowflake.ts +++ b/backend/src/ee/services/dynamic-secret/providers/snowflake.ts @@ -5,6 +5,7 @@ import { z } from "zod"; import { BadRequestError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { DynamicSecretSnowflakeSchema, TDynamicProviderFns } from "./models"; @@ -12,7 +13,7 @@ import { DynamicSecretSnowflakeSchema, TDynamicProviderFns } from "./models"; const noop = () => {}; const generatePassword = (size = 48) => { - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#"; + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*"; return customAlphabet(charset, 48)(size); }; @@ -31,10 +32,22 @@ const getDaysToExpiry = (expiryDate: Date) => { export const SnowflakeProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretSnowflakeSchema.parseAsync(inputs); + validateHandlebarTemplate("Snowflake creation", providerInputs.creationStatement, { + allowedExpressions: (val) => ["username", "password", "expiration"].includes(val) + }); + if (providerInputs.renewStatement) { + validateHandlebarTemplate("Snowflake renew", providerInputs.renewStatement, { + allowedExpressions: (val) => ["username", "expiration"].includes(val) + }); + } + validateHandlebarTemplate("Snowflake revoke", providerInputs.revocationStatement, { + allowedExpressions: (val) => ["username"].includes(val) + }); + return providerInputs; }; - const getClient = async (providerInputs: z.infer) => { + const $getClient = async (providerInputs: z.infer) => { const client = snowflake.createConnection({ account: `${providerInputs.orgId}-${providerInputs.accountId}`, username: providerInputs.username, @@ -49,7 +62,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); let isValidConnection: boolean; @@ -72,7 +85,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => { const create = async (inputs: unknown, expireAt: number) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const username = generateUsername(); const password = generatePassword(); @@ -107,7 +120,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => { const revoke = async (inputs: unknown, username: string) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); try { const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username }); @@ -131,17 +144,16 @@ export const SnowflakeProvider = (): TDynamicProviderFns => { return { entityId: username }; }; - const renew = async (inputs: unknown, username: string, expireAt: number) => { + const renew = async (inputs: unknown, entityId: string, expireAt: number) => { const providerInputs = await validateProviderInputs(inputs); + if (!providerInputs.renewStatement) return { entityId }; - if (!providerInputs.renewStatement) return { entityId: username }; - - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); try { const expiration = getDaysToExpiry(new Date(expireAt)); const renewStatement = handlebars.compile(providerInputs.renewStatement)({ - username, + username: entityId, expiration }); @@ -161,7 +173,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => { client.destroy(noop); } - return { entityId: username }; + return { entityId }; }; return { diff --git a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts index 6acf23b06..178ca4ef9 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts @@ -1,21 +1,107 @@ +import { randomInt } from "crypto"; import handlebars from "handlebars"; import knex from "knex"; -import { customAlphabet } from "nanoid"; import { z } from "zod"; +import { withGatewayProxy } from "@app/lib/gateway"; import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; +import { TGatewayServiceFactory } from "../../gateway/gateway-service"; import { verifyHostInputValidity } from "../dynamic-secret-fns"; -import { DynamicSecretSqlDBSchema, SqlProviders, TDynamicProviderFns } from "./models"; +import { DynamicSecretSqlDBSchema, PasswordRequirements, SqlProviders, TDynamicProviderFns } from "./models"; const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000; -const generatePassword = (provider: SqlProviders) => { - // oracle has limit of 48 password length - const size = provider === SqlProviders.Oracle ? 30 : 48; +const DEFAULT_PASSWORD_REQUIREMENTS = { + length: 48, + required: { + lowercase: 1, + uppercase: 1, + digits: 1, + symbols: 0 + }, + allowedSymbols: "-_.~!*" +}; - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#"; - return customAlphabet(charset, 48)(size); +const ORACLE_PASSWORD_REQUIREMENTS = { + ...DEFAULT_PASSWORD_REQUIREMENTS, + length: 30 +}; + +const generatePassword = (provider: SqlProviders, requirements?: PasswordRequirements) => { + const defaultReqs = provider === SqlProviders.Oracle ? ORACLE_PASSWORD_REQUIREMENTS : DEFAULT_PASSWORD_REQUIREMENTS; + const finalReqs = requirements || defaultReqs; + + try { + const { length, required, allowedSymbols } = finalReqs; + + const chars = { + lowercase: "abcdefghijklmnopqrstuvwxyz", + uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + digits: "0123456789", + symbols: allowedSymbols || "-_.~!*" + }; + + const parts: string[] = []; + + if (required.lowercase > 0) { + parts.push( + ...Array(required.lowercase) + .fill(0) + .map(() => chars.lowercase[randomInt(chars.lowercase.length)]) + ); + } + + if (required.uppercase > 0) { + parts.push( + ...Array(required.uppercase) + .fill(0) + .map(() => chars.uppercase[randomInt(chars.uppercase.length)]) + ); + } + + if (required.digits > 0) { + parts.push( + ...Array(required.digits) + .fill(0) + .map(() => chars.digits[randomInt(chars.digits.length)]) + ); + } + + if (required.symbols > 0) { + parts.push( + ...Array(required.symbols) + .fill(0) + .map(() => chars.symbols[randomInt(chars.symbols.length)]) + ); + } + + const requiredTotal = Object.values(required).reduce((a, b) => a + b, 0); + const remainingLength = Math.max(length - requiredTotal, 0); + + const allowedChars = Object.entries(chars) + .filter(([key]) => required[key as keyof typeof required] > 0) + .map(([, value]) => value) + .join(""); + + parts.push( + ...Array(remainingLength) + .fill(0) + .map(() => allowedChars[randomInt(allowedChars.length)]) + ); + + // shuffle the array to mix up the characters + for (let i = parts.length - 1; i > 0; i -= 1) { + const j = randomInt(i + 1); + [parts[i], parts[j]] = [parts[j], parts[i]]; + } + + return parts.join(""); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : "Unknown error"; + throw new Error(`Failed to generate password: ${message}`); + } }; const generateUsername = (provider: SqlProviders) => { @@ -25,15 +111,34 @@ const generateUsername = (provider: SqlProviders) => { return alphaNumericNanoId(32); }; -export const SqlDatabaseProvider = (): TDynamicProviderFns => { +type TSqlDatabaseProviderDTO = { + gatewayService: Pick; +}; + +export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs); - verifyHostInputValidity(providerInputs.host); - return providerInputs; + + const [hostIp] = await verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.projectGatewayId)); + validateHandlebarTemplate("SQL creation", providerInputs.creationStatement, { + allowedExpressions: (val) => ["username", "password", "expiration", "database"].includes(val) + }); + if (providerInputs.renewStatement) { + validateHandlebarTemplate("SQL renew", providerInputs.renewStatement, { + allowedExpressions: (val) => ["username", "expiration", "database"].includes(val) + }); + } + validateHandlebarTemplate("SQL revoke", providerInputs.revocationStatement, { + allowedExpressions: (val) => ["username", "database"].includes(val) + }); + + return { ...providerInputs, hostIp }; }; - const getClient = async (providerInputs: z.infer) => { + const $getClient = async (providerInputs: z.infer) => { const ssl = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined; + const isMsSQLClient = providerInputs.client === SqlProviders.MsSQL; + const db = knex({ client: providerInputs.client, connection: { @@ -43,92 +148,165 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => { user: providerInputs.username, password: providerInputs.password, ssl, - pool: { min: 0, max: 1 } + // @ts-expect-error this is because of knexjs type signature issue. This is directly passed to driver + // https://github.com/knex/knex/blob/b6507a7129d2b9fafebf5f831494431e64c6a8a0/lib/dialects/mssql/index.js#L66 + // https://github.com/tediousjs/tedious/blob/ebb023ed90969a7ec0e4b036533ad52739d921f7/test/config.ci.ts#L19 + options: isMsSQLClient + ? { + trustServerCertificate: !providerInputs.ca, + cryptoCredentialsDetails: providerInputs.ca ? { ca: providerInputs.ca } : {} + } + : undefined }, - acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT + acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT, + pool: { min: 0, max: 7 } }); return db; }; + const gatewayProxyWrapper = async ( + providerInputs: z.infer, + gatewayCallback: (host: string, port: number) => Promise + ) => { + const relayDetails = await gatewayService.fnGetGatewayClientTls(providerInputs.projectGatewayId as string); + const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); + await withGatewayProxy( + async (port) => { + await gatewayCallback("localhost", port); + }, + { + targetHost: providerInputs.host, + targetPort: providerInputs.port, + relayHost, + relayPort: Number(relayPort), + identityId: relayDetails.identityId, + orgId: relayDetails.orgId, + tlsOptions: { + ca: relayDetails.certChain, + cert: relayDetails.certificate, + key: relayDetails.privateKey.toString() + } + } + ); + }; + const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const db = await getClient(providerInputs); - // oracle needs from keyword - const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1"; + let isConnected = false; + const gatewayCallback = async (host = providerInputs.hostIp, port = providerInputs.port) => { + const db = await $getClient({ ...providerInputs, port, host }); + // oracle needs from keyword + const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1"; - const isConnected = await db.raw(testStatement).then(() => true); - await db.destroy(); + isConnected = await db.raw(testStatement).then(() => true); + await db.destroy(); + }; + + if (providerInputs.projectGatewayId) { + await gatewayProxyWrapper(providerInputs, gatewayCallback); + } else { + await gatewayCallback(); + } return isConnected; }; const create = async (inputs: unknown, expireAt: number) => { const providerInputs = await validateProviderInputs(inputs); - const db = await getClient(providerInputs); - const username = generateUsername(providerInputs.client); - const password = generatePassword(providerInputs.client); - const { database } = providerInputs; - const expiration = new Date(expireAt).toISOString(); + const password = generatePassword(providerInputs.client, providerInputs.passwordRequirements); + const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => { + const db = await $getClient({ ...providerInputs, port, host }); + try { + const { database } = providerInputs; + const expiration = new Date(expireAt).toISOString(); - const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({ - username, - password, - expiration, - database - }); + const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({ + username, + password, + expiration, + database + }); - const queries = creationStatement.toString().split(";").filter(Boolean); - await db.transaction(async (tx) => { - for (const query of queries) { - // eslint-disable-next-line - await tx.raw(query); + const queries = creationStatement.toString().split(";").filter(Boolean); + await db.transaction(async (tx) => { + for (const query of queries) { + // eslint-disable-next-line + await tx.raw(query); + } + }); + } finally { + await db.destroy(); } - }); - await db.destroy(); + }; + if (providerInputs.projectGatewayId) { + await gatewayProxyWrapper(providerInputs, gatewayCallback); + } else { + await gatewayCallback(); + } return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } }; }; const revoke = async (inputs: unknown, entityId: string) => { const providerInputs = await validateProviderInputs(inputs); - const db = await getClient(providerInputs); - const username = entityId; const { database } = providerInputs; - - const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database }); - const queries = revokeStatement.toString().split(";").filter(Boolean); - await db.transaction(async (tx) => { - for (const query of queries) { - // eslint-disable-next-line - await tx.raw(query); + const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => { + const db = await $getClient({ ...providerInputs, port, host }); + try { + const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database }); + const queries = revokeStatement.toString().split(";").filter(Boolean); + await db.transaction(async (tx) => { + for (const query of queries) { + // eslint-disable-next-line + await tx.raw(query); + } + }); + } finally { + await db.destroy(); } - }); - - await db.destroy(); + }; + if (providerInputs.projectGatewayId) { + await gatewayProxyWrapper(providerInputs, gatewayCallback); + } else { + await gatewayCallback(); + } return { entityId: username }; }; const renew = async (inputs: unknown, entityId: string, expireAt: number) => { const providerInputs = await validateProviderInputs(inputs); - const db = await getClient(providerInputs); + if (!providerInputs.renewStatement) return { entityId }; - const username = entityId; - const expiration = new Date(expireAt).toISOString(); - const { database } = providerInputs; + const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => { + const db = await $getClient({ ...providerInputs, port, host }); + const expiration = new Date(expireAt).toISOString(); + const { database } = providerInputs; - const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration, database }); - if (renewStatement) { - const queries = renewStatement.toString().split(";").filter(Boolean); - await db.transaction(async (tx) => { - for (const query of queries) { - // eslint-disable-next-line - await tx.raw(query); - } + const renewStatement = handlebars.compile(providerInputs.renewStatement)({ + username: entityId, + expiration, + database }); + try { + if (renewStatement) { + const queries = renewStatement.toString().split(";").filter(Boolean); + await db.transaction(async (tx) => { + for (const query of queries) { + // eslint-disable-next-line + await tx.raw(query); + } + }); + } + } finally { + await db.destroy(); + } + }; + if (providerInputs.projectGatewayId) { + await gatewayProxyWrapper(providerInputs, gatewayCallback); + } else { + await gatewayCallback(); } - - await db.destroy(); - return { entityId: username }; + return { entityId }; }; return { diff --git a/backend/src/ee/services/dynamic-secret/providers/totp.ts b/backend/src/ee/services/dynamic-secret/providers/totp.ts new file mode 100644 index 000000000..d16b82306 --- /dev/null +++ b/backend/src/ee/services/dynamic-secret/providers/totp.ts @@ -0,0 +1,90 @@ +import { authenticator } from "otplib"; +import { HashAlgorithms } from "otplib/core"; + +import { alphaNumericNanoId } from "@app/lib/nanoid"; + +import { DynamicSecretTotpSchema, TDynamicProviderFns, TotpConfigType } from "./models"; + +export const TotpProvider = (): TDynamicProviderFns => { + const validateProviderInputs = async (inputs: unknown) => { + const providerInputs = await DynamicSecretTotpSchema.parseAsync(inputs); + + return providerInputs; + }; + + const validateConnection = async () => { + return true; + }; + + const create = async (inputs: unknown) => { + const providerInputs = await validateProviderInputs(inputs); + + const entityId = alphaNumericNanoId(32); + const authenticatorInstance = authenticator.clone(); + + let secret: string; + let period: number | null | undefined; + let digits: number | null | undefined; + let algorithm: HashAlgorithms | null | undefined; + + if (providerInputs.configType === TotpConfigType.URL) { + const urlObj = new URL(providerInputs.url); + secret = urlObj.searchParams.get("secret") as string; + const periodFromUrl = urlObj.searchParams.get("period"); + const digitsFromUrl = urlObj.searchParams.get("digits"); + const algorithmFromUrl = urlObj.searchParams.get("algorithm"); + + if (periodFromUrl) { + period = +periodFromUrl; + } + + if (digitsFromUrl) { + digits = +digitsFromUrl; + } + + if (algorithmFromUrl) { + algorithm = algorithmFromUrl.toLowerCase() as HashAlgorithms; + } + } else { + secret = providerInputs.secret; + period = providerInputs.period; + digits = providerInputs.digits; + algorithm = providerInputs.algorithm as unknown as HashAlgorithms; + } + + if (digits) { + authenticatorInstance.options = { digits }; + } + + if (algorithm) { + authenticatorInstance.options = { algorithm }; + } + + if (period) { + authenticatorInstance.options = { step: period }; + } + + return { + entityId, + data: { TOTP: authenticatorInstance.generate(secret), TIME_REMAINING: authenticatorInstance.timeRemaining() } + }; + }; + + const revoke = async (_inputs: unknown, entityId: string) => { + return { entityId }; + }; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const renew = async (_inputs: unknown, entityId: string) => { + // No renewal necessary + return { entityId }; + }; + + return { + validateProviderInputs, + validateConnection, + create, + revoke, + renew + }; +}; diff --git a/backend/src/ee/services/external-kms/external-kms-service.ts b/backend/src/ee/services/external-kms/external-kms-service.ts index c3b774afd..49ac293ed 100644 --- a/backend/src/ee/services/external-kms/external-kms-service.ts +++ b/backend/src/ee/services/external-kms/external-kms-service.ts @@ -1,11 +1,13 @@ +import { KMSServiceException } from "@aws-sdk/client-kms"; +import { STSServiceException } from "@aws-sdk/client-sts"; import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; -import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; -import { KmsDataKey } from "@app/services/kms/kms-types"; +import { KmsDataKey, KmsKeyUsage } from "@app/services/kms/kms-types"; import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; @@ -20,7 +22,8 @@ import { TUpdateExternalKmsDTO } from "./external-kms-types"; import { AwsKmsProviderFactory } from "./providers/aws-kms"; -import { ExternalKmsAwsSchema, KmsProviders } from "./providers/model"; +import { GcpKmsProviderFactory } from "./providers/gcp-kms"; +import { ExternalKmsAwsSchema, ExternalKmsGcpSchema, KmsProviders, TExternalKmsGcpSchema } from "./providers/model"; type TExternalKmsServiceFactoryDep = { externalKmsDAL: TExternalKmsDALFactory; @@ -70,7 +73,16 @@ export const externalKmsServiceFactory = ({ switch (provider.type) { case KmsProviders.Aws: { - const externalKms = await AwsKmsProviderFactory({ inputs: provider.inputs }); + const externalKms = await AwsKmsProviderFactory({ inputs: provider.inputs }).catch((error) => { + if (error instanceof STSServiceException || error instanceof KMSServiceException) { + throw new InternalServerError({ + message: error.message ? `AWS error: ${error.message}` : "" + }); + } + + throw error; + }); + // if missing kms key this generate a new kms key id and returns new provider input const newProviderInput = await externalKms.generateInputKmsKey(); sanitizedProviderInput = JSON.stringify(newProviderInput); @@ -78,6 +90,13 @@ export const externalKmsServiceFactory = ({ await externalKms.validateConnection(); } break; + case KmsProviders.Gcp: + { + const externalKms = await GcpKmsProviderFactory({ inputs: provider.inputs }); + await externalKms.validateConnection(); + sanitizedProviderInput = JSON.stringify(provider.inputs); + } + break; default: throw new BadRequestError({ message: "external kms provided is invalid" }); } @@ -88,7 +107,7 @@ export const externalKmsServiceFactory = ({ }); const { cipherTextBlob: encryptedProviderInputs } = orgDataKeyEncryptor({ - plainText: Buffer.from(sanitizedProviderInput, "utf8") + plainText: Buffer.from(sanitizedProviderInput) }); const externalKms = await externalKmsDAL.transaction(async (tx) => { @@ -96,6 +115,7 @@ export const externalKmsServiceFactory = ({ { isReserved: false, description, + keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT, name: kmsName, orgId: actorOrgId }, @@ -162,7 +182,7 @@ export const externalKmsServiceFactory = ({ case KmsProviders.Aws: { const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync( - JSON.parse(decryptedProviderInputBlob.toString("utf8")) + JSON.parse(decryptedProviderInputBlob.toString()) ); const updatedProviderInput = { ...decryptedProviderInput, ...provider.inputs }; const externalKms = await AwsKmsProviderFactory({ inputs: updatedProviderInput }); @@ -170,6 +190,17 @@ export const externalKmsServiceFactory = ({ sanitizedProviderInput = JSON.stringify(updatedProviderInput); } break; + case KmsProviders.Gcp: + { + const decryptedProviderInput = await ExternalKmsGcpSchema.parseAsync( + JSON.parse(decryptedProviderInputBlob.toString()) + ); + const updatedProviderInput = { ...decryptedProviderInput, ...provider.inputs }; + const externalKms = await GcpKmsProviderFactory({ inputs: updatedProviderInput }); + await externalKms.validateConnection(); + sanitizedProviderInput = JSON.stringify(updatedProviderInput); + } + break; default: throw new BadRequestError({ message: "external kms provided is invalid" }); } @@ -178,7 +209,7 @@ export const externalKmsServiceFactory = ({ let encryptedProviderInputs: Buffer | undefined; if (sanitizedProviderInput) { const { cipherTextBlob } = orgDataKeyEncryptor({ - plainText: Buffer.from(sanitizedProviderInput, "utf8") + plainText: Buffer.from(sanitizedProviderInput) }); encryptedProviderInputs = cipherTextBlob; } @@ -271,10 +302,17 @@ export const externalKmsServiceFactory = ({ switch (externalKmsDoc.provider) { case KmsProviders.Aws: { const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync( - JSON.parse(decryptedProviderInputBlob.toString("utf8")) + JSON.parse(decryptedProviderInputBlob.toString()) ); return { ...kmsDoc, external: { ...externalKmsDoc, providerInput: decryptedProviderInput } }; } + case KmsProviders.Gcp: { + const decryptedProviderInput = await ExternalKmsGcpSchema.parseAsync( + JSON.parse(decryptedProviderInputBlob.toString()) + ); + + return { ...kmsDoc, external: { ...externalKmsDoc, providerInput: decryptedProviderInput } }; + } default: throw new BadRequestError({ message: "external kms provided is invalid" }); } @@ -312,21 +350,34 @@ export const externalKmsServiceFactory = ({ switch (externalKmsDoc.provider) { case KmsProviders.Aws: { const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync( - JSON.parse(decryptedProviderInputBlob.toString("utf8")) + JSON.parse(decryptedProviderInputBlob.toString()) ); return { ...kmsDoc, external: { ...externalKmsDoc, providerInput: decryptedProviderInput } }; } + case KmsProviders.Gcp: { + const decryptedProviderInput = await ExternalKmsGcpSchema.parseAsync( + JSON.parse(decryptedProviderInputBlob.toString()) + ); + + return { ...kmsDoc, external: { ...externalKmsDoc, providerInput: decryptedProviderInput } }; + } default: throw new BadRequestError({ message: "external kms provided is invalid" }); } }; + const fetchGcpKeys = async ({ credential, gcpRegion }: Pick) => { + const externalKms = await GcpKmsProviderFactory({ inputs: { credential, gcpRegion, keyName: "" } }); + return externalKms.getKeysList(); + }; + return { create, updateById, deleteById, list, findById, - findByName + findByName, + fetchGcpKeys }; }; diff --git a/backend/src/ee/services/external-kms/providers/gcp-kms.ts b/backend/src/ee/services/external-kms/providers/gcp-kms.ts new file mode 100644 index 000000000..bee1eb24b --- /dev/null +++ b/backend/src/ee/services/external-kms/providers/gcp-kms.ts @@ -0,0 +1,113 @@ +import { KeyManagementServiceClient } from "@google-cloud/kms"; + +import { BadRequestError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; + +import { ExternalKmsGcpSchema, TExternalKmsGcpClientSchema, TExternalKmsProviderFns } from "./model"; + +const getGcpKmsClient = async ({ credential, gcpRegion }: TExternalKmsGcpClientSchema) => { + const gcpKmsClient = new KeyManagementServiceClient({ + credentials: credential + }); + const projectId = credential.project_id; + const locationName = gcpKmsClient.locationPath(projectId, gcpRegion); + + return { + gcpKmsClient, + locationName + }; +}; + +type GcpKmsProviderArgs = { + inputs: unknown; +}; +type TGcpKmsProviderFactoryReturn = TExternalKmsProviderFns & { + getKeysList: () => Promise<{ keys: string[] }>; +}; + +export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Promise => { + const { credential, gcpRegion, keyName } = await ExternalKmsGcpSchema.parseAsync(inputs); + const { gcpKmsClient, locationName } = await getGcpKmsClient({ + credential, + gcpRegion + }); + + const validateConnection = async () => { + try { + await gcpKmsClient.listKeyRings({ + parent: locationName + }); + return true; + } catch (error) { + throw new BadRequestError({ + message: "Cannot connect to GCP KMS" + }); + } + }; + + // Used when adding the KMS to fetch the list of keys in specified region + const getKeysList = async () => { + try { + const [keyRings] = await gcpKmsClient.listKeyRings({ + parent: locationName + }); + + const validKeyRings = keyRings + .filter( + (keyRing): keyRing is { name: string } => + keyRing !== null && typeof keyRing === "object" && "name" in keyRing && typeof keyRing.name === "string" + ) + .map((keyRing) => keyRing.name); + const keyList: string[] = []; + const keyListPromises = validKeyRings.map((keyRingName) => + gcpKmsClient + .listCryptoKeys({ + parent: keyRingName + }) + .then(([cryptoKeys]) => + cryptoKeys + .filter( + (key): key is { name: string } => + key !== null && typeof key === "object" && "name" in key && typeof key.name === "string" + ) + .map((key) => key.name) + ) + ); + + const cryptoKeyLists = await Promise.all(keyListPromises); + keyList.push(...cryptoKeyLists.flat()); + return { keys: keyList }; + } catch (error) { + logger.error(error, "Could not validate GCP KMS connection and credentials"); + throw new BadRequestError({ + message: "Could not validate GCP KMS connection and credentials", + error + }); + } + }; + + const encrypt = async (data: Buffer) => { + const encryptedText = await gcpKmsClient.encrypt({ + name: keyName, + plaintext: data + }); + if (!encryptedText[0].ciphertext) throw new Error("encryption failed"); + return { encryptedBlob: Buffer.from(encryptedText[0].ciphertext as Uint8Array) }; + }; + + const decrypt = async (encryptedBlob: Buffer) => { + const decryptedText = await gcpKmsClient.decrypt({ + name: keyName, + ciphertext: encryptedBlob + }); + if (!decryptedText[0].plaintext) throw new Error("decryption failed"); + return { data: Buffer.from(decryptedText[0].plaintext as Uint8Array) }; + }; + + return { + validateConnection, + getKeysList, + encrypt, + decrypt + }; +}; diff --git a/backend/src/ee/services/external-kms/providers/model.ts b/backend/src/ee/services/external-kms/providers/model.ts index 5a87e0c98..436b39423 100644 --- a/backend/src/ee/services/external-kms/providers/model.ts +++ b/backend/src/ee/services/external-kms/providers/model.ts @@ -1,13 +1,23 @@ import { z } from "zod"; export enum KmsProviders { - Aws = "aws" + Aws = "aws", + Gcp = "gcp" } export enum KmsAwsCredentialType { AssumeRole = "assume-role", AccessKey = "access-key" } +// Google uses snake_case for their enum values and we need to match that +export enum KmsGcpCredentialType { + ServiceAccount = "service_account" +} + +export enum KmsGcpKeyFetchAuthType { + Credential = "credential", + Kms = "kmsId" +} export const ExternalKmsAwsSchema = z.object({ credential: z @@ -42,14 +52,44 @@ export const ExternalKmsAwsSchema = z.object({ }); export type TExternalKmsAwsSchema = z.infer; +export const ExternalKmsGcpCredentialSchema = z.object({ + type: z.literal(KmsGcpCredentialType.ServiceAccount), + project_id: z.string().min(1), + private_key_id: z.string().min(1), + private_key: z.string().min(1), + client_email: z.string().min(1), + client_id: z.string().min(1), + auth_uri: z.string().min(1), + token_uri: z.string().min(1), + auth_provider_x509_cert_url: z.string().min(1), + client_x509_cert_url: z.string().min(1), + universe_domain: z.string().min(1) +}); + +export type TExternalKmsGcpCredentialSchema = z.infer; + +export const ExternalKmsGcpSchema = z.object({ + credential: ExternalKmsGcpCredentialSchema.describe("GCP Service Account JSON credential to connect"), + gcpRegion: z.string().trim().describe("GCP region where the KMS key is located"), + keyName: z.string().trim().describe("GCP key name") +}); +export type TExternalKmsGcpSchema = z.infer; + +const ExternalKmsGcpClientSchema = ExternalKmsGcpSchema.pick({ gcpRegion: true }).extend({ + credential: ExternalKmsGcpCredentialSchema +}); +export type TExternalKmsGcpClientSchema = z.infer; + // The root schema of the JSON export const ExternalKmsInputSchema = z.discriminatedUnion("type", [ - z.object({ type: z.literal(KmsProviders.Aws), inputs: ExternalKmsAwsSchema }) + z.object({ type: z.literal(KmsProviders.Aws), inputs: ExternalKmsAwsSchema }), + z.object({ type: z.literal(KmsProviders.Gcp), inputs: ExternalKmsGcpSchema }) ]); export type TExternalKmsInputSchema = z.infer; export const ExternalKmsInputUpdateSchema = z.discriminatedUnion("type", [ - z.object({ type: z.literal(KmsProviders.Aws), inputs: ExternalKmsAwsSchema.partial() }) + z.object({ type: z.literal(KmsProviders.Aws), inputs: ExternalKmsAwsSchema.partial() }), + z.object({ type: z.literal(KmsProviders.Gcp), inputs: ExternalKmsGcpSchema.partial() }) ]); export type TExternalKmsInputUpdateSchema = z.infer; diff --git a/backend/src/ee/services/gateway/gateway-dal.ts b/backend/src/ee/services/gateway/gateway-dal.ts new file mode 100644 index 000000000..fbf5558e4 --- /dev/null +++ b/backend/src/ee/services/gateway/gateway-dal.ts @@ -0,0 +1,86 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { GatewaysSchema, TableName, TGateways } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { + buildFindFilter, + ormify, + selectAllTableCols, + sqlNestRelationships, + TFindFilter, + TFindOpt +} from "@app/lib/knex"; + +export type TGatewayDALFactory = ReturnType; + +export const gatewayDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.Gateway); + + const find = async (filter: TFindFilter, { offset, limit, sort, tx }: TFindOpt = {}) => { + try { + const query = (tx || db)(TableName.Gateway) + // eslint-disable-next-line @typescript-eslint/no-misused-promises + .where(buildFindFilter(filter)) + .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`) + .leftJoin(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`) + .leftJoin(TableName.Project, `${TableName.Project}.id`, `${TableName.ProjectGateway}.projectId`) + .select(selectAllTableCols(TableName.Gateway)) + .select( + db.ref("name").withSchema(TableName.Identity).as("identityName"), + db.ref("name").withSchema(TableName.Project).as("projectName"), + db.ref("slug").withSchema(TableName.Project).as("projectSlug"), + db.ref("id").withSchema(TableName.Project).as("projectId") + ); + if (limit) void query.limit(limit); + if (offset) void query.offset(offset); + if (sort) { + void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); + } + + const docs = await query; + return sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (data) => ({ + ...GatewaysSchema.parse(data), + identity: { id: data.identityId, name: data.identityName } + }), + childrenMapper: [ + { + key: "projectId", + label: "projects" as const, + mapper: ({ projectId, projectName, projectSlug }) => ({ + id: projectId, + name: projectName, + slug: projectSlug + }) + } + ] + }); + } catch (error) { + throw new DatabaseError({ error, name: `${TableName.Gateway}: Find` }); + } + }; + + const findByProjectId = async (projectId: string, tx?: Knex) => { + try { + const query = (tx || db)(TableName.Gateway) + .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`) + .join(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`) + .select(selectAllTableCols(TableName.Gateway)) + .select( + db.ref("name").withSchema(TableName.Identity).as("identityName"), + db.ref("id").withSchema(TableName.ProjectGateway).as("projectGatewayId") + ) + .where({ [`${TableName.ProjectGateway}.projectId` as "projectId"]: projectId }); + + const docs = await query; + return docs.map((el) => ({ ...el, identity: { id: el.identityId, name: el.identityName } })); + } catch (error) { + throw new DatabaseError({ error, name: `${TableName.Gateway}: Find by project id` }); + } + }; + + return { ...orm, find, findByProjectId }; +}; diff --git a/backend/src/ee/services/gateway/gateway-service.ts b/backend/src/ee/services/gateway/gateway-service.ts new file mode 100644 index 000000000..5a17bc028 --- /dev/null +++ b/backend/src/ee/services/gateway/gateway-service.ts @@ -0,0 +1,652 @@ +import crypto from "node:crypto"; + +import { ForbiddenError } from "@casl/ability"; +import * as x509 from "@peculiar/x509"; +import { z } from "zod"; + +import { ActionProjectType } from "@app/db/schemas"; +import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { pingGatewayAndVerify } from "@app/lib/gateway"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { getTurnCredentials } from "@app/lib/turn/credentials"; +import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; +import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; +import { + createSerialNumber, + keyAlgorithmToAlgCfg +} from "@app/services/certificate-authority/certificate-authority-fns"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { TLicenseServiceFactory } from "../license/license-service"; +import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission"; +import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TGatewayDALFactory } from "./gateway-dal"; +import { + TExchangeAllocatedRelayAddressDTO, + TGetGatewayByIdDTO, + TGetProjectGatewayByIdDTO, + THeartBeatDTO, + TListGatewaysDTO, + TUpdateGatewayByIdDTO +} from "./gateway-types"; +import { TOrgGatewayConfigDALFactory } from "./org-gateway-config-dal"; +import { TProjectGatewayDALFactory } from "./project-gateway-dal"; + +type TGatewayServiceFactoryDep = { + gatewayDAL: TGatewayDALFactory; + projectGatewayDAL: TProjectGatewayDALFactory; + orgGatewayConfigDAL: Pick; + licenseService: Pick; + kmsService: Pick; + permissionService: Pick; + keyStore: Pick; +}; + +export type TGatewayServiceFactory = ReturnType; +const TURN_SERVER_CREDENTIALS_SCHEMA = z.object({ + username: z.string(), + password: z.string() +}); + +export const gatewayServiceFactory = ({ + gatewayDAL, + licenseService, + kmsService, + permissionService, + orgGatewayConfigDAL, + keyStore, + projectGatewayDAL +}: TGatewayServiceFactoryDep) => { + const $validateOrgAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => { + // if (!licenseService.onPremFeatures.gateway) { + // throw new BadRequestError({ + // message: + // "Gateway handshake failed due to instance plan restrictions. Please upgrade your instance to Infisical's Enterprise plan." + // }); + // } + const orgLicensePlan = await licenseService.getPlan(orgId); + if (!orgLicensePlan.gateway) { + throw new BadRequestError({ + message: + "Gateway handshake failed due to organization plan restrictions. Please upgrade your instance to Infisical's Enterprise plan." + }); + } + const { permission } = await permissionService.getOrgPermission( + ActorType.IDENTITY, + actorId, + orgId, + actorAuthMethod, + orgId + ); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionGatewayActions.CreateGateways, + OrgPermissionSubjects.Gateway + ); + }; + + const getGatewayRelayDetails = async (actorId: string, actorOrgId: string, actorAuthMethod: ActorAuthMethod) => { + const TURN_CRED_EXPIRY = 10 * 60; // 10 minutes + + const envCfg = getConfig(); + await $validateOrgAccessToGateway(actorOrgId, actorId, actorAuthMethod); + const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: actorOrgId + }); + + if (!envCfg.GATEWAY_RELAY_AUTH_SECRET || !envCfg.GATEWAY_RELAY_ADDRESS || !envCfg.GATEWAY_RELAY_REALM) { + throw new BadRequestError({ + message: "Gateway handshake failed due to missing instance configuration." + }); + } + + let turnServerUsername = ""; + let turnServerPassword = ""; + // keep it in redis for 5mins to avoid generating so many credentials + const previousCredential = await keyStore.getItem(KeyStorePrefixes.GatewayIdentityCredential(actorId)); + if (previousCredential) { + const el = await TURN_SERVER_CREDENTIALS_SCHEMA.parseAsync( + JSON.parse(decryptor({ cipherTextBlob: Buffer.from(previousCredential, "hex") }).toString()) + ); + turnServerUsername = el.username; + turnServerPassword = el.password; + } else { + const el = getTurnCredentials(actorId, envCfg.GATEWAY_RELAY_AUTH_SECRET); + await keyStore.setItemWithExpiry( + KeyStorePrefixes.GatewayIdentityCredential(actorId), + TURN_CRED_EXPIRY, + encryptor({ + plainText: Buffer.from(JSON.stringify({ username: el.username, password: el.password })) + }).cipherTextBlob.toString("hex") + ); + turnServerUsername = el.username; + turnServerPassword = el.password; + } + + return { + turnServerUsername, + turnServerPassword, + turnServerRealm: envCfg.GATEWAY_RELAY_REALM, + turnServerAddress: envCfg.GATEWAY_RELAY_ADDRESS, + infisicalStaticIp: envCfg.GATEWAY_INFISICAL_STATIC_IP_ADDRESS + }; + }; + + const exchangeAllocatedRelayAddress = async ({ + identityId, + identityOrg, + relayAddress, + identityOrgAuthMethod + }: TExchangeAllocatedRelayAddressDTO) => { + await $validateOrgAccessToGateway(identityOrg, identityId, identityOrgAuthMethod); + const { encryptor: orgKmsEncryptor, decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityOrg + }); + + const orgGatewayConfig = await orgGatewayConfigDAL.transaction(async (tx) => { + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.OrgGatewayRootCaInit(identityOrg)]); + const existingGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: identityOrg }); + if (existingGatewayConfig) return existingGatewayConfig; + + const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); + // generate root CA + const rootCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const rootCaSerialNumber = createSerialNumber(); + const rootCaSkObj = crypto.KeyObject.from(rootCaKeys.privateKey); + const rootCaIssuedAt = new Date(); + const rootCaKeyAlgorithm = CertKeyAlgorithm.RSA_2048; + const rootCaExpiration = new Date(new Date().setFullYear(2045)); + const rootCaCert = await x509.X509CertificateGenerator.createSelfSigned({ + name: `O=${identityOrg},CN=Infisical Gateway Root CA`, + serialNumber: rootCaSerialNumber, + notBefore: rootCaIssuedAt, + notAfter: rootCaExpiration, + signingAlgorithm: alg, + keys: rootCaKeys, + extensions: [ + // eslint-disable-next-line no-bitwise + new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true), + await x509.SubjectKeyIdentifierExtension.create(rootCaKeys.publicKey) + ] + }); + + // generate client ca + const clientCaSerialNumber = createSerialNumber(); + const clientCaIssuedAt = new Date(); + const clientCaExpiration = new Date(new Date().setFullYear(2045)); + const clientCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const clientCaSkObj = crypto.KeyObject.from(clientCaKeys.privateKey); + + const clientCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: clientCaSerialNumber, + subject: `O=${identityOrg},CN=Client Intermediate CA`, + issuer: rootCaCert.subject, + notBefore: clientCaIssuedAt, + notAfter: clientCaExpiration, + signingKey: rootCaKeys.privateKey, + publicKey: clientCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 0, true), + await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(clientCaKeys.publicKey) + ] + }); + + const clientKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const clientCertSerialNumber = createSerialNumber(); + const clientCert = await x509.X509CertificateGenerator.create({ + serialNumber: clientCertSerialNumber, + subject: `O=${identityOrg},OU=gateway-client,CN=cloud`, + issuer: clientCaCert.subject, + notAfter: clientCaExpiration, + notBefore: clientCaIssuedAt, + signingKey: clientCaKeys.privateKey, + publicKey: clientKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.BasicConstraintsExtension(false), + await x509.AuthorityKeyIdentifierExtension.create(clientCaCert, 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) + ] + }); + const clientSkObj = crypto.KeyObject.from(clientKeys.privateKey); + + // generate gateway ca + const gatewayCaSerialNumber = createSerialNumber(); + const gatewayCaIssuedAt = new Date(); + const gatewayCaExpiration = new Date(new Date().setFullYear(2045)); + const gatewayCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const gatewayCaSkObj = crypto.KeyObject.from(gatewayCaKeys.privateKey); + const gatewayCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: gatewayCaSerialNumber, + subject: `O=${identityOrg},CN=Gateway CA`, + issuer: rootCaCert.subject, + notBefore: gatewayCaIssuedAt, + notAfter: gatewayCaExpiration, + signingKey: rootCaKeys.privateKey, + publicKey: gatewayCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 0, true), + await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(gatewayCaKeys.publicKey) + ] + }); + + return orgGatewayConfigDAL.create({ + orgId: identityOrg, + rootCaIssuedAt, + rootCaExpiration, + rootCaSerialNumber, + rootCaKeyAlgorithm, + encryptedRootCaPrivateKey: orgKmsEncryptor({ + plainText: rootCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + }).cipherTextBlob, + encryptedRootCaCertificate: orgKmsEncryptor({ plainText: Buffer.from(rootCaCert.rawData) }).cipherTextBlob, + + clientCaIssuedAt, + clientCaExpiration, + clientCaSerialNumber, + encryptedClientCaPrivateKey: orgKmsEncryptor({ + plainText: clientCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + }).cipherTextBlob, + encryptedClientCaCertificate: orgKmsEncryptor({ + plainText: Buffer.from(clientCaCert.rawData) + }).cipherTextBlob, + + clientCertIssuedAt: clientCaIssuedAt, + clientCertExpiration: clientCaExpiration, + clientCertKeyAlgorithm: CertKeyAlgorithm.RSA_2048, + clientCertSerialNumber, + encryptedClientPrivateKey: orgKmsEncryptor({ + plainText: clientSkObj.export({ + type: "pkcs8", + format: "der" + }) + }).cipherTextBlob, + encryptedClientCertificate: orgKmsEncryptor({ + plainText: Buffer.from(clientCert.rawData) + }).cipherTextBlob, + + gatewayCaIssuedAt, + gatewayCaExpiration, + gatewayCaSerialNumber, + encryptedGatewayCaPrivateKey: orgKmsEncryptor({ + plainText: gatewayCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + }).cipherTextBlob, + encryptedGatewayCaCertificate: orgKmsEncryptor({ + plainText: Buffer.from(gatewayCaCert.rawData) + }).cipherTextBlob + }); + }); + + const rootCaCert = new x509.X509Certificate( + orgKmsDecryptor({ + cipherTextBlob: orgGatewayConfig.encryptedRootCaCertificate + }) + ); + const clientCaCert = new x509.X509Certificate( + orgKmsDecryptor({ + cipherTextBlob: orgGatewayConfig.encryptedClientCaCertificate + }) + ); + + const gatewayCaAlg = keyAlgorithmToAlgCfg(orgGatewayConfig.rootCaKeyAlgorithm as CertKeyAlgorithm); + const gatewayCaSkObj = crypto.createPrivateKey({ + key: orgKmsDecryptor({ cipherTextBlob: orgGatewayConfig.encryptedGatewayCaPrivateKey }), + format: "der", + type: "pkcs8" + }); + const gatewayCaCert = new x509.X509Certificate( + orgKmsDecryptor({ + cipherTextBlob: orgGatewayConfig.encryptedGatewayCaCertificate + }) + ); + + const gatewayCaPrivateKey = await crypto.subtle.importKey( + "pkcs8", + gatewayCaSkObj.export({ format: "der", type: "pkcs8" }), + gatewayCaAlg, + true, + ["sign"] + ); + + const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); + const gatewayKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const certIssuedAt = new Date(); + // then need to periodically init + const certExpireAt = new Date(new Date().setMonth(new Date().getMonth() + 1)); + + const extensions: x509.Extension[] = [ + new x509.BasicConstraintsExtension(false), + await x509.AuthorityKeyIdentifierExtension.create(gatewayCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(gatewayKeys.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], + true + ), + new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true), + // san + new x509.SubjectAlternativeNameExtension([{ type: "ip", value: relayAddress.split(":")[0] }], false) + ]; + + const serialNumber = createSerialNumber(); + const privateKey = crypto.KeyObject.from(gatewayKeys.privateKey); + const gatewayCertificate = await x509.X509CertificateGenerator.create({ + serialNumber, + subject: `CN=${identityId},O=${identityOrg},OU=Gateway`, + issuer: gatewayCaCert.subject, + notBefore: certIssuedAt, + notAfter: certExpireAt, + signingKey: gatewayCaPrivateKey, + publicKey: gatewayKeys.publicKey, + signingAlgorithm: alg, + extensions + }); + + const appCfg = getConfig(); + // just for local development + const formatedRelayAddress = + appCfg.NODE_ENV === "development" ? relayAddress.replace("127.0.0.1", "host.docker.internal") : relayAddress; + + await gatewayDAL.transaction(async (tx) => { + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.OrgGatewayCertExchange(identityOrg)]); + const existingGateway = await gatewayDAL.findOne({ identityId, orgGatewayRootCaId: orgGatewayConfig.id }); + + if (existingGateway) { + return gatewayDAL.updateById(existingGateway.id, { + keyAlgorithm: CertKeyAlgorithm.RSA_2048, + issuedAt: certIssuedAt, + expiration: certExpireAt, + serialNumber, + relayAddress: orgKmsEncryptor({ + plainText: Buffer.from(formatedRelayAddress) + }).cipherTextBlob + }); + } + + return gatewayDAL.create({ + keyAlgorithm: CertKeyAlgorithm.RSA_2048, + issuedAt: certIssuedAt, + expiration: certExpireAt, + serialNumber, + relayAddress: orgKmsEncryptor({ + plainText: Buffer.from(formatedRelayAddress) + }).cipherTextBlob, + identityId, + orgGatewayRootCaId: orgGatewayConfig.id, + name: `gateway-${alphaNumericNanoId(6).toLowerCase()}` + }); + }); + + const gatewayCertificateChain = `${clientCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim(); + + return { + serialNumber, + privateKey: privateKey.export({ format: "pem", type: "pkcs8" }) as string, + certificate: gatewayCertificate.toString("pem"), + certificateChain: gatewayCertificateChain + }; + }; + + const heartbeat = async ({ orgPermission }: THeartBeatDTO) => { + await $validateOrgAccessToGateway(orgPermission.orgId, orgPermission.id, orgPermission.authMethod); + const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId }); + if (!orgGatewayConfig) throw new NotFoundError({ message: `Identity with ID ${orgPermission.id} not found.` }); + + const [gateway] = await gatewayDAL.find({ identityId: orgPermission.id, orgGatewayRootCaId: orgGatewayConfig.id }); + if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${orgPermission.id} not found.` }); + + const { decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: orgGatewayConfig.orgId + }); + + const rootCaCert = new x509.X509Certificate( + orgKmsDecryptor({ + cipherTextBlob: orgGatewayConfig.encryptedRootCaCertificate + }) + ); + const gatewayCaCert = new x509.X509Certificate( + orgKmsDecryptor({ + cipherTextBlob: orgGatewayConfig.encryptedGatewayCaCertificate + }) + ); + const clientCert = new x509.X509Certificate( + orgKmsDecryptor({ + cipherTextBlob: orgGatewayConfig.encryptedClientCertificate + }) + ); + + const privateKey = crypto + .createPrivateKey({ + key: orgKmsDecryptor({ cipherTextBlob: orgGatewayConfig.encryptedClientPrivateKey }), + format: "der", + type: "pkcs8" + }) + .export({ type: "pkcs8", format: "pem" }); + + const relayAddress = orgKmsDecryptor({ cipherTextBlob: gateway.relayAddress }).toString(); + const [relayHost, relayPort] = relayAddress.split(":"); + + await pingGatewayAndVerify({ + relayHost, + relayPort: Number(relayPort), + tlsOptions: { + key: privateKey.toString(), + ca: `${gatewayCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim(), + cert: clientCert.toString("pem") + }, + identityId: orgPermission.id, + orgId: orgPermission.orgId + }); + + await gatewayDAL.updateById(gateway.id, { heartbeat: new Date() }); + }; + + const listGateways = async ({ orgPermission }: TListGatewaysDTO) => { + const { permission } = await permissionService.getOrgPermission( + orgPermission.type, + orgPermission.id, + orgPermission.orgId, + orgPermission.authMethod, + orgPermission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionGatewayActions.ListGateways, + OrgPermissionSubjects.Gateway + ); + const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId }); + if (!orgGatewayConfig) return []; + + const gateways = await gatewayDAL.find({ + orgGatewayRootCaId: orgGatewayConfig.id + }); + return gateways; + }; + + const getGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => { + const { permission } = await permissionService.getOrgPermission( + orgPermission.type, + orgPermission.id, + orgPermission.orgId, + orgPermission.authMethod, + orgPermission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionGatewayActions.ListGateways, + OrgPermissionSubjects.Gateway + ); + const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId }); + if (!orgGatewayConfig) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` }); + + const [gateway] = await gatewayDAL.find({ id, orgGatewayRootCaId: orgGatewayConfig.id }); + if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` }); + return gateway; + }; + + const updateGatewayById = async ({ orgPermission, id, name, projectIds }: TUpdateGatewayByIdDTO) => { + const { permission } = await permissionService.getOrgPermission( + orgPermission.type, + orgPermission.id, + orgPermission.orgId, + orgPermission.authMethod, + orgPermission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionGatewayActions.EditGateways, + OrgPermissionSubjects.Gateway + ); + const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId }); + if (!orgGatewayConfig) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` }); + + const [gateway] = await gatewayDAL.update({ id, orgGatewayRootCaId: orgGatewayConfig.id }, { name }); + if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` }); + if (projectIds) { + await projectGatewayDAL.transaction(async (tx) => { + await projectGatewayDAL.delete({ gatewayId: gateway.id }, tx); + await projectGatewayDAL.insertMany( + projectIds.map((el) => ({ gatewayId: gateway.id, projectId: el })), + tx + ); + }); + } + + return gateway; + }; + + const deleteGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => { + const { permission } = await permissionService.getOrgPermission( + orgPermission.type, + orgPermission.id, + orgPermission.orgId, + orgPermission.authMethod, + orgPermission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionGatewayActions.DeleteGateways, + OrgPermissionSubjects.Gateway + ); + const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId }); + if (!orgGatewayConfig) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` }); + + const [gateway] = await gatewayDAL.delete({ id, orgGatewayRootCaId: orgGatewayConfig.id }); + if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` }); + return gateway; + }; + + const getProjectGateways = async ({ projectId, projectPermission }: TGetProjectGatewayByIdDTO) => { + await permissionService.getProjectPermission({ + projectId, + actor: projectPermission.type, + actorId: projectPermission.id, + actorOrgId: projectPermission.orgId, + actorAuthMethod: projectPermission.authMethod, + actionProjectType: ActionProjectType.Any + }); + + const gateways = await gatewayDAL.findByProjectId(projectId); + return gateways; + }; + + // this has no permission check and used for dynamic secrets directly + // assumes permission check is already done + const fnGetGatewayClientTls = async (projectGatewayId: string) => { + const projectGateway = await projectGatewayDAL.findById(projectGatewayId); + if (!projectGateway) throw new NotFoundError({ message: `Project gateway with ID ${projectGatewayId} not found.` }); + + const { gatewayId } = projectGateway; + const gateway = await gatewayDAL.findById(gatewayId); + if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found.` }); + + const orgGatewayConfig = await orgGatewayConfigDAL.findById(gateway.orgGatewayRootCaId); + const { decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: orgGatewayConfig.orgId + }); + + const rootCaCert = new x509.X509Certificate( + orgKmsDecryptor({ + cipherTextBlob: orgGatewayConfig.encryptedRootCaCertificate + }) + ); + const gatewayCaCert = new x509.X509Certificate( + orgKmsDecryptor({ + cipherTextBlob: orgGatewayConfig.encryptedGatewayCaCertificate + }) + ); + const clientCert = new x509.X509Certificate( + orgKmsDecryptor({ + cipherTextBlob: orgGatewayConfig.encryptedClientCertificate + }) + ); + + const clientSkObj = crypto.createPrivateKey({ + key: orgKmsDecryptor({ cipherTextBlob: orgGatewayConfig.encryptedClientPrivateKey }), + format: "der", + type: "pkcs8" + }); + + return { + relayAddress: orgKmsDecryptor({ cipherTextBlob: gateway.relayAddress }).toString(), + privateKey: clientSkObj.export({ type: "pkcs8", format: "pem" }), + certificate: clientCert.toString("pem"), + certChain: `${gatewayCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim(), + identityId: gateway.identityId, + orgId: orgGatewayConfig.orgId + }; + }; + + return { + getGatewayRelayDetails, + exchangeAllocatedRelayAddress, + listGateways, + getGatewayById, + updateGatewayById, + deleteGatewayById, + getProjectGateways, + fnGetGatewayClientTls, + heartbeat + }; +}; diff --git a/backend/src/ee/services/gateway/gateway-types.ts b/backend/src/ee/services/gateway/gateway-types.ts new file mode 100644 index 000000000..220dc7147 --- /dev/null +++ b/backend/src/ee/services/gateway/gateway-types.ts @@ -0,0 +1,39 @@ +import { OrgServiceActor } from "@app/lib/types"; +import { ActorAuthMethod } from "@app/services/auth/auth-type"; + +export type TExchangeAllocatedRelayAddressDTO = { + identityId: string; + identityOrg: string; + identityOrgAuthMethod: ActorAuthMethod; + relayAddress: string; +}; + +export type TListGatewaysDTO = { + orgPermission: OrgServiceActor; +}; + +export type TGetGatewayByIdDTO = { + id: string; + orgPermission: OrgServiceActor; +}; + +export type TUpdateGatewayByIdDTO = { + id: string; + name?: string; + projectIds?: string[]; + orgPermission: OrgServiceActor; +}; + +export type TDeleteGatewayByIdDTO = { + id: string; + orgPermission: OrgServiceActor; +}; + +export type TGetProjectGatewayByIdDTO = { + projectId: string; + projectPermission: OrgServiceActor; +}; + +export type THeartBeatDTO = { + orgPermission: OrgServiceActor; +}; diff --git a/backend/src/ee/services/gateway/org-gateway-config-dal.ts b/backend/src/ee/services/gateway/org-gateway-config-dal.ts new file mode 100644 index 000000000..9d4f9384a --- /dev/null +++ b/backend/src/ee/services/gateway/org-gateway-config-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TOrgGatewayConfigDALFactory = ReturnType; + +export const orgGatewayConfigDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.OrgGatewayConfig); + return orm; +}; diff --git a/backend/src/ee/services/gateway/project-gateway-dal.ts b/backend/src/ee/services/gateway/project-gateway-dal.ts new file mode 100644 index 000000000..44c36f5f6 --- /dev/null +++ b/backend/src/ee/services/gateway/project-gateway-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TProjectGatewayDALFactory = ReturnType; + +export const projectGatewayDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.ProjectGateway); + return orm; +}; diff --git a/backend/src/ee/services/group/group-dal.ts b/backend/src/ee/services/group/group-dal.ts index 5e25f6113..59f82c05d 100644 --- a/backend/src/ee/services/group/group-dal.ts +++ b/backend/src/ee/services/group/group-dal.ts @@ -5,6 +5,8 @@ import { TableName, TGroups } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex"; +import { EFilterReturnedUsers } from "./group-types"; + export type TGroupDALFactory = ReturnType; export const groupDALFactory = (db: TDbClient) => { @@ -66,7 +68,8 @@ export const groupDALFactory = (db: TDbClient) => { offset = 0, limit, username, // depreciated in favor of search - search + search, + filter }: { orgId: string; groupId: string; @@ -74,6 +77,7 @@ export const groupDALFactory = (db: TDbClient) => { limit?: number; username?: string; search?: string; + filter?: EFilterReturnedUsers; }) => { try { const query = db @@ -90,6 +94,7 @@ export const groupDALFactory = (db: TDbClient) => { .select( db.ref("id").withSchema(TableName.OrgMembership), db.ref("groupId").withSchema(TableName.UserGroupMembership), + db.ref("createdAt").withSchema(TableName.UserGroupMembership).as("joinedGroupAt"), db.ref("email").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users), db.ref("firstName").withSchema(TableName.Users), @@ -106,22 +111,42 @@ export const groupDALFactory = (db: TDbClient) => { } if (search) { - void query.andWhereRaw(`CONCAT_WS(' ', "firstName", "lastName", "username") ilike '%${search}%'`); + void query.andWhereRaw(`CONCAT_WS(' ', "firstName", "lastName", "username") ilike ?`, [`%${search}%`]); } else if (username) { void query.andWhere(`${TableName.Users}.username`, "ilike", `%${username}%`); } + switch (filter) { + case EFilterReturnedUsers.EXISTING_MEMBERS: + void query.andWhere(`${TableName.UserGroupMembership}.createdAt`, "is not", null); + break; + case EFilterReturnedUsers.NON_MEMBERS: + void query.andWhere(`${TableName.UserGroupMembership}.createdAt`, "is", null); + break; + default: + break; + } + const members = await query; return { members: members.map( - ({ email, username: memberUsername, firstName, lastName, userId, groupId: memberGroupId }) => ({ + ({ + email, + username: memberUsername, + firstName, + lastName, + userId, + groupId: memberGroupId, + joinedGroupAt + }) => ({ id: userId, email, username: memberUsername, firstName, lastName, - isPartOfGroup: !!memberGroupId + isPartOfGroup: !!memberGroupId, + joinedGroupAt }) ), // @ts-expect-error col select is raw and not strongly typed diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts index 7e7139a6b..b9206771e 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -2,8 +2,8 @@ import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; -import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; +import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; @@ -13,7 +13,8 @@ import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal import { TUserDALFactory } from "@app/services/user/user-dal"; import { TLicenseServiceFactory } from "../license/license-service"; -import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; +import { OrgPermissionGroupActions, OrgPermissionSubjects } from "../permission/org-permission"; +import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { TGroupDALFactory } from "./group-dal"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "./group-fns"; @@ -32,7 +33,7 @@ type TGroupServiceFactoryDep = { userDAL: Pick; groupDAL: Pick< TGroupDALFactory, - "create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById" + "create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById" | "transaction" >; groupProjectDAL: Pick; orgDAL: Pick; @@ -45,6 +46,7 @@ type TGroupServiceFactoryDep = { projectKeyDAL: Pick; permissionService: Pick; licenseService: Pick; + oidcConfigDAL: Pick; }; export type TGroupServiceFactory = ReturnType; @@ -59,19 +61,20 @@ export const groupServiceFactory = ({ projectBotDAL, projectKeyDAL, permissionService, - licenseService + licenseService, + oidcConfigDAL }: TGroupServiceFactoryDep) => { const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Groups); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Create, OrgPermissionSubjects.Groups); const plan = await licenseService.getPlan(actorOrgId); if (!plan.groups) @@ -84,16 +87,47 @@ export const groupServiceFactory = ({ actorOrgId ); const isCustomRole = Boolean(customRole); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission); - if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" }); + if (role !== OrgMembershipRole.NoAccess) { + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + OrgPermissionGroupActions.GrantPrivileges, + OrgPermissionSubjects.Groups, + permission, + rolePermission + ); - const group = await groupDAL.create({ - name, - slug: slug || slugify(`${name}-${alphaNumericNanoId(4)}`), - orgId: actorOrgId, - role: isCustomRole ? OrgMembershipRole.Custom : role, - roleId: customRole?.id + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to create group", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionGroupActions.GrantPrivileges, + OrgPermissionSubjects.Groups + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + + const group = await groupDAL.transaction(async (tx) => { + const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name }, tx); + if (existingGroup) { + throw new BadRequestError({ + message: `Failed to create group with name '${name}'. Group with the same name already exists` + }); + } + + const newGroup = await groupDAL.create( + { + name, + slug: slug || slugify(`${name}-${alphaNumericNanoId(4)}`), + orgId: actorOrgId, + role: isCustomRole ? OrgMembershipRole.Custom : role, + roleId: customRole?.id + }, + tx + ); + + return newGroup; }); return group; @@ -111,14 +145,15 @@ export const groupServiceFactory = ({ }: TUpdateGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); const plan = await licenseService.getPlan(actorOrgId); if (!plan.groups) @@ -139,27 +174,56 @@ export const groupServiceFactory = ({ ); const isCustomRole = Boolean(customOrgRole); - const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission); - if (!hasRequiredNewRolePermission) - throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" }); + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + OrgPermissionGroupActions.GrantPrivileges, + OrgPermissionSubjects.Groups, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update group", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionGroupActions.GrantPrivileges, + OrgPermissionSubjects.Groups + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); if (isCustomRole) customRole = customOrgRole; } - const [updatedGroup] = await groupDAL.update( - { - id: group.id - }, - { - name, - slug: slug ? slugify(slug) : undefined, - ...(role - ? { - role: customRole ? OrgMembershipRole.Custom : role, - roleId: customRole?.id ?? null - } - : {}) + const updatedGroup = await groupDAL.transaction(async (tx) => { + if (name) { + const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name }, tx); + + if (existingGroup && existingGroup.id !== id) { + throw new BadRequestError({ + message: `Failed to update group with name '${name}'. Group with the same name already exists` + }); + } } - ); + + const [updated] = await groupDAL.update( + { + id: group.id + }, + { + name, + slug: slug ? slugify(slug) : undefined, + ...(role + ? { + role: customRole ? OrgMembershipRole.Custom : role, + roleId: customRole?.id ?? null + } + : {}) + }, + tx + ); + + return updated; + }); return updatedGroup; }; @@ -174,7 +238,7 @@ export const groupServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Groups); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Delete, OrgPermissionSubjects.Groups); const plan = await licenseService.getPlan(actorOrgId); @@ -201,7 +265,7 @@ export const groupServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); const group = await groupDAL.findById(id); if (!group) { @@ -222,7 +286,8 @@ export const groupServiceFactory = ({ actorId, actorAuthMethod, actorOrgId, - search + search, + filter }: TListGroupUsersDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); @@ -233,7 +298,7 @@ export const groupServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); const group = await groupDAL.findOne({ orgId: actorOrgId, @@ -251,7 +316,8 @@ export const groupServiceFactory = ({ offset, limit, username, - search + search, + filter }); return { users: members, totalCount }; @@ -260,14 +326,14 @@ export const groupServiceFactory = ({ const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); // check if group with slug exists const group = await groupDAL.findOne({ @@ -280,12 +346,39 @@ export const groupServiceFactory = ({ message: `Failed to find group with ID ${id}` }); + const oidcConfig = await oidcConfigDAL.findOne({ + orgId: group.orgId, + isActive: true + }); + + if (oidcConfig?.manageGroupMemberships) { + throw new BadRequestError({ + message: + "Cannot add user to group: OIDC group membership mapping is enabled - user must be assigned to this group in your OIDC provider." + }); + } + const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId); // check if user has broader or equal to privileges than group - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, groupRolePermission); - if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" }); + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + OrgPermissionGroupActions.AddMembers, + OrgPermissionSubjects.Groups, + permission, + groupRolePermission + ); + + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to add user to more privileged group", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionGroupActions.AddMembers, + OrgPermissionSubjects.Groups + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); const user = await userDAL.findOne({ username }); if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` }); @@ -315,14 +408,14 @@ export const groupServiceFactory = ({ }: TRemoveUserFromGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); // check if group with slug exists const group = await groupDAL.findOne({ @@ -335,12 +428,38 @@ export const groupServiceFactory = ({ message: `Failed to find group with ID ${id}` }); + const oidcConfig = await oidcConfigDAL.findOne({ + orgId: group.orgId, + isActive: true + }); + + if (oidcConfig?.manageGroupMemberships) { + throw new BadRequestError({ + message: + "Cannot remove user from group: OIDC group membership mapping is enabled - user must be removed from this group in your OIDC provider." + }); + } + const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId); // check if user has broader or equal to privileges than group - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, groupRolePermission); - if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to delete user from more privileged group" }); + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + OrgPermissionGroupActions.RemoveMembers, + OrgPermissionSubjects.Groups, + permission, + groupRolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to delete user from more privileged group", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionGroupActions.RemoveMembers, + OrgPermissionSubjects.Groups + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); const user = await userDAL.findOne({ username }); if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` }); diff --git a/backend/src/ee/services/group/group-types.ts b/backend/src/ee/services/group/group-types.ts index a6eb4782b..9424075ca 100644 --- a/backend/src/ee/services/group/group-types.ts +++ b/backend/src/ee/services/group/group-types.ts @@ -39,6 +39,7 @@ export type TListGroupUsersDTO = { limit: number; username?: string; search?: string; + filter?: EFilterReturnedUsers; } & TGenericPermission; export type TAddUserToGroupDTO = { @@ -101,3 +102,8 @@ export type TConvertPendingGroupAdditionsToGroupMemberships = { projectBotDAL: Pick; tx?: Knex; }; + +export enum EFilterReturnedUsers { + EXISTING_MEMBERS = "existingMembers", + NON_MEMBERS = "nonMembers" +} diff --git a/backend/src/ee/services/hsm/hsm-fns.ts b/backend/src/ee/services/hsm/hsm-fns.ts new file mode 100644 index 000000000..ef975a371 --- /dev/null +++ b/backend/src/ee/services/hsm/hsm-fns.ts @@ -0,0 +1,56 @@ +import * as pkcs11js from "pkcs11js"; + +import { TEnvConfig } from "@app/lib/config/env"; +import { logger } from "@app/lib/logger"; + +import { HsmModule } from "./hsm-types"; + +export const initializeHsmModule = (envConfig: Pick) => { + // Create a new instance of PKCS11 module + const pkcs11 = new pkcs11js.PKCS11(); + let isInitialized = false; + + const initialize = () => { + if (!envConfig.isHsmConfigured) { + return; + } + + try { + // Load the PKCS#11 module + pkcs11.load(envConfig.HSM_LIB_PATH!); + + // Initialize the module + pkcs11.C_Initialize(); + isInitialized = true; + + logger.info("PKCS#11 module initialized"); + } catch (err) { + logger.error(err, "Failed to initialize PKCS#11 module"); + throw err; + } + }; + + const finalize = () => { + if (isInitialized) { + try { + pkcs11.C_Finalize(); + isInitialized = false; + logger.info("PKCS#11 module finalized"); + } catch (err) { + logger.error(err, "Failed to finalize PKCS#11 module"); + throw err; + } + } + }; + + const getModule = (): HsmModule => ({ + pkcs11, + isInitialized + }); + + return { + initialize, + finalize, + getModule + }; +}; diff --git a/backend/src/ee/services/hsm/hsm-service.ts b/backend/src/ee/services/hsm/hsm-service.ts new file mode 100644 index 000000000..0ed4c5faf --- /dev/null +++ b/backend/src/ee/services/hsm/hsm-service.ts @@ -0,0 +1,469 @@ +import pkcs11js from "pkcs11js"; + +import { TEnvConfig } from "@app/lib/config/env"; +import { logger } from "@app/lib/logger"; + +import { HsmKeyType, HsmModule } from "./hsm-types"; + +type THsmServiceFactoryDep = { + hsmModule: HsmModule; + envConfig: Pick; +}; + +export type THsmServiceFactory = ReturnType; + +type SyncOrAsync = T | Promise; +type SessionCallback = (session: pkcs11js.Handle) => SyncOrAsync; + +// eslint-disable-next-line no-empty-pattern +export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envConfig }: THsmServiceFactoryDep) => { + // Constants for buffer structures + const IV_LENGTH = 16; // Luna HSM typically expects 16-byte IV for cbc + const BLOCK_SIZE = 16; + const HMAC_SIZE = 32; + + const AES_KEY_SIZE = 256; + const HMAC_KEY_SIZE = 256; + + const $withSession = async (callbackWithSession: SessionCallback): Promise => { + const RETRY_INTERVAL = 200; // 200ms between attempts + const MAX_TIMEOUT = 90_000; // 90 seconds maximum total time + + let sessionHandle: pkcs11js.Handle | null = null; + + const removeSession = () => { + if (sessionHandle !== null) { + try { + pkcs11.C_Logout(sessionHandle); + pkcs11.C_CloseSession(sessionHandle); + logger.info("HSM: Terminated session successfully"); + } catch (error) { + logger.error(error, "HSM: Failed to terminate session"); + } finally { + sessionHandle = null; + } + } + }; + + try { + if (!pkcs11 || !isInitialized) { + throw new Error("PKCS#11 module is not initialized"); + } + + // Get slot list + let slots: pkcs11js.Handle[]; + try { + slots = pkcs11.C_GetSlotList(false); // false to get all slots + } catch (error) { + throw new Error(`Failed to get slot list: ${(error as Error)?.message}`); + } + + if (slots.length === 0) { + throw new Error("No slots available"); + } + + if (envConfig.HSM_SLOT >= slots.length) { + throw new Error(`HSM slot ${envConfig.HSM_SLOT} not found or not initialized`); + } + + const slotId = slots[envConfig.HSM_SLOT]; + + const startTime = Date.now(); + while (Date.now() - startTime < MAX_TIMEOUT) { + try { + // Open session + // eslint-disable-next-line no-bitwise + sessionHandle = pkcs11.C_OpenSession(slotId, pkcs11js.CKF_SERIAL_SESSION | pkcs11js.CKF_RW_SESSION); + + // Login + try { + pkcs11.C_Login(sessionHandle, pkcs11js.CKU_USER, envConfig.HSM_PIN); + logger.info("HSM: Successfully authenticated"); + break; + } catch (error) { + // Handle specific error cases + if (error instanceof pkcs11js.Pkcs11Error) { + if (error.code === pkcs11js.CKR_PIN_INCORRECT) { + // We throw instantly here to prevent further attempts, because if too many attempts are made, the HSM will potentially wipe all key material + logger.error(error, `HSM: Incorrect PIN detected for HSM slot ${envConfig.HSM_SLOT}`); + throw new Error("HSM: Incorrect HSM Pin detected. Please check the HSM configuration."); + } + if (error.code === pkcs11js.CKR_USER_ALREADY_LOGGED_IN) { + logger.warn("HSM: Session already logged in"); + } + } + throw error; // Re-throw other errors + } + } catch (error) { + logger.warn(`HSM: Session creation failed. Retrying... Error: ${(error as Error)?.message}`); + + if (sessionHandle !== null) { + try { + pkcs11.C_CloseSession(sessionHandle); + } catch (closeError) { + logger.error(closeError, "HSM: Failed to close session"); + } + sessionHandle = null; + } + + // Wait before retrying + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, RETRY_INTERVAL); + }); + } + } + + if (sessionHandle === null) { + throw new Error("HSM: Failed to open session after maximum retries"); + } + + // Execute callback with session handle + const result = await callbackWithSession(sessionHandle); + removeSession(); + return result; + } catch (error) { + logger.error(error, "HSM: Failed to open session"); + throw error; + } finally { + // Ensure cleanup + removeSession(); + } + }; + + const $findKey = (sessionHandle: pkcs11js.Handle, type: HsmKeyType) => { + const label = type === HsmKeyType.HMAC ? `${envConfig.HSM_KEY_LABEL}_HMAC` : envConfig.HSM_KEY_LABEL; + const keyType = type === HsmKeyType.HMAC ? pkcs11js.CKK_GENERIC_SECRET : pkcs11js.CKK_AES; + + const template = [ + { type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY }, + { type: pkcs11js.CKA_KEY_TYPE, value: keyType }, + { type: pkcs11js.CKA_LABEL, value: label } + ]; + + try { + // Initialize search + pkcs11.C_FindObjectsInit(sessionHandle, template); + + try { + // Find first matching object + const handles = pkcs11.C_FindObjects(sessionHandle, 1); + + if (handles.length === 0) { + throw new Error("Failed to find master key"); + } + + return handles[0]; // Return the key handle + } finally { + // Always finalize the search operation + pkcs11.C_FindObjectsFinal(sessionHandle); + } + } catch (error) { + return null; + } + }; + + const $keyExists = (session: pkcs11js.Handle, type: HsmKeyType): boolean => { + try { + const key = $findKey(session, type); + // items(0) will throw an error if no items are found + // Return true only if we got a valid object with handle + return !!key && key.length > 0; + } catch (error) { + // If items(0) throws, it means no key was found + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call + logger.error(error, "HSM: Failed while checking for HSM key presence"); + + if (error instanceof pkcs11js.Pkcs11Error) { + if (error.code === pkcs11js.CKR_OBJECT_HANDLE_INVALID) { + return false; + } + } + + return false; + } + }; + + const encrypt: { + (data: Buffer, providedSession: pkcs11js.Handle): Promise; + (data: Buffer): Promise; + } = async (data: Buffer, providedSession?: pkcs11js.Handle) => { + if (!pkcs11 || !isInitialized) { + throw new Error("PKCS#11 module is not initialized"); + } + + const $performEncryption = (sessionHandle: pkcs11js.Handle) => { + try { + const aesKey = $findKey(sessionHandle, HsmKeyType.AES); + if (!aesKey) { + throw new Error("HSM: Encryption failed, AES key not found"); + } + + const hmacKey = $findKey(sessionHandle, HsmKeyType.HMAC); + if (!hmacKey) { + throw new Error("HSM: Encryption failed, HMAC key not found"); + } + + const iv = Buffer.alloc(IV_LENGTH); + pkcs11.C_GenerateRandom(sessionHandle, iv); + + const encryptMechanism = { + mechanism: pkcs11js.CKM_AES_CBC_PAD, + parameter: iv + }; + + pkcs11.C_EncryptInit(sessionHandle, encryptMechanism, aesKey); + + // Calculate max buffer size (input length + potential full block of padding) + const maxEncryptedLength = Math.ceil(data.length / BLOCK_SIZE) * BLOCK_SIZE + BLOCK_SIZE; + + // Encrypt the data - this returns the encrypted data directly + const encryptedData = pkcs11.C_Encrypt(sessionHandle, data, Buffer.alloc(maxEncryptedLength)); + + // Initialize HMAC + const hmacMechanism = { + mechanism: pkcs11js.CKM_SHA256_HMAC + }; + + pkcs11.C_SignInit(sessionHandle, hmacMechanism, hmacKey); + + // Sign the IV and encrypted data + pkcs11.C_SignUpdate(sessionHandle, iv); + pkcs11.C_SignUpdate(sessionHandle, encryptedData); + + // Get the HMAC + const hmac = Buffer.alloc(HMAC_SIZE); + pkcs11.C_SignFinal(sessionHandle, hmac); + + // Combine encrypted data and HMAC [Encrypted Data | HMAC] + const finalBuffer = Buffer.alloc(encryptedData.length + hmac.length); + encryptedData.copy(finalBuffer); + hmac.copy(finalBuffer, encryptedData.length); + + return Buffer.concat([iv, finalBuffer]); + } catch (error) { + logger.error(error, "HSM: Failed to perform encryption"); + throw new Error(`HSM: Encryption failed: ${(error as Error)?.message}`); + } + }; + + if (providedSession) { + return $performEncryption(providedSession); + } + + const result = await $withSession($performEncryption); + return result; + }; + + const decrypt: { + (encryptedBlob: Buffer, providedSession: pkcs11js.Handle): Promise; + (encryptedBlob: Buffer): Promise; + } = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle): Promise => { + if (!pkcs11 || !isInitialized) { + throw new Error("PKCS#11 module is not initialized"); + } + + const $performDecryption = (sessionHandle: pkcs11js.Handle) => { + try { + // structure is: [IV (16 bytes) | Encrypted Data (N bytes) | HMAC (32 bytes)] + const iv = encryptedBlob.subarray(0, IV_LENGTH); + const encryptedDataWithHmac = encryptedBlob.subarray(IV_LENGTH); + + // Split encrypted data and HMAC + const hmac = encryptedDataWithHmac.subarray(-HMAC_SIZE); // Last 32 bytes are HMAC + + const encryptedData = encryptedDataWithHmac.subarray(0, -HMAC_SIZE); // Everything except last 32 bytes + + // Find the keys + const aesKey = $findKey(sessionHandle, HsmKeyType.AES); + if (!aesKey) { + throw new Error("HSM: Decryption failed, AES key not found"); + } + + const hmacKey = $findKey(sessionHandle, HsmKeyType.HMAC); + if (!hmacKey) { + throw new Error("HSM: Decryption failed, HMAC key not found"); + } + + // Verify HMAC first + const hmacMechanism = { + mechanism: pkcs11js.CKM_SHA256_HMAC + }; + + pkcs11.C_VerifyInit(sessionHandle, hmacMechanism, hmacKey); + pkcs11.C_VerifyUpdate(sessionHandle, iv); + pkcs11.C_VerifyUpdate(sessionHandle, encryptedData); + + try { + pkcs11.C_VerifyFinal(sessionHandle, hmac); + } catch (error) { + logger.error(error, "HSM: HMAC verification failed"); + throw new Error("HSM: Decryption failed"); // Generic error for failed verification + } + + // Only decrypt if verification passed + const decryptMechanism = { + mechanism: pkcs11js.CKM_AES_CBC_PAD, + parameter: iv + }; + + pkcs11.C_DecryptInit(sessionHandle, decryptMechanism, aesKey); + + const tempBuffer: Buffer = Buffer.alloc(encryptedData.length); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const decryptedData = pkcs11.C_Decrypt(sessionHandle, encryptedData, tempBuffer); + + return Buffer.from(decryptedData); + } catch (error) { + logger.error(error, "HSM: Failed to perform decryption"); + throw new Error("HSM: Decryption failed"); // Generic error for failed decryption, to avoid leaking details about why it failed (such as padding related errors) + } + }; + + if (providedSession) { + return $performDecryption(providedSession); + } + + const result = await $withSession($performDecryption); + return result; + }; + + // We test the core functionality of the PKCS#11 module that we are using throughout Infisical. This is to ensure that the user doesn't configure a faulty or unsupported HSM device. + const $testPkcs11Module = async (session: pkcs11js.Handle) => { + try { + if (!pkcs11 || !isInitialized) { + throw new Error("PKCS#11 module is not initialized"); + } + + if (!session) { + throw new Error("HSM: Attempted to run test without a valid session"); + } + + const randomData = pkcs11.C_GenerateRandom(session, Buffer.alloc(500)); + + const encryptedData = await encrypt(randomData, session); + const decryptedData = await decrypt(encryptedData, session); + + const randomDataHex = randomData.toString("hex"); + const decryptedDataHex = decryptedData.toString("hex"); + + if (randomDataHex !== decryptedDataHex && Buffer.compare(randomData, decryptedData)) { + throw new Error("HSM: Startup test failed. Decrypted data does not match original data"); + } + + return true; + } catch (error) { + logger.error(error, "HSM: Error testing PKCS#11 module"); + return false; + } + }; + + const isActive = async () => { + if (!isInitialized || !envConfig.isHsmConfigured) { + return false; + } + + let pkcs11TestPassed = false; + + try { + pkcs11TestPassed = await $withSession($testPkcs11Module); + } catch (err) { + logger.error(err, "HSM: Error testing PKCS#11 module"); + } + + return envConfig.isHsmConfigured && isInitialized && pkcs11TestPassed; + }; + + const startService = async () => { + if (!envConfig.isHsmConfigured || !pkcs11 || !isInitialized) return; + + try { + await $withSession(async (sessionHandle) => { + // Check if master key exists, create if not + + const genericAttributes = [ + { type: pkcs11js.CKA_TOKEN, value: true }, // Persistent storage + { type: pkcs11js.CKA_EXTRACTABLE, value: false }, // Cannot be extracted + { type: pkcs11js.CKA_SENSITIVE, value: true }, // Sensitive value + { type: pkcs11js.CKA_PRIVATE, value: true } // Requires authentication + ]; + + if (!$keyExists(sessionHandle, HsmKeyType.AES)) { + // Template for generating 256-bit AES master key + const keyTemplate = [ + { type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY }, + { type: pkcs11js.CKA_KEY_TYPE, value: pkcs11js.CKK_AES }, + { type: pkcs11js.CKA_VALUE_LEN, value: AES_KEY_SIZE / 8 }, + { type: pkcs11js.CKA_LABEL, value: envConfig.HSM_KEY_LABEL! }, + { type: pkcs11js.CKA_ENCRYPT, value: true }, // Allow encryption + { type: pkcs11js.CKA_DECRYPT, value: true }, // Allow decryption + ...genericAttributes + ]; + + // Generate the key + pkcs11.C_GenerateKey( + sessionHandle, + { + mechanism: pkcs11js.CKM_AES_KEY_GEN + }, + keyTemplate + ); + + logger.info(`HSM: Master key created successfully with label: ${envConfig.HSM_KEY_LABEL}`); + } + + // Check if HMAC key exists, create if not + if (!$keyExists(sessionHandle, HsmKeyType.HMAC)) { + const hmacKeyTemplate = [ + { type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY }, + { type: pkcs11js.CKA_KEY_TYPE, value: pkcs11js.CKK_GENERIC_SECRET }, + { type: pkcs11js.CKA_VALUE_LEN, value: HMAC_KEY_SIZE / 8 }, // 256-bit key + { type: pkcs11js.CKA_LABEL, value: `${envConfig.HSM_KEY_LABEL!}_HMAC` }, + { type: pkcs11js.CKA_SIGN, value: true }, // Allow signing + { type: pkcs11js.CKA_VERIFY, value: true }, // Allow verification + ...genericAttributes + ]; + + // Generate the HMAC key + pkcs11.C_GenerateKey( + sessionHandle, + { + mechanism: pkcs11js.CKM_GENERIC_SECRET_KEY_GEN + }, + hmacKeyTemplate + ); + + logger.info(`HSM: HMAC key created successfully with label: ${envConfig.HSM_KEY_LABEL}_HMAC`); + } + + // Get slot info to check supported mechanisms + const slotId = pkcs11.C_GetSessionInfo(sessionHandle).slotID; + const mechanisms = pkcs11.C_GetMechanismList(slotId); + + // Check for AES CBC PAD support + const hasAesCbc = mechanisms.includes(pkcs11js.CKM_AES_CBC_PAD); + + if (!hasAesCbc) { + throw new Error(`Required mechanism CKM_AEC_CBC_PAD not supported by HSM`); + } + + // Run test encryption/decryption + const testPassed = await $testPkcs11Module(sessionHandle); + + if (!testPassed) { + throw new Error("PKCS#11 module test failed. Please ensure that the HSM is correctly configured."); + } + }); + } catch (error) { + logger.error(error, "HSM: Error initializing HSM service:"); + throw error; + } + }; + + return { + encrypt, + startService, + isActive, + decrypt + }; +}; diff --git a/backend/src/ee/services/hsm/hsm-types.ts b/backend/src/ee/services/hsm/hsm-types.ts new file mode 100644 index 000000000..b688147f5 --- /dev/null +++ b/backend/src/ee/services/hsm/hsm-types.ts @@ -0,0 +1,11 @@ +import pkcs11js from "pkcs11js"; + +export type HsmModule = { + pkcs11: pkcs11js.PKCS11; + isInitialized: boolean; +}; + +export enum HsmKeyType { + AES = "AES", + HMAC = "hmac" +} diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts index 26a694a4a..bf75ce5cd 100644 --- a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts @@ -1,17 +1,18 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import { packRules } from "@casl/ability/extra"; -import ms from "ms"; -import { TableName } from "@app/db/schemas"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; -import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; -import { unpackPermissions } from "@app/server/routes/santizedSchemas/permission"; +import { ActionProjectType, TableName } from "@app/db/schemas"; +import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; +import { ms } from "@app/lib/ms"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; +import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission"; import { ActorType } from "@app/services/auth/auth-type"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "../permission/project-permission"; import { TIdentityProjectAdditionalPrivilegeV2DALFactory } from "./identity-project-additional-privilege-v2-dal"; import { IdentityProjectAdditionalPrivilegeTemporaryMode, @@ -55,28 +56,50 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ if (!identityProjectMembership) throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - identityProjectMembership.projectId, + projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Edit, + subject(ProjectPermissionSub.Identity, { identityId }) ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); - const { permission: targetIdentityPermission } = await permissionService.getProjectPermission( - ActorType.IDENTITY, - identityId, - identityProjectMembership.projectId, + const { permission: targetIdentityPermission, membership } = await permissionService.getProjectPermission({ + actor: ActorType.IDENTITY, + actorId: identityId, + projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); // we need to validate that the privilege given is not higher than the assigning users permission // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission)); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission); - if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionSub.Identity, + permission, + targetIdentityPermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update more privileged identity", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionSub.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + validateHandlebarTemplate("Identity Additional Privilege Create", JSON.stringify(customPermission || []), { + allowedExpressions: (val) => val.includes("identity.") + }); const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ slug, @@ -132,28 +155,51 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - identityProjectMembership.projectId, + projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Edit, + subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); - const { permission: targetIdentityPermission } = await permissionService.getProjectPermission( - ActorType.IDENTITY, - identityProjectMembership.identityId, - identityProjectMembership.projectId, + const { permission: targetIdentityPermission, membership } = await permissionService.getProjectPermission({ + actor: ActorType.IDENTITY, + actorId: identityProjectMembership.identityId, + projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); // we need to validate that the privilege given is not higher than the assigning users permission // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || [])); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission); - if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionSub.Identity, + permission, + targetIdentityPermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update more privileged identity", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionSub.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + + validateHandlebarTemplate("Identity Additional Privilege Update", JSON.stringify(data.permissions || []), { + allowedExpressions: (val) => val.includes("identity.") + }); if (data?.slug) { const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ @@ -209,24 +255,43 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, membership } = await permissionService.getProjectPermission({ actor, actorId, - identityProjectMembership.projectId, + projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Edit, + subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity); - const { permission: identityRolePermission } = await permissionService.getProjectPermission( - ActorType.IDENTITY, - identityProjectMembership.identityId, - identityProjectMembership.projectId, + const { permission: identityRolePermission } = await permissionService.getProjectPermission({ + actor: ActorType.IDENTITY, + actorId: identityProjectMembership.identityId, + projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionSub.Identity, + permission, + identityRolePermission ); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); - if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update more privileged identity", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionSub.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id); return { @@ -251,14 +316,18 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - identityProjectMembership.projectId, + projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); return { ...identityPrivilege, @@ -282,14 +351,18 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); if (!identityProjectMembership) throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - identityProjectMembership.projectId, + projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ slug, @@ -314,14 +387,18 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); if (!identityProjectMembership) throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - identityProjectMembership.projectId, + projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find( { diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts index 4811eb52a..cbfcc4670 100644 --- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts @@ -1,16 +1,22 @@ -import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability"; +import { ForbiddenError, MongoAbility, RawRuleOf, subject } from "@casl/ability"; import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; -import ms from "ms"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; -import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; -import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; +import { ActionProjectType } from "@app/db/schemas"; +import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; +import { ms } from "@app/lib/ms"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; +import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { ActorType } from "@app/services/auth/auth-type"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSet, ProjectPermissionSub } from "../permission/project-permission"; +import { + ProjectPermissionIdentityActions, + ProjectPermissionSet, + ProjectPermissionSub +} from "../permission/project-permission"; import { TIdentityProjectAdditionalPrivilegeDALFactory } from "./identity-project-additional-privilege-dal"; import { IdentityProjectAdditionalPrivilegeTemporaryMode, @@ -62,28 +68,49 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ if (!identityProjectMembership) throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, membership } = await permissionService.getProjectPermission({ actor, actorId, - identityProjectMembership.projectId, + projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Edit, + subject(ProjectPermissionSub.Identity, { identityId }) ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); - const { permission: targetIdentityPermission } = await permissionService.getProjectPermission( - ActorType.IDENTITY, - identityId, - identityProjectMembership.projectId, + + const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({ + actor: ActorType.IDENTITY, + actorId: identityId, + projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); // we need to validate that the privilege given is not higher than the assigning users permission // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission)); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission); - if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionSub.Identity, + permission, + targetIdentityPermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update more privileged identity", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionSub.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ slug, @@ -91,6 +118,10 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ }); if (existingSlug) throw new BadRequestError({ message: "Additional privilege of provided slug exist" }); + validateHandlebarTemplate("Identity Additional Privilege Create", JSON.stringify(customPermission || []), { + allowedExpressions: (val) => val.includes("identity.") + }); + const packedPermission = JSON.stringify(packRules(customPermission)); if (!dto.isTemporary) { const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({ @@ -139,29 +170,49 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ if (!identityProjectMembership) throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, membership } = await permissionService.getProjectPermission({ actor, actorId, - identityProjectMembership.projectId, + projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); - const { permission: targetIdentityPermission } = await permissionService.getProjectPermission( - ActorType.IDENTITY, - identityProjectMembership.identityId, - identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Edit, + subject(ProjectPermissionSub.Identity, { identityId }) ); + const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({ + actor: ActorType.IDENTITY, + actorId: identityProjectMembership.identityId, + projectId: identityProjectMembership.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + // we need to validate that the privilege given is not higher than the assigning users permission // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || [])); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission); - if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionSub.Identity, + permission, + targetIdentityPermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update more privileged identity", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionSub.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ slug, @@ -182,6 +233,9 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ } const isTemporary = typeof data?.isTemporary !== "undefined" ? data.isTemporary : identityPrivilege.isTemporary; + validateHandlebarTemplate("Identity Additional Privilege Update", JSON.stringify(data.permissions || []), { + allowedExpressions: (val) => val.includes("identity.") + }); const packedPermission = data.permissions ? JSON.stringify(packRules(data.permissions)) : undefined; if (isTemporary) { @@ -234,24 +288,44 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ if (!identityProjectMembership) throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, membership } = await permissionService.getProjectPermission({ actor, actorId, - identityProjectMembership.projectId, + projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Edit, + subject(ProjectPermissionSub.Identity, { identityId }) ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); - const { permission: identityRolePermission } = await permissionService.getProjectPermission( - ActorType.IDENTITY, - identityProjectMembership.identityId, - identityProjectMembership.projectId, + + const { permission: identityRolePermission } = await permissionService.getProjectPermission({ + actor: ActorType.IDENTITY, + actorId: identityProjectMembership.identityId, + projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionSub.Identity, + permission, + identityRolePermission ); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); - if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to edit more privileged identity" }); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to edit more privileged identity", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionSub.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ slug, @@ -287,14 +361,18 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); if (!identityProjectMembership) throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - identityProjectMembership.projectId, + projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + subject(ProjectPermissionSub.Identity, { identityId }) ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ slug, @@ -326,14 +404,19 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); if (!identityProjectMembership) throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - identityProjectMembership.projectId, + projectId: identityProjectMembership.projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + subject(ProjectPermissionSub.Identity, { identityId }) ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find({ projectMembershipId: identityProjectMembership.id diff --git a/backend/src/ee/services/kmip/kmip-client-certificate-dal.ts b/backend/src/ee/services/kmip/kmip-client-certificate-dal.ts new file mode 100644 index 000000000..989b53324 --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-client-certificate-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TKmipClientCertificateDALFactory = ReturnType; + +export const kmipClientCertificateDALFactory = (db: TDbClient) => { + const kmipClientCertOrm = ormify(db, TableName.KmipClientCertificates); + + return kmipClientCertOrm; +}; diff --git a/backend/src/ee/services/kmip/kmip-client-dal.ts b/backend/src/ee/services/kmip/kmip-client-dal.ts new file mode 100644 index 000000000..2650ebad0 --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-client-dal.ts @@ -0,0 +1,86 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName, TKmipClients } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { OrderByDirection } from "@app/lib/types"; + +import { KmipClientOrderBy } from "./kmip-types"; + +export type TKmipClientDALFactory = ReturnType; + +export const kmipClientDALFactory = (db: TDbClient) => { + const kmipClientOrm = ormify(db, TableName.KmipClient); + + const findByProjectAndClientId = async (projectId: string, clientId: string) => { + try { + const client = await db + .replicaNode()(TableName.KmipClient) + .join(TableName.Project, `${TableName.Project}.id`, `${TableName.KmipClient}.projectId`) + .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Project}.orgId`) + .where({ + [`${TableName.KmipClient}.projectId` as "projectId"]: projectId, + [`${TableName.KmipClient}.id` as "id"]: clientId + }) + .select(selectAllTableCols(TableName.KmipClient)) + .select(db.ref("id").withSchema(TableName.Organization).as("orgId")) + .first(); + + return client; + } catch (error) { + throw new DatabaseError({ error, name: "Find by project and client ID" }); + } + }; + + const findByProjectId = async ( + { + projectId, + offset = 0, + limit, + orderBy = KmipClientOrderBy.Name, + orderDirection = OrderByDirection.ASC, + search + }: { + projectId: string; + offset?: number; + limit?: number; + orderBy?: KmipClientOrderBy; + orderDirection?: OrderByDirection; + search?: string; + }, + tx?: Knex + ) => { + try { + const query = (tx || db.replicaNode())(TableName.KmipClient) + .where("projectId", projectId) + .where((qb) => { + if (search) { + void qb.whereILike("name", `%${search}%`); + } + }) + .select< + (TKmipClients & { + total_count: number; + })[] + >(selectAllTableCols(TableName.KmipClient), db.raw(`count(*) OVER() as total_count`)) + .orderBy(orderBy, orderDirection); + + if (limit) { + void query.limit(limit).offset(offset); + } + + const data = await query; + + return { kmipClients: data, totalCount: Number(data?.[0]?.total_count ?? 0) }; + } catch (error) { + throw new DatabaseError({ error, name: "Find KMIP clients by project id" }); + } + }; + + return { + ...kmipClientOrm, + findByProjectId, + findByProjectAndClientId + }; +}; diff --git a/backend/src/ee/services/kmip/kmip-enum.ts b/backend/src/ee/services/kmip/kmip-enum.ts new file mode 100644 index 000000000..80af88e1c --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-enum.ts @@ -0,0 +1,11 @@ +export enum KmipPermission { + Create = "create", + Locate = "locate", + Check = "check", + Get = "get", + GetAttributes = "get-attributes", + Activate = "activate", + Revoke = "revoke", + Destroy = "destroy", + Register = "register" +} diff --git a/backend/src/ee/services/kmip/kmip-operation-service.ts b/backend/src/ee/services/kmip/kmip-operation-service.ts new file mode 100644 index 000000000..45f201498 --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-operation-service.ts @@ -0,0 +1,424 @@ +import { ForbiddenError } from "@casl/ability"; + +import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsKeyUsage } from "@app/services/kms/kms-types"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; + +import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission"; +import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TKmipClientDALFactory } from "./kmip-client-dal"; +import { KmipPermission } from "./kmip-enum"; +import { + TKmipCreateDTO, + TKmipDestroyDTO, + TKmipGetAttributesDTO, + TKmipGetDTO, + TKmipLocateDTO, + TKmipRegisterDTO, + TKmipRevokeDTO +} from "./kmip-types"; + +type TKmipOperationServiceFactoryDep = { + kmsService: TKmsServiceFactory; + kmsDAL: TKmsKeyDALFactory; + kmipClientDAL: TKmipClientDALFactory; + projectDAL: Pick; + permissionService: Pick; +}; + +export type TKmipOperationServiceFactory = ReturnType; + +export const kmipOperationServiceFactory = ({ + kmsService, + kmsDAL, + projectDAL, + kmipClientDAL, + permissionService +}: TKmipOperationServiceFactoryDep) => { + const create = async ({ + projectId, + clientId, + algorithm, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TKmipCreateDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); + + const kmipClient = await kmipClientDAL.findOne({ + id: clientId, + projectId + }); + + if (!kmipClient.permissions?.includes(KmipPermission.Create)) { + throw new ForbiddenRequestError({ + message: "Client does not have sufficient permission to perform KMIP create" + }); + } + + const kmsKey = await kmsService.generateKmsKey({ + encryptionAlgorithm: algorithm, + orgId: actorOrgId, + projectId, + isReserved: false + }); + + return kmsKey; + }; + + const destroy = async ({ projectId, id, clientId, actor, actorId, actorOrgId, actorAuthMethod }: TKmipDestroyDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); + + const kmipClient = await kmipClientDAL.findOne({ + id: clientId, + projectId + }); + + if (!kmipClient.permissions?.includes(KmipPermission.Destroy)) { + throw new ForbiddenRequestError({ + message: "Client does not have sufficient permission to perform KMIP destroy" + }); + } + + const key = await kmsDAL.findOne({ + id, + projectId + }); + + if (!key) { + throw new NotFoundError({ message: `Key with ID ${id} not found` }); + } + + if (key.isReserved) { + throw new BadRequestError({ message: "Cannot destroy reserved keys" }); + } + + const completeKeyDetails = await kmsDAL.findByIdWithAssociatedKms(id); + if (!completeKeyDetails.internalKms) { + throw new BadRequestError({ + message: "Cannot destroy external keys" + }); + } + + if (!completeKeyDetails.isDisabled) { + throw new BadRequestError({ + message: "Cannot destroy active keys" + }); + } + + const kms = kmsDAL.deleteById(id); + + return kms; + }; + + const get = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); + + const kmipClient = await kmipClientDAL.findOne({ + id: clientId, + projectId + }); + + if (!kmipClient.permissions?.includes(KmipPermission.Get)) { + throw new ForbiddenRequestError({ + message: "Client does not have sufficient permission to perform KMIP get" + }); + } + + const key = await kmsDAL.findOne({ + id, + projectId + }); + + if (!key) { + throw new NotFoundError({ message: `Key with ID ${id} not found` }); + } + + if (key.isReserved) { + throw new BadRequestError({ message: "Cannot get reserved keys" }); + } + + const completeKeyDetails = await kmsDAL.findByIdWithAssociatedKms(id); + + if (!completeKeyDetails.internalKms) { + throw new BadRequestError({ + message: "Cannot get external keys" + }); + } + + const kmsKey = await kmsService.getKeyMaterial({ + kmsId: key.id + }); + + return { + id: key.id, + value: kmsKey.toString("base64"), + algorithm: completeKeyDetails.internalKms.encryptionAlgorithm, + isActive: !key.isDisabled, + createdAt: key.createdAt, + updatedAt: key.updatedAt + }; + }; + + const activate = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); + + const kmipClient = await kmipClientDAL.findOne({ + id: clientId, + projectId + }); + + if (!kmipClient.permissions?.includes(KmipPermission.Activate)) { + throw new ForbiddenRequestError({ + message: "Client does not have sufficient permission to perform KMIP activate" + }); + } + + const key = await kmsDAL.findOne({ + id, + projectId + }); + + if (!key) { + throw new NotFoundError({ message: `Key with ID ${id} not found` }); + } + + return { + id: key.id, + isActive: !key.isDisabled + }; + }; + + const revoke = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipRevokeDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); + + const kmipClient = await kmipClientDAL.findOne({ + id: clientId, + projectId + }); + + if (!kmipClient.permissions?.includes(KmipPermission.Revoke)) { + throw new ForbiddenRequestError({ + message: "Client does not have sufficient permission to perform KMIP revoke" + }); + } + + const key = await kmsDAL.findOne({ + id, + projectId + }); + + if (!key) { + throw new NotFoundError({ message: `Key with ID ${id} not found` }); + } + + if (key.isReserved) { + throw new BadRequestError({ message: "Cannot revoke reserved keys" }); + } + + const completeKeyDetails = await kmsDAL.findByIdWithAssociatedKms(id); + + if (!completeKeyDetails.internalKms) { + throw new BadRequestError({ + message: "Cannot revoke external keys" + }); + } + + const revokedKey = await kmsDAL.updateById(key.id, { + isDisabled: true + }); + + return { + id: key.id, + updatedAt: revokedKey.updatedAt + }; + }; + + const getAttributes = async ({ + projectId, + id, + clientId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TKmipGetAttributesDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); + + const kmipClient = await kmipClientDAL.findOne({ + id: clientId, + projectId + }); + + if (!kmipClient.permissions?.includes(KmipPermission.GetAttributes)) { + throw new ForbiddenRequestError({ + message: "Client does not have sufficient permission to perform KMIP get attributes" + }); + } + + const key = await kmsDAL.findOne({ + id, + projectId + }); + + if (!key) { + throw new NotFoundError({ message: `Key with ID ${id} not found` }); + } + + if (key.isReserved) { + throw new BadRequestError({ message: "Cannot get reserved keys" }); + } + + const completeKeyDetails = await kmsDAL.findByIdWithAssociatedKms(id); + + if (!completeKeyDetails.internalKms) { + throw new BadRequestError({ + message: "Cannot get external keys" + }); + } + + return { + id: key.id, + algorithm: completeKeyDetails.internalKms.encryptionAlgorithm, + isActive: !key.isDisabled, + createdAt: key.createdAt, + updatedAt: key.updatedAt + }; + }; + + const locate = async ({ projectId, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipLocateDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); + + const kmipClient = await kmipClientDAL.findOne({ + id: clientId, + projectId + }); + + if (!kmipClient.permissions?.includes(KmipPermission.Locate)) { + throw new ForbiddenRequestError({ + message: "Client does not have sufficient permission to perform KMIP locate" + }); + } + + const keys = await kmsDAL.findProjectCmeks(projectId); + + return keys; + }; + + const register = async ({ + projectId, + clientId, + key, + algorithm, + name, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TKmipRegisterDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); + + const kmipClient = await kmipClientDAL.findOne({ + id: clientId, + projectId + }); + + if (!kmipClient.permissions?.includes(KmipPermission.Register)) { + throw new ForbiddenRequestError({ + message: "Client does not have sufficient permission to perform KMIP register" + }); + } + + const project = await projectDAL.findById(projectId); + + const kmsKey = await kmsService.importKeyMaterial({ + name, + key: Buffer.from(key, "base64"), + algorithm, + isReserved: false, + projectId, + keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT, + orgId: project.orgId + }); + + return kmsKey; + }; + + return { + create, + get, + activate, + getAttributes, + destroy, + revoke, + locate, + register + }; +}; diff --git a/backend/src/ee/services/kmip/kmip-org-config-dal.ts b/backend/src/ee/services/kmip/kmip-org-config-dal.ts new file mode 100644 index 000000000..a6567fafd --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-org-config-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TKmipOrgConfigDALFactory = ReturnType; + +export const kmipOrgConfigDALFactory = (db: TDbClient) => { + const kmipOrgConfigOrm = ormify(db, TableName.KmipOrgConfig); + + return kmipOrgConfigOrm; +}; diff --git a/backend/src/ee/services/kmip/kmip-org-server-certificate-dal.ts b/backend/src/ee/services/kmip/kmip-org-server-certificate-dal.ts new file mode 100644 index 000000000..98626aad1 --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-org-server-certificate-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TKmipOrgServerCertificateDALFactory = ReturnType; + +export const kmipOrgServerCertificateDALFactory = (db: TDbClient) => { + const kmipOrgServerCertificateOrm = ormify(db, TableName.KmipOrgServerCertificates); + + return kmipOrgServerCertificateOrm; +}; diff --git a/backend/src/ee/services/kmip/kmip-service.ts b/backend/src/ee/services/kmip/kmip-service.ts new file mode 100644 index 000000000..82ff1a9aa --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-service.ts @@ -0,0 +1,818 @@ +import { ForbiddenError } from "@casl/ability"; +import * as x509 from "@peculiar/x509"; +import crypto, { KeyObject } from "crypto"; + +import { ActionProjectType } from "@app/db/schemas"; +import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; +import { isValidIp } from "@app/lib/ip"; +import { ms } from "@app/lib/ms"; +import { isFQDN } from "@app/lib/validator/validate-url"; +import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns"; +import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; +import { + createSerialNumber, + keyAlgorithmToAlgCfg +} from "@app/services/certificate-authority/certificate-authority-fns"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { TLicenseServiceFactory } from "../license/license-service"; +import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission"; +import { TPermissionServiceFactory } from "../permission/permission-service"; +import { ProjectPermissionKmipActions, ProjectPermissionSub } from "../permission/project-permission"; +import { TKmipClientCertificateDALFactory } from "./kmip-client-certificate-dal"; +import { TKmipClientDALFactory } from "./kmip-client-dal"; +import { TKmipOrgConfigDALFactory } from "./kmip-org-config-dal"; +import { TKmipOrgServerCertificateDALFactory } from "./kmip-org-server-certificate-dal"; +import { + TCreateKmipClientCertificateDTO, + TCreateKmipClientDTO, + TDeleteKmipClientDTO, + TGenerateOrgKmipServerCertificateDTO, + TGetKmipClientDTO, + TGetOrgKmipDTO, + TListKmipClientsByProjectIdDTO, + TRegisterServerDTO, + TSetupOrgKmipDTO, + TUpdateKmipClientDTO +} from "./kmip-types"; + +type TKmipServiceFactoryDep = { + kmipClientDAL: TKmipClientDALFactory; + kmipClientCertificateDAL: TKmipClientCertificateDALFactory; + kmipOrgServerCertificateDAL: TKmipOrgServerCertificateDALFactory; + permissionService: Pick; + kmsService: Pick; + kmipOrgConfigDAL: TKmipOrgConfigDALFactory; + licenseService: Pick; +}; + +export type TKmipServiceFactory = ReturnType; + +export const kmipServiceFactory = ({ + kmipClientDAL, + permissionService, + kmipClientCertificateDAL, + kmipOrgConfigDAL, + kmsService, + kmipOrgServerCertificateDAL, + licenseService +}: TKmipServiceFactoryDep) => { + const createKmipClient = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + projectId, + name, + description, + permissions + }: TCreateKmipClientDTO) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionKmipActions.CreateClients, + ProjectPermissionSub.Kmip + ); + + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.kmip) + throw new BadRequestError({ + message: "Failed to create KMIP client. Upgrade your plan to enterprise." + }); + + const kmipClient = await kmipClientDAL.create({ + projectId, + name, + description, + permissions + }); + + return kmipClient; + }; + + const updateKmipClient = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + name, + description, + permissions, + id + }: TUpdateKmipClientDTO) => { + const kmipClient = await kmipClientDAL.findById(id); + + if (!kmipClient) { + throw new NotFoundError({ + message: `KMIP client with ID ${id} does not exist` + }); + } + + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.kmip) + throw new BadRequestError({ + message: "Failed to update KMIP client. Upgrade your plan to enterprise." + }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: kmipClient.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionKmipActions.UpdateClients, + ProjectPermissionSub.Kmip + ); + + const updatedKmipClient = await kmipClientDAL.updateById(id, { + name, + description, + permissions + }); + + return updatedKmipClient; + }; + + const deleteKmipClient = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteKmipClientDTO) => { + const kmipClient = await kmipClientDAL.findById(id); + + if (!kmipClient) { + throw new NotFoundError({ + message: `KMIP client with ID ${id} does not exist` + }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: kmipClient.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionKmipActions.DeleteClients, + ProjectPermissionSub.Kmip + ); + + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.kmip) + throw new BadRequestError({ + message: "Failed to delete KMIP client. Upgrade your plan to enterprise." + }); + + const deletedKmipClient = await kmipClientDAL.deleteById(id); + + return deletedKmipClient; + }; + + const getKmipClient = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TGetKmipClientDTO) => { + const kmipClient = await kmipClientDAL.findById(id); + + if (!kmipClient) { + throw new NotFoundError({ + message: `KMIP client with ID ${id} does not exist` + }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: kmipClient.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionKmipActions.ReadClients, ProjectPermissionSub.Kmip); + + return kmipClient; + }; + + const listKmipClientsByProjectId = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + projectId, + ...rest + }: TListKmipClientsByProjectIdDTO) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionKmipActions.ReadClients, ProjectPermissionSub.Kmip); + + return kmipClientDAL.findByProjectId({ projectId, ...rest }); + }; + + const createKmipClientCertificate = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + ttl, + keyAlgorithm, + clientId + }: TCreateKmipClientCertificateDTO) => { + const kmipClient = await kmipClientDAL.findById(clientId); + + if (!kmipClient) { + throw new NotFoundError({ + message: `KMIP client with ID ${clientId} does not exist` + }); + } + + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.kmip) + throw new BadRequestError({ + message: "Failed to create KMIP client. Upgrade your plan to enterprise." + }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: kmipClient.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionKmipActions.GenerateClientCertificates, + ProjectPermissionSub.Kmip + ); + + const kmipConfig = await kmipOrgConfigDAL.findOne({ + orgId: actorOrgId + }); + + if (!kmipConfig) { + throw new InternalServerError({ + message: "KMIP has not been configured for the organization" + }); + } + + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: actorOrgId + }); + + const caCertObj = new x509.X509Certificate( + decryptor({ cipherTextBlob: kmipConfig.encryptedClientIntermediateCaCertificate }) + ); + + const notBeforeDate = new Date(); + const notAfterDate = new Date(new Date().getTime() + ms(ttl)); + + const caCertNotBeforeDate = new Date(caCertObj.notBefore); + const caCertNotAfterDate = new Date(caCertObj.notAfter); + + // check not before constraint + if (notBeforeDate < caCertNotBeforeDate) { + throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" }); + } + + if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" }); + + // check not after constraint + if (notAfterDate > caCertNotAfterDate) { + throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); + } + + const alg = keyAlgorithmToAlgCfg(keyAlgorithm); + const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + + const extensions: x509.Extension[] = [ + new x509.BasicConstraintsExtension(false), + await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), + await x509.SubjectKeyIdentifierExtension.create(leafKeys.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) + ]; + + const caAlg = keyAlgorithmToAlgCfg(kmipConfig.caKeyAlgorithm as CertKeyAlgorithm); + + const caSkObj = crypto.createPrivateKey({ + key: decryptor({ cipherTextBlob: kmipConfig.encryptedClientIntermediateCaPrivateKey }), + format: "der", + type: "pkcs8" + }); + + const caPrivateKey = await crypto.subtle.importKey( + "pkcs8", + caSkObj.export({ format: "der", type: "pkcs8" }), + caAlg, + true, + ["sign"] + ); + + const serialNumber = createSerialNumber(); + const leafCert = await x509.X509CertificateGenerator.create({ + serialNumber, + subject: `OU=${kmipClient.projectId},CN=${clientId}`, + issuer: caCertObj.subject, + notBefore: notBeforeDate, + notAfter: notAfterDate, + signingKey: caPrivateKey, + publicKey: leafKeys.publicKey, + signingAlgorithm: alg, + extensions + }); + + const skLeafObj = KeyObject.from(leafKeys.privateKey); + + const rootCaCert = new x509.X509Certificate(decryptor({ cipherTextBlob: kmipConfig.encryptedRootCaCertificate })); + const serverIntermediateCaCert = new x509.X509Certificate( + decryptor({ cipherTextBlob: kmipConfig.encryptedServerIntermediateCaCertificate }) + ); + + await kmipClientCertificateDAL.create({ + kmipClientId: clientId, + keyAlgorithm, + issuedAt: notBeforeDate, + expiration: notAfterDate, + serialNumber + }); + + return { + serialNumber, + privateKey: skLeafObj.export({ format: "pem", type: "pkcs8" }) as string, + certificate: leafCert.toString("pem"), + certificateChain: constructPemChainFromCerts([serverIntermediateCaCert, rootCaCert]), + projectId: kmipClient.projectId + }; + }; + + const getServerCertificateBySerialNumber = async (orgId: string, serialNumber: string) => { + const serverCert = await kmipOrgServerCertificateDAL.findOne({ + serialNumber, + orgId + }); + + if (!serverCert) { + throw new NotFoundError({ + message: "Server certificate not found" + }); + } + + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId + }); + + const parsedCertificate = new x509.X509Certificate(decryptor({ cipherTextBlob: serverCert.encryptedCertificate })); + + return { + publicKey: parsedCertificate.publicKey.toString("pem"), + keyAlgorithm: serverCert.keyAlgorithm as CertKeyAlgorithm + }; + }; + + const setupOrgKmip = async ({ caKeyAlgorithm, actorOrgId, actor, actorId, actorAuthMethod }: TSetupOrgKmipDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip); + + const kmipConfig = await kmipOrgConfigDAL.findOne({ + orgId: actorOrgId + }); + + if (kmipConfig) { + throw new BadRequestError({ + message: "KMIP has already been configured for the organization" + }); + } + + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.kmip) + throw new BadRequestError({ + message: "Failed to setup KMIP. Upgrade your plan to enterprise." + }); + + const alg = keyAlgorithmToAlgCfg(caKeyAlgorithm); + + // generate root CA + const rootCaSerialNumber = createSerialNumber(); + const rootCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const rootCaSkObj = KeyObject.from(rootCaKeys.privateKey); + const rootCaIssuedAt = new Date(); + const rootCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 20)); + + const rootCaCert = await x509.X509CertificateGenerator.createSelfSigned({ + name: `CN=KMIP Root CA,OU=${actorOrgId}`, + serialNumber: rootCaSerialNumber, + notBefore: rootCaIssuedAt, + notAfter: rootCaExpiration, + signingAlgorithm: alg, + keys: rootCaKeys, + extensions: [ + // eslint-disable-next-line no-bitwise + new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true), + await x509.SubjectKeyIdentifierExtension.create(rootCaKeys.publicKey) + ] + }); + + // generate intermediate server CA + const serverIntermediateCaSerialNumber = createSerialNumber(); + const serverIntermediateCaIssuedAt = new Date(); + const serverIntermediateCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 10)); + const serverIntermediateCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const serverIntermediateCaSkObj = KeyObject.from(serverIntermediateCaKeys.privateKey); + + const serverIntermediateCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: serverIntermediateCaSerialNumber, + subject: `CN=KMIP Server Intermediate CA,OU=${actorOrgId}`, + issuer: rootCaCert.subject, + notBefore: serverIntermediateCaIssuedAt, + notAfter: serverIntermediateCaExpiration, + signingKey: rootCaKeys.privateKey, + publicKey: serverIntermediateCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 0, true), + await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(serverIntermediateCaKeys.publicKey) + ] + }); + + // generate intermediate client CA + const clientIntermediateCaSerialNumber = createSerialNumber(); + const clientIntermediateCaIssuedAt = new Date(); + const clientIntermediateCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 10)); + const clientIntermediateCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const clientIntermediateCaSkObj = KeyObject.from(clientIntermediateCaKeys.privateKey); + + const clientIntermediateCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: clientIntermediateCaSerialNumber, + subject: `CN=KMIP Client Intermediate CA,OU=${actorOrgId}`, + issuer: rootCaCert.subject, + notBefore: clientIntermediateCaIssuedAt, + notAfter: clientIntermediateCaExpiration, + signingKey: rootCaKeys.privateKey, + publicKey: clientIntermediateCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 0, true), + await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(clientIntermediateCaKeys.publicKey) + ] + }); + + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: actorOrgId + }); + + await kmipOrgConfigDAL.create({ + orgId: actorOrgId, + caKeyAlgorithm, + rootCaIssuedAt, + rootCaExpiration, + rootCaSerialNumber, + encryptedRootCaCertificate: encryptor({ plainText: Buffer.from(rootCaCert.rawData) }).cipherTextBlob, + encryptedRootCaPrivateKey: encryptor({ + plainText: rootCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + }).cipherTextBlob, + serverIntermediateCaIssuedAt, + serverIntermediateCaExpiration, + serverIntermediateCaSerialNumber, + encryptedServerIntermediateCaCertificate: encryptor({ + plainText: Buffer.from(new Uint8Array(serverIntermediateCaCert.rawData)) + }).cipherTextBlob, + encryptedServerIntermediateCaChain: encryptor({ plainText: Buffer.from(rootCaCert.toString("pem")) }) + .cipherTextBlob, + encryptedServerIntermediateCaPrivateKey: encryptor({ + plainText: serverIntermediateCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + }).cipherTextBlob, + clientIntermediateCaIssuedAt, + clientIntermediateCaExpiration, + clientIntermediateCaSerialNumber, + encryptedClientIntermediateCaCertificate: encryptor({ + plainText: Buffer.from(new Uint8Array(clientIntermediateCaCert.rawData)) + }).cipherTextBlob, + encryptedClientIntermediateCaChain: encryptor({ plainText: Buffer.from(rootCaCert.toString("pem")) }) + .cipherTextBlob, + encryptedClientIntermediateCaPrivateKey: encryptor({ + plainText: clientIntermediateCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + }).cipherTextBlob + }); + + return { + serverCertificateChain: constructPemChainFromCerts([serverIntermediateCaCert, rootCaCert]), + clientCertificateChain: constructPemChainFromCerts([clientIntermediateCaCert, rootCaCert]) + }; + }; + + const getOrgKmip = async ({ actorOrgId, actor, actorId, actorAuthMethod }: TGetOrgKmipDTO) => { + await permissionService.getOrgPermission(actor, actorId, actorOrgId, actorAuthMethod, actorOrgId); + + const kmipConfig = await kmipOrgConfigDAL.findOne({ + orgId: actorOrgId + }); + + if (!kmipConfig) { + throw new BadRequestError({ + message: "KMIP has not been configured for the organization" + }); + } + + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: actorOrgId + }); + + const rootCaCert = new x509.X509Certificate(decryptor({ cipherTextBlob: kmipConfig.encryptedRootCaCertificate })); + const serverIntermediateCaCert = new x509.X509Certificate( + decryptor({ cipherTextBlob: kmipConfig.encryptedServerIntermediateCaCertificate }) + ); + + const clientIntermediateCaCert = new x509.X509Certificate( + decryptor({ cipherTextBlob: kmipConfig.encryptedClientIntermediateCaCertificate }) + ); + + return { + id: kmipConfig.id, + serverCertificateChain: constructPemChainFromCerts([serverIntermediateCaCert, rootCaCert]), + clientCertificateChain: constructPemChainFromCerts([clientIntermediateCaCert, rootCaCert]) + }; + }; + + const generateOrgKmipServerCertificate = async ({ + orgId, + ttl, + commonName, + altNames, + keyAlgorithm + }: TGenerateOrgKmipServerCertificateDTO) => { + const kmipOrgConfig = await kmipOrgConfigDAL.findOne({ + orgId + }); + + if (!kmipOrgConfig) { + throw new BadRequestError({ + message: "KMIP has not been configured for the organization" + }); + } + + const plan = await licenseService.getPlan(orgId); + if (!plan.kmip) + throw new BadRequestError({ + message: "Failed to generate KMIP server certificate. Upgrade your plan to enterprise." + }); + + const { decryptor, encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId + }); + + const caCertObj = new x509.X509Certificate( + decryptor({ cipherTextBlob: kmipOrgConfig.encryptedServerIntermediateCaCertificate }) + ); + + const notBeforeDate = new Date(); + const notAfterDate = new Date(new Date().getTime() + ms(ttl)); + + const caCertNotBeforeDate = new Date(caCertObj.notBefore); + const caCertNotAfterDate = new Date(caCertObj.notAfter); + + // check not before constraint + if (notBeforeDate < caCertNotBeforeDate) { + throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" }); + } + + if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" }); + + // check not after constraint + if (notAfterDate > caCertNotAfterDate) { + throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); + } + + const alg = keyAlgorithmToAlgCfg(keyAlgorithm); + const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + + const extensions: x509.Extension[] = [ + new x509.BasicConstraintsExtension(false), + await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), + await x509.SubjectKeyIdentifierExtension.create(leafKeys.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], + true + ), + new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true) + ]; + + const altNamesArray: { + type: "email" | "dns" | "ip"; + value: string; + }[] = altNames + .split(",") + .map((name) => name.trim()) + .map((altName) => { + if (isFQDN(altName, { allow_wildcard: true })) { + return { + type: "dns", + value: altName + }; + } + + if (isValidIp(altName)) { + return { + type: "ip", + value: altName + }; + } + + throw new Error(`Invalid altName: ${altName}`); + }); + + const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); + extensions.push(altNamesExtension); + + const caAlg = keyAlgorithmToAlgCfg(kmipOrgConfig.caKeyAlgorithm as CertKeyAlgorithm); + + const decryptedCaCertChain = decryptor({ + cipherTextBlob: kmipOrgConfig.encryptedServerIntermediateCaChain + }).toString("utf-8"); + + const caSkObj = crypto.createPrivateKey({ + key: decryptor({ cipherTextBlob: kmipOrgConfig.encryptedServerIntermediateCaPrivateKey }), + format: "der", + type: "pkcs8" + }); + + const caPrivateKey = await crypto.subtle.importKey( + "pkcs8", + caSkObj.export({ format: "der", type: "pkcs8" }), + caAlg, + true, + ["sign"] + ); + + const serialNumber = createSerialNumber(); + const leafCert = await x509.X509CertificateGenerator.create({ + serialNumber, + subject: `CN=${commonName}`, + issuer: caCertObj.subject, + notBefore: notBeforeDate, + notAfter: notAfterDate, + signingKey: caPrivateKey, + publicKey: leafKeys.publicKey, + signingAlgorithm: alg, + extensions + }); + + const skLeafObj = KeyObject.from(leafKeys.privateKey); + const certificateChain = `${caCertObj.toString("pem")}\n${decryptedCaCertChain}`.trim(); + + await kmipOrgServerCertificateDAL.create({ + orgId, + keyAlgorithm, + issuedAt: notBeforeDate, + expiration: notAfterDate, + serialNumber, + commonName, + altNames, + encryptedCertificate: encryptor({ plainText: Buffer.from(new Uint8Array(leafCert.rawData)) }).cipherTextBlob, + encryptedChain: encryptor({ plainText: Buffer.from(certificateChain) }).cipherTextBlob + }); + + return { + serialNumber, + privateKey: skLeafObj.export({ format: "pem", type: "pkcs8" }) as string, + certificate: leafCert.toString("pem"), + certificateChain + }; + }; + + const registerServer = async ({ + actorOrgId, + actor, + actorId, + actorAuthMethod, + ttl, + commonName, + keyAlgorithm, + hostnamesOrIps + }: TRegisterServerDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); + + const kmipConfig = await kmipOrgConfigDAL.findOne({ + orgId: actorOrgId + }); + + if (!kmipConfig) { + throw new BadRequestError({ + message: "KMIP has not been configured for the organization" + }); + } + + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.kmip) + throw new BadRequestError({ + message: "Failed to register KMIP server. Upgrade your plan to enterprise." + }); + + const { privateKey, certificate, certificateChain, serialNumber } = await generateOrgKmipServerCertificate({ + orgId: actorOrgId, + commonName: commonName ?? "kmip-server", + altNames: hostnamesOrIps, + keyAlgorithm: keyAlgorithm ?? (kmipConfig.caKeyAlgorithm as CertKeyAlgorithm), + ttl + }); + + const { clientCertificateChain } = await getOrgKmip({ + actor, + actorAuthMethod, + actorId, + actorOrgId + }); + + return { + serverCertificateSerialNumber: serialNumber, + clientCertificateChain, + privateKey, + certificate, + certificateChain + }; + }; + + return { + createKmipClient, + updateKmipClient, + deleteKmipClient, + getKmipClient, + listKmipClientsByProjectId, + createKmipClientCertificate, + setupOrgKmip, + generateOrgKmipServerCertificate, + getOrgKmip, + getServerCertificateBySerialNumber, + registerServer + }; +}; diff --git a/backend/src/ee/services/kmip/kmip-types.ts b/backend/src/ee/services/kmip/kmip-types.ts new file mode 100644 index 000000000..81d0d8766 --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-types.ts @@ -0,0 +1,102 @@ +import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher"; +import { OrderByDirection, TOrgPermission, TProjectPermission } from "@app/lib/types"; +import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; + +import { KmipPermission } from "./kmip-enum"; + +export type TCreateKmipClientCertificateDTO = { + clientId: string; + keyAlgorithm: CertKeyAlgorithm; + ttl: string; +} & Omit; + +export type TCreateKmipClientDTO = { + name: string; + description?: string; + permissions: KmipPermission[]; +} & TProjectPermission; + +export type TUpdateKmipClientDTO = { + id: string; + name?: string; + description?: string; + permissions?: KmipPermission[]; +} & Omit; + +export type TDeleteKmipClientDTO = { + id: string; +} & Omit; + +export type TGetKmipClientDTO = { + id: string; +} & Omit; + +export enum KmipClientOrderBy { + Name = "name" +} + +export type TListKmipClientsByProjectIdDTO = { + offset?: number; + limit?: number; + orderBy?: KmipClientOrderBy; + orderDirection?: OrderByDirection; + search?: string; +} & TProjectPermission; + +type KmipOperationBaseDTO = { + clientId: string; + projectId: string; +} & Omit; + +export type TKmipCreateDTO = { + algorithm: SymmetricKeyAlgorithm; +} & KmipOperationBaseDTO; + +export type TKmipGetDTO = { + id: string; +} & KmipOperationBaseDTO; + +export type TKmipGetAttributesDTO = { + id: string; +} & KmipOperationBaseDTO; + +export type TKmipDestroyDTO = { + id: string; +} & KmipOperationBaseDTO; + +export type TKmipActivateDTO = { + id: string; +} & KmipOperationBaseDTO; + +export type TKmipRevokeDTO = { + id: string; +} & KmipOperationBaseDTO; + +export type TKmipLocateDTO = KmipOperationBaseDTO; + +export type TKmipRegisterDTO = { + name: string; + key: string; + algorithm: SymmetricKeyAlgorithm; +} & KmipOperationBaseDTO; + +export type TSetupOrgKmipDTO = { + caKeyAlgorithm: CertKeyAlgorithm; +} & Omit; + +export type TGetOrgKmipDTO = Omit; + +export type TGenerateOrgKmipServerCertificateDTO = { + commonName: string; + altNames: string; + keyAlgorithm: CertKeyAlgorithm; + ttl: string; + orgId: string; +}; + +export type TRegisterServerDTO = { + hostnamesOrIps: string; + commonName?: string; + keyAlgorithm?: CertKeyAlgorithm; + ttl: string; +} & Omit; diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts index 0cbab8c32..e22b18e1b 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -1,25 +1,18 @@ import { ForbiddenError } from "@casl/ability"; import jwt from "jsonwebtoken"; -import { OrgMembershipStatus, SecretKeyEncoding, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas"; +import { OrgMembershipStatus, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { getConfig } from "@app/lib/config/env"; -import { - decryptSymmetric, - encryptSymmetric, - generateAsymmetricKeyPair, - generateSymmetricKey, - infisicalSymmetricDecrypt, - infisicalSymmetricEncypt -} from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { AuthMethod, 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 { TOrgBotDALFactory } from "@app/services/org/org-bot-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"; @@ -59,7 +52,6 @@ type TLdapConfigServiceFactoryDep = { TOrgDALFactory, "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" >; - orgBotDAL: Pick; groupDAL: Pick; groupProjectDAL: Pick; projectKeyDAL: Pick; @@ -84,6 +76,7 @@ type TLdapConfigServiceFactoryDep = { licenseService: Pick; tokenService: Pick; smtpService: Pick; + kmsService: Pick; }; export type TLdapConfigServiceFactory = ReturnType; @@ -93,7 +86,6 @@ export const ldapConfigServiceFactory = ({ ldapGroupMapDAL, orgDAL, orgMembershipDAL, - orgBotDAL, groupDAL, groupProjectDAL, projectKeyDAL, @@ -105,7 +97,8 @@ export const ldapConfigServiceFactory = ({ permissionService, licenseService, tokenService, - smtpService + smtpService, + kmsService }: TLdapConfigServiceFactoryDep) => { const createLdapCfg = async ({ actor, @@ -133,77 +126,23 @@ export const ldapConfigServiceFactory = ({ message: "Failed to create LDAP configuration due to plan restriction. Upgrade plan to create LDAP configuration." }); - - const orgBot = await orgBotDAL.transaction(async (tx) => { - const doc = await orgBotDAL.findOne({ orgId }, tx); - if (doc) return doc; - - const { privateKey, publicKey } = generateAsymmetricKeyPair(); - const key = generateSymmetricKey(); - const { - ciphertext: encryptedPrivateKey, - iv: privateKeyIV, - tag: privateKeyTag, - encoding: privateKeyKeyEncoding, - algorithm: privateKeyAlgorithm - } = infisicalSymmetricEncypt(privateKey); - const { - ciphertext: encryptedSymmetricKey, - iv: symmetricKeyIV, - tag: symmetricKeyTag, - encoding: symmetricKeyKeyEncoding, - algorithm: symmetricKeyAlgorithm - } = infisicalSymmetricEncypt(key); - - return orgBotDAL.create( - { - name: "Infisical org bot", - publicKey, - privateKeyIV, - encryptedPrivateKey, - symmetricKeyIV, - symmetricKeyTag, - encryptedSymmetricKey, - symmetricKeyAlgorithm, - orgId, - privateKeyTag, - privateKeyAlgorithm, - privateKeyKeyEncoding, - symmetricKeyKeyEncoding - }, - tx - ); + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId }); - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding - }); - - const { ciphertext: encryptedBindDN, iv: bindDNIV, tag: bindDNTag } = encryptSymmetric(bindDN, key); - const { ciphertext: encryptedBindPass, iv: bindPassIV, tag: bindPassTag } = encryptSymmetric(bindPass, key); - const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); - const ldapConfig = await ldapConfigDAL.create({ orgId, isActive, url, - encryptedBindDN, - bindDNIV, - bindDNTag, - encryptedBindPass, - bindPassIV, - bindPassTag, uniqueUserAttribute, searchBase, searchFilter, groupSearchBase, groupSearchFilter, - encryptedCACert, - caCertIV, - caCertTag + encryptedLdapCaCertificate: encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob, + encryptedLdapBindDN: encryptor({ plainText: Buffer.from(bindDN) }).cipherTextBlob, + encryptedLdapBindPass: encryptor({ plainText: Buffer.from(bindPass) }).cipherTextBlob }); return ldapConfig; @@ -246,38 +185,21 @@ export const ldapConfigServiceFactory = ({ uniqueUserAttribute }; - const orgBot = await orgBotDAL.findOne({ orgId }); - if (!orgBot) - throw new NotFoundError({ - message: `Organization bot in organization with ID '${orgId}' not found`, - name: "OrgBotNotFound" - }); - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId }); if (bindDN !== undefined) { - const { ciphertext: encryptedBindDN, iv: bindDNIV, tag: bindDNTag } = encryptSymmetric(bindDN, key); - updateQuery.encryptedBindDN = encryptedBindDN; - updateQuery.bindDNIV = bindDNIV; - updateQuery.bindDNTag = bindDNTag; + updateQuery.encryptedLdapBindDN = encryptor({ plainText: Buffer.from(bindDN) }).cipherTextBlob; } if (bindPass !== undefined) { - const { ciphertext: encryptedBindPass, iv: bindPassIV, tag: bindPassTag } = encryptSymmetric(bindPass, key); - updateQuery.encryptedBindPass = encryptedBindPass; - updateQuery.bindPassIV = bindPassIV; - updateQuery.bindPassTag = bindPassTag; + updateQuery.encryptedLdapBindPass = encryptor({ plainText: Buffer.from(bindPass) }).cipherTextBlob; } if (caCert !== undefined) { - const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); - updateQuery.encryptedCACert = encryptedCACert; - updateQuery.caCertIV = caCertIV; - updateQuery.caCertTag = caCertTag; + updateQuery.encryptedLdapCaCertificate = encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob; } const [ldapConfig] = await ldapConfigDAL.update({ orgId }, updateQuery); @@ -293,61 +215,24 @@ export const ldapConfigServiceFactory = ({ }); } - const orgBot = await orgBotDAL.findOne({ orgId: ldapConfig.orgId }); - if (!orgBot) { - throw new NotFoundError({ - message: `Organization bot not found in organization with ID ${ldapConfig.orgId}`, - name: "OrgBotNotFound" - }); - } - - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: ldapConfig.orgId }); - const { - encryptedBindDN, - bindDNIV, - bindDNTag, - encryptedBindPass, - bindPassIV, - bindPassTag, - encryptedCACert, - caCertIV, - caCertTag - } = ldapConfig; - let bindDN = ""; - if (encryptedBindDN && bindDNIV && bindDNTag) { - bindDN = decryptSymmetric({ - ciphertext: encryptedBindDN, - key, - tag: bindDNTag, - iv: bindDNIV - }); + if (ldapConfig.encryptedLdapBindDN) { + bindDN = decryptor({ cipherTextBlob: ldapConfig.encryptedLdapBindDN }).toString(); } let bindPass = ""; - if (encryptedBindPass && bindPassIV && bindPassTag) { - bindPass = decryptSymmetric({ - ciphertext: encryptedBindPass, - key, - tag: bindPassTag, - iv: bindPassIV - }); + if (ldapConfig.encryptedLdapBindPass) { + bindPass = decryptor({ cipherTextBlob: ldapConfig.encryptedLdapBindPass }).toString(); } let caCert = ""; - if (encryptedCACert && caCertIV && caCertTag) { - caCert = decryptSymmetric({ - ciphertext: encryptedCACert, - key, - tag: caCertTag, - iv: caCertIV - }); + if (ldapConfig.encryptedLdapCaCertificate) { + caCert = decryptor({ cipherTextBlob: ldapConfig.encryptedLdapCaCertificate }).toString(); } return { @@ -476,14 +361,14 @@ export const ldapConfigServiceFactory = ({ }); } else { const plan = await licenseService.getPlan(orgId); - if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) { + if (plan?.slug !== "enterprise" && plan?.memberLimit && plan.membersUsed >= plan.memberLimit) { // limit imposed on number of members allowed / number of members used exceeds the number of members allowed throw new BadRequestError({ message: "Failed to create new member via LDAP due to member limit reached. Upgrade plan to add more members." }); } - if (plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { + if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed throw new BadRequestError({ message: "Failed to create new member via LDAP due to member limit reached. Upgrade plan to add more members." diff --git a/backend/src/ee/services/ldap-config/ldap-fns.ts b/backend/src/ee/services/ldap-config/ldap-fns.ts index 66d799583..44af718ed 100644 --- a/backend/src/ee/services/ldap-config/ldap-fns.ts +++ b/backend/src/ee/services/ldap-config/ldap-fns.ts @@ -36,8 +36,7 @@ export const testLDAPConfig = async (ldapConfig: TLDAPConfig): Promise }); ldapClient.on("error", (err) => { - logger.error("LDAP client error:", err); - logger.error(err); + logger.error(err, "LDAP client error"); resolve(false); }); @@ -98,12 +97,14 @@ export const searchGroups = async ( res.on("searchEntry", (entry) => { const dn = entry.dn.toString(); - const regex = /cn=([^,]+)/; - const match = dn.match(regex); - // parse the cn from the dn - const cn = (match && match[1]) as string; + const cnStartIndex = dn.indexOf("cn="); - groups.push({ dn, cn }); + if (cnStartIndex !== -1) { + const valueStartIndex = cnStartIndex + 3; + const commaIndex = dn.indexOf(",", valueStartIndex); + const cn = dn.substring(valueStartIndex, commaIndex === -1 ? undefined : commaIndex); + groups.push({ dn, cn }); + } }); res.on("error", (error) => { ldapClient.unbind(); diff --git a/backend/src/ee/services/license/licence-enums.ts b/backend/src/ee/services/license/licence-enums.ts new file mode 100644 index 000000000..047eb0a38 --- /dev/null +++ b/backend/src/ee/services/license/licence-enums.ts @@ -0,0 +1,24 @@ +export const BillingPlanRows = { + MemberLimit: { name: "Organization member limit", field: "memberLimit" }, + IdentityLimit: { name: "Organization identity limit", field: "identityLimit" }, + WorkspaceLimit: { name: "Project limit", field: "workspaceLimit" }, + EnvironmentLimit: { name: "Environment limit", field: "environmentLimit" }, + SecretVersioning: { name: "Secret versioning", field: "secretVersioning" }, + PitRecovery: { name: "Point in time recovery", field: "pitRecovery" }, + Rbac: { name: "RBAC", field: "rbac" }, + CustomRateLimits: { name: "Custom rate limits", field: "customRateLimits" }, + CustomAlerts: { name: "Custom alerts", field: "customAlerts" }, + AuditLogs: { name: "Audit logs", field: "auditLogs" }, + SamlSSO: { name: "SAML SSO", field: "samlSSO" }, + Hsm: { name: "Hardware Security Module (HSM)", field: "hsm" }, + OidcSSO: { name: "OIDC SSO", field: "oidcSSO" }, + SecretApproval: { name: "Secret approvals", field: "secretApproval" }, + SecretRotation: { name: "Secret rotation", field: "secretRotation" }, + InstanceUserManagement: { name: "Instance User Management", field: "instanceUserManagement" }, + ExternalKms: { name: "External KMS", field: "externalKms" } +} as const; + +export const BillingPlanTableHead = { + Allowed: { name: "Allowed" }, + Used: { name: "Used" } +} as const; diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index 0864ab33c..3f4af174b 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -24,11 +24,13 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ rbac: false, customRateLimits: false, customAlerts: false, + secretAccessInsights: false, auditLogs: false, auditLogsRetentionDays: 0, auditLogStreams: false, auditLogStreamLimit: 3, samlSSO: false, + hsm: false, oidcSSO: false, scim: false, ldap: false, @@ -37,7 +39,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ trial_end: null, has_used_trial: true, secretApproval: false, - secretRotation: true, + secretRotation: false, caCrl: false, instanceUserManagement: false, externalKms: false, @@ -48,7 +50,9 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ }, pkiEst: false, enforceMfa: false, - projectTemplates: false + projectTemplates: false, + kmip: false, + gateway: false }); export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index dc56e7bc3..cf9818658 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -5,6 +5,7 @@ // TODO(akhilmhdh): With tony find out the api structure and fill it here import { ForbiddenError } from "@casl/ability"; +import { CronJob } from "cron"; import { Knex } from "knex"; import { TKeyStoreFactory } from "@app/keystore/keystore"; @@ -12,10 +13,13 @@ import { getConfig } from "@app/lib/config/env"; import { verifyOfflineLicense } from "@app/lib/crypto"; import { NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; +import { TIdentityOrgDALFactory } from "@app/services/identity/identity-org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TPermissionServiceFactory } from "../permission/permission-service"; +import { BillingPlanRows, BillingPlanTableHead } from "./licence-enums"; import { TLicenseDALFactory } from "./license-dal"; import { getDefaultOnPremFeatures, setupLicenseRequestWithStore } from "./license-fns"; import { @@ -28,6 +32,7 @@ import { TFeatureSet, TGetOrgBillInfoDTO, TGetOrgTaxIdDTO, + TOfflineLicense, TOfflineLicenseContents, TOrgInvoiceDTO, TOrgLicensesDTO, @@ -39,10 +44,12 @@ import { } from "./license-types"; type TLicenseServiceFactoryDep = { - orgDAL: Pick; + orgDAL: Pick; permissionService: Pick; licenseDAL: TLicenseDALFactory; keyStore: Pick; + identityOrgMembershipDAL: TIdentityOrgDALFactory; + projectDAL: TProjectDALFactory; }; export type TLicenseServiceFactory = ReturnType; @@ -50,18 +57,21 @@ export type TLicenseServiceFactory = ReturnType; const LICENSE_SERVER_CLOUD_LOGIN = "/api/auth/v1/license-server-login"; const LICENSE_SERVER_ON_PREM_LOGIN = "/api/auth/v1/license-login"; -const LICENSE_SERVER_CLOUD_PLAN_TTL = 30; // 30 second +const LICENSE_SERVER_CLOUD_PLAN_TTL = 5 * 60; // 5 mins const FEATURE_CACHE_KEY = (orgId: string) => `infisical-cloud-plan-${orgId}`; export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL, - keyStore + keyStore, + identityOrgMembershipDAL, + projectDAL }: TLicenseServiceFactoryDep) => { let isValidLicense = false; let instanceType = InstanceType.OnPrem; let onPremFeatures: TFeatureSet = getDefaultOnPremFeatures(); + let selfHostedLicense: TOfflineLicense | null = null; const appCfg = getConfig(); const licenseServerCloudApi = setupLicenseRequestWithStore( @@ -76,6 +86,20 @@ export const licenseServiceFactory = ({ appCfg.LICENSE_KEY || "" ); + const syncLicenseKeyOnPremFeatures = async (shouldThrow: boolean = false) => { + logger.info("Start syncing license key features"); + try { + const { + data: { currentPlan } + } = await licenseServerOnPremApi.request.get<{ currentPlan: TFeatureSet }>("/api/license/v1/plan"); + onPremFeatures = currentPlan; + logger.info("Successfully synchronized license key features"); + } catch (error) { + logger.error(error, "Failed to synchronize license key features"); + if (shouldThrow) throw error; + } + }; + const init = async () => { try { if (appCfg.LICENSE_SERVER_KEY) { @@ -89,10 +113,7 @@ export const licenseServiceFactory = ({ if (appCfg.LICENSE_KEY) { const token = await licenseServerOnPremApi.refreshLicense(); if (token) { - const { - data: { currentPlan } - } = await licenseServerOnPremApi.request.get<{ currentPlan: TFeatureSet }>("/api/license/v1/plan"); - onPremFeatures = currentPlan; + await syncLicenseKeyOnPremFeatures(true); instanceType = InstanceType.EnterpriseOnPrem; logger.info(`Instance type: ${InstanceType.EnterpriseOnPrem}`); isValidLicense = true; @@ -125,6 +146,7 @@ export const licenseServiceFactory = ({ instanceType = InstanceType.EnterpriseOnPremOffline; logger.info(`Instance type: ${InstanceType.EnterpriseOnPremOffline}`); isValidLicense = true; + selfHostedLicense = contents.license; return; } } @@ -137,12 +159,24 @@ export const licenseServiceFactory = ({ } }; + const initializeBackgroundSync = async () => { + if (appCfg.LICENSE_KEY) { + logger.info("Setting up background sync process for refresh onPremFeatures"); + const job = new CronJob("*/10 * * * *", syncLicenseKeyOnPremFeatures); + job.start(); + return job; + } + }; + const getPlan = async (orgId: string, projectId?: string) => { logger.info(`getPlan: attempting to fetch plan for [orgId=${orgId}] [projectId=${projectId}]`); try { if (instanceType === InstanceType.Cloud) { const cachedPlan = await keyStore.getItem(FEATURE_CACHE_KEY(orgId)); - if (cachedPlan) return JSON.parse(cachedPlan) as TFeatureSet; + if (cachedPlan) { + logger.info(`getPlan: plan fetched from cache [orgId=${orgId}] [projectId=${projectId}]`); + return JSON.parse(cachedPlan) as TFeatureSet; + } const org = await orgDAL.findOrgById(orgId); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); @@ -161,8 +195,8 @@ export const licenseServiceFactory = ({ } } catch (error) { logger.error( - `getPlan: encountered an error when fetching pan [orgId=${orgId}] [projectId=${projectId}] [error]`, - error + error, + `getPlan: encountered an error when fetching pan [orgId=${orgId}] [projectId=${projectId}] [error]` ); await keyStore.setItemWithExpiry( FEATURE_CACHE_KEY(orgId), @@ -170,6 +204,8 @@ export const licenseServiceFactory = ({ JSON.stringify(onPremFeatures) ); return onPremFeatures; + } finally { + logger.info(`getPlan: Process done for [orgId=${orgId}] [projectId=${projectId}]`); } return onPremFeatures; }; @@ -246,8 +282,7 @@ export const licenseServiceFactory = ({ }; const getOrgPlan = async ({ orgId, actor, actorId, actorOrgId, actorAuthMethod, projectId }: TOrgPlanDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); + await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); const plan = await getPlan(orgId, projectId); return plan; }; @@ -344,10 +379,21 @@ export const licenseServiceFactory = ({ message: `Organization with ID '${orgId}' not found` }); } - const { data } = await licenseServerCloudApi.request.get( - `/api/license-server/v1/customers/${organization.customerId}/cloud-plan/billing` - ); - return data; + if (instanceType !== InstanceType.OnPrem && instanceType !== InstanceType.EnterpriseOnPremOffline) { + const { data } = await licenseServerCloudApi.request.get( + `/api/license-server/v1/customers/${organization.customerId}/cloud-plan/billing` + ); + return data; + } + + return { + currentPeriodStart: selfHostedLicense?.issuedAt ? Date.parse(selfHostedLicense?.issuedAt) / 1000 : undefined, + currentPeriodEnd: selfHostedLicense?.expiresAt ? Date.parse(selfHostedLicense?.expiresAt) / 1000 : undefined, + interval: "month", + intervalCount: 1, + amount: 0, + quantity: 1 + }; }; // returns org current plan feature table @@ -361,10 +407,41 @@ export const licenseServiceFactory = ({ message: `Organization with ID '${orgId}' not found` }); } - const { data } = await licenseServerCloudApi.request.get( - `/api/license-server/v1/customers/${organization.customerId}/cloud-plan/table` + if (instanceType !== InstanceType.OnPrem && instanceType !== InstanceType.EnterpriseOnPremOffline) { + const { data } = await licenseServerCloudApi.request.get( + `/api/license-server/v1/customers/${organization.customerId}/cloud-plan/table` + ); + return data; + } + + const mappedRows = await Promise.all( + Object.values(BillingPlanRows).map(async ({ name, field }: { name: string; field: string }) => { + const allowed = onPremFeatures[field as keyof TFeatureSet]; + let used = "-"; + + if (field === BillingPlanRows.MemberLimit.field) { + const orgMemberships = await orgDAL.countAllOrgMembers(orgId); + used = orgMemberships.toString(); + } else if (field === BillingPlanRows.WorkspaceLimit.field) { + const projects = await projectDAL.find({ orgId }); + used = projects.length.toString(); + } else if (field === BillingPlanRows.IdentityLimit.field) { + const identities = await identityOrgMembershipDAL.countAllOrgIdentities({ orgId }); + used = identities.toString(); + } + + return { + name, + allowed, + used + }; + }) ); - return data; + + return { + head: Object.values(BillingPlanTableHead), + rows: mappedRows + }; }; const getOrgBillingDetails = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { @@ -606,6 +683,7 @@ export const licenseServiceFactory = ({ getOrgTaxInvoices, getOrgTaxIds, addOrgTaxId, - delOrgTaxId + delOrgTaxId, + initializeBackgroundSync }; }; diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 0ba54afc3..c2bf42e2e 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -46,7 +46,9 @@ export type TFeatureSet = { auditLogStreams: false; auditLogStreamLimit: 3; samlSSO: false; + hsm: false; oidcSSO: false; + secretAccessInsights: false; scim: false; ldap: false; groups: false; @@ -54,7 +56,7 @@ export type TFeatureSet = { trial_end: null; has_used_trial: true; secretApproval: false; - secretRotation: true; + secretRotation: false; caCrl: false; instanceUserManagement: false; externalKms: false; @@ -66,6 +68,8 @@ export type TFeatureSet = { pkiEst: boolean; enforceMfa: boolean; projectTemplates: false; + kmip: false; + gateway: false; }; export type TOrgPlansTableDTO = { diff --git a/backend/src/ee/services/oidc/oidc-config-dal.ts b/backend/src/ee/services/oidc/oidc-config-dal.ts index ffdba2cf7..b9b0a2659 100644 --- a/backend/src/ee/services/oidc/oidc-config-dal.ts +++ b/backend/src/ee/services/oidc/oidc-config-dal.ts @@ -1,6 +1,5 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; -import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; export type TOidcConfigDALFactory = ReturnType; @@ -8,22 +7,5 @@ export type TOidcConfigDALFactory = ReturnType; export const oidcConfigDALFactory = (db: TDbClient) => { const oidcCfgOrm = ormify(db, TableName.OidcConfig); - const findEnforceableOidcCfg = async (orgId: string) => { - try { - const oidcCfg = await db - .replicaNode()(TableName.OidcConfig) - .where({ - orgId, - isActive: true - }) - .whereNotNull("lastUsed") - .first(); - - return oidcCfg; - } catch (error) { - throw new DatabaseError({ error, name: "Find org by id" }); - } - }; - - return { ...oidcCfgOrm, findEnforceableOidcCfg }; + return oidcCfgOrm; }; diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index 17c1ddaaf..adfe92341 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -3,28 +3,31 @@ import { ForbiddenError } from "@casl/ability"; import jwt from "jsonwebtoken"; import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client"; -import { OrgMembershipStatus, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas"; +import { OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas"; import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs"; +import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; +import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; +import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; -import { - decryptSymmetric, - encryptSymmetric, - generateAsymmetricKeyPair, - generateSymmetricKey, - infisicalSymmetricDecrypt, - infisicalSymmetricEncypt -} from "@app/lib/crypto/encryption"; -import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; -import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; +import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors"; +import { OrgServiceActor } from "@app/lib/types"; +import { ActorType, AuthMethod, 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 { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; +import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-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"; @@ -45,7 +48,14 @@ import { type TOidcConfigServiceFactoryDep = { userDAL: Pick< TUserDALFactory, - "create" | "findOne" | "transaction" | "updateById" | "findById" | "findUserEncKeyByUserId" + | "create" + | "findOne" + | "updateById" + | "findById" + | "findUserEncKeyByUserId" + | "findUserEncKeyByUserIdsBatch" + | "find" + | "transaction" >; userAliasDAL: Pick; orgDAL: Pick< @@ -53,12 +63,27 @@ type TOidcConfigServiceFactoryDep = { "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" >; orgMembershipDAL: Pick; - orgBotDAL: Pick; licenseService: Pick; tokenService: Pick; - smtpService: Pick; - permissionService: Pick; + smtpService: Pick; + permissionService: Pick; oidcConfigDAL: Pick; + groupDAL: Pick; + userGroupMembershipDAL: Pick< + TUserGroupMembershipDALFactory, + | "find" + | "transaction" + | "insertMany" + | "findGroupMembershipsByUserIdInOrg" + | "delete" + | "filterProjectsByUserMembership" + >; + groupProjectDAL: Pick; + projectKeyDAL: Pick; + projectDAL: Pick; + projectBotDAL: Pick; + auditLogService: Pick; + kmsService: Pick; }; export type TOidcConfigServiceFactory = ReturnType; @@ -71,9 +96,16 @@ export const oidcConfigServiceFactory = ({ licenseService, permissionService, tokenService, - orgBotDAL, smtpService, - oidcConfigDAL + oidcConfigDAL, + userGroupMembershipDAL, + groupDAL, + groupProjectDAL, + projectKeyDAL, + projectDAL, + projectBotDAL, + auditLogService, + kmsService }: TOidcConfigServiceFactoryDep) => { const getOidc = async (dto: TGetOidcCfgDTO) => { const org = await orgDAL.findOne({ slug: dto.orgSlug }); @@ -104,43 +136,19 @@ export const oidcConfigServiceFactory = ({ }); } - // decrypt and return cfg - const orgBot = await orgBotDAL.findOne({ orgId: oidcCfg.orgId }); - if (!orgBot) { - throw new NotFoundError({ - message: `Organization bot for organization with ID '${oidcCfg.orgId}' not found`, - name: "OrgBotNotFound" - }); - } - - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: oidcCfg.orgId }); - const { encryptedClientId, clientIdIV, clientIdTag, encryptedClientSecret, clientSecretIV, clientSecretTag } = - oidcCfg; - let clientId = ""; - if (encryptedClientId && clientIdIV && clientIdTag) { - clientId = decryptSymmetric({ - ciphertext: encryptedClientId, - key, - tag: clientIdTag, - iv: clientIdIV - }); + if (oidcCfg.encryptedOidcClientId) { + clientId = decryptor({ cipherTextBlob: oidcCfg.encryptedOidcClientId }).toString(); } let clientSecret = ""; - if (encryptedClientSecret && clientSecretIV && clientSecretTag) { - clientSecret = decryptSymmetric({ - key, - tag: clientSecretTag, - iv: clientSecretIV, - ciphertext: encryptedClientSecret - }); + if (oidcCfg.encryptedOidcClientSecret) { + clientSecret = decryptor({ cipherTextBlob: oidcCfg.encryptedOidcClientSecret }).toString(); } return { @@ -156,11 +164,22 @@ export const oidcConfigServiceFactory = ({ isActive: oidcCfg.isActive, allowedEmailDomains: oidcCfg.allowedEmailDomains, clientId, - clientSecret + clientSecret, + manageGroupMemberships: oidcCfg.manageGroupMemberships, + jwtSignatureAlgorithm: oidcCfg.jwtSignatureAlgorithm }; }; - const oidcLogin = async ({ externalId, email, firstName, lastName, orgId, callbackPort }: TOidcLoginDTO) => { + const oidcLogin = async ({ + externalId, + email, + firstName, + lastName, + orgId, + callbackPort, + groups = [], + manageGroupMemberships + }: TOidcLoginDTO) => { const serverCfg = await getServerCfg(); if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.OIDC)) { @@ -223,6 +242,7 @@ export const oidcConfigServiceFactory = ({ let newUser: TUsers | undefined; if (serverCfg.trustOidcEmails) { + // we prioritize getting the most complete user to create the new alias under newUser = await userDAL.findOne( { email, @@ -230,6 +250,23 @@ export const oidcConfigServiceFactory = ({ }, tx ); + + if (!newUser) { + // this fetches user entries created via invites + newUser = await userDAL.findOne( + { + username: email + }, + tx + ); + + if (newUser && !newUser.isEmailVerified) { + // we automatically mark it as email-verified because we've configured trust for OIDC emails + newUser = await userDAL.updateById(newUser.id, { + isEmailVerified: true + }); + } + } } if (!newUser) { @@ -297,6 +334,83 @@ export const oidcConfigServiceFactory = ({ }); } + if (manageGroupMemberships) { + const userGroups = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(user.id, orgId); + const orgGroups = await groupDAL.findByOrgId(orgId); + + const userGroupsNames = userGroups.map((membership) => membership.groupName); + const missingGroupsMemberships = groups.filter((groupName) => !userGroupsNames.includes(groupName)); + const groupsToAddUserTo = orgGroups.filter((group) => missingGroupsMemberships.includes(group.name)); + + for await (const group of groupsToAddUserTo) { + await addUsersToGroupByUserIds({ + userIds: [user.id], + group, + userDAL, + userGroupMembershipDAL, + orgDAL, + groupProjectDAL, + projectKeyDAL, + projectDAL, + projectBotDAL + }); + } + + if (groupsToAddUserTo.length) { + await auditLogService.createAuditLog({ + actor: { + type: ActorType.PLATFORM, + metadata: {} + }, + orgId, + event: { + type: EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER, + metadata: { + userId: user.id, + userEmail: user.email ?? user.username, + assignedToGroups: groupsToAddUserTo.map(({ id, name }) => ({ id, name })), + userGroupsClaim: groups + } + } + }); + } + + const membershipsToRemove = userGroups + .filter((membership) => !groups.includes(membership.groupName)) + .map((membership) => membership.groupId); + const groupsToRemoveUserFrom = orgGroups.filter((group) => membershipsToRemove.includes(group.id)); + + for await (const group of groupsToRemoveUserFrom) { + await removeUsersFromGroupByUserIds({ + userIds: [user.id], + group, + userDAL, + userGroupMembershipDAL, + groupProjectDAL, + projectKeyDAL + }); + } + + if (groupsToRemoveUserFrom.length) { + await auditLogService.createAuditLog({ + actor: { + type: ActorType.PLATFORM, + metadata: {} + }, + orgId, + event: { + type: EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER, + metadata: { + userId: user.id, + userEmail: user.email ?? user.username, + removedFromGroups: groupsToRemoveUserFrom.map(({ id, name }) => ({ id, name })), + userGroupsClaim: groups + } + } + }); + } + } + await licenseService.updateSubscriptionOrgMemberCount(organization.id); const userEnc = await userDAL.findUserEncKeyByUserId(user.id); @@ -332,14 +446,20 @@ export const oidcConfigServiceFactory = ({ userId: user.id }); - await smtpService.sendMail({ - template: SmtpTemplates.EmailVerification, - subjectLine: "Infisical confirmation code", - recipients: [user.email], - substitutions: { - code: token - } - }); + await smtpService + .sendMail({ + template: SmtpTemplates.EmailVerification, + subjectLine: "Infisical confirmation code", + recipients: [user.email], + substitutions: { + code: token + } + }) + .catch((err: Error) => { + throw new OidcAuthError({ + message: `Error sending email confirmation code for user registration - contact the Infisical instance admin. ${err.message}` + }); + }); } return { isUserCompleted, providerAuthToken }; @@ -361,7 +481,9 @@ export const oidcConfigServiceFactory = ({ tokenEndpoint, userinfoEndpoint, clientId, - clientSecret + clientSecret, + manageGroupMemberships, + jwtSignatureAlgorithm }: TUpdateOidcCfgDTO) => { const org = await orgDAL.findOne({ slug: orgSlug @@ -389,19 +511,22 @@ export const oidcConfigServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); - const orgBot = await orgBotDAL.findOne({ orgId: org.id }); - if (!orgBot) - throw new NotFoundError({ - message: `Organization bot for organization with ID '${org.id}' not found`, - name: "OrgBotNotFound" - }); - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: org.id }); + const serverCfg = await getServerCfg(); + if (isActive && !serverCfg.trustOidcEmails) { + const isSmtpConnected = await smtpService.verify(); + if (!isSmtpConnected) { + throw new BadRequestError({ + message: + "Cannot enable OIDC when there are issues with the instance's SMTP configuration. Bypass this by turning on trust for OIDC emails in the server admin console." + }); + } + } + const updateQuery: TOidcConfigsUpdate = { allowedEmailDomains, configurationType, @@ -412,26 +537,17 @@ export const oidcConfigServiceFactory = ({ userinfoEndpoint, jwksUri, isActive, - lastUsed: null + lastUsed: null, + manageGroupMemberships, + jwtSignatureAlgorithm }; if (clientId !== undefined) { - const { ciphertext: encryptedClientId, iv: clientIdIV, tag: clientIdTag } = encryptSymmetric(clientId, key); - updateQuery.encryptedClientId = encryptedClientId; - updateQuery.clientIdIV = clientIdIV; - updateQuery.clientIdTag = clientIdTag; + updateQuery.encryptedOidcClientId = encryptor({ plainText: Buffer.from(clientId) }).cipherTextBlob; } if (clientSecret !== undefined) { - const { - ciphertext: encryptedClientSecret, - iv: clientSecretIV, - tag: clientSecretTag - } = encryptSymmetric(clientSecret, key); - - updateQuery.encryptedClientSecret = encryptedClientSecret; - updateQuery.clientSecretIV = clientSecretIV; - updateQuery.clientSecretTag = clientSecretTag; + updateQuery.encryptedOidcClientSecret = encryptor({ plainText: Buffer.from(clientSecret) }).cipherTextBlob; } const [ssoConfig] = await oidcConfigDAL.update({ orgId: org.id }, updateQuery); @@ -455,7 +571,9 @@ export const oidcConfigServiceFactory = ({ tokenEndpoint, userinfoEndpoint, clientId, - clientSecret + clientSecret, + manageGroupMemberships, + jwtSignatureAlgorithm }: TCreateOidcCfgDTO) => { const org = await orgDAL.findOne({ slug: orgSlug @@ -482,61 +600,11 @@ export const oidcConfigServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso); - const orgBot = await orgBotDAL.transaction(async (tx) => { - const doc = await orgBotDAL.findOne({ orgId: org.id }, tx); - if (doc) return doc; - - const { privateKey, publicKey } = generateAsymmetricKeyPair(); - const key = generateSymmetricKey(); - const { - ciphertext: encryptedPrivateKey, - iv: privateKeyIV, - tag: privateKeyTag, - encoding: privateKeyKeyEncoding, - algorithm: privateKeyAlgorithm - } = infisicalSymmetricEncypt(privateKey); - const { - ciphertext: encryptedSymmetricKey, - iv: symmetricKeyIV, - tag: symmetricKeyTag, - encoding: symmetricKeyKeyEncoding, - algorithm: symmetricKeyAlgorithm - } = infisicalSymmetricEncypt(key); - - return orgBotDAL.create( - { - name: "Infisical org bot", - publicKey, - privateKeyIV, - encryptedPrivateKey, - symmetricKeyIV, - symmetricKeyTag, - encryptedSymmetricKey, - symmetricKeyAlgorithm, - orgId: org.id, - privateKeyTag, - privateKeyAlgorithm, - privateKeyKeyEncoding, - symmetricKeyKeyEncoding - }, - tx - ); + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: org.id }); - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding - }); - - const { ciphertext: encryptedClientId, iv: clientIdIV, tag: clientIdTag } = encryptSymmetric(clientId, key); - const { - ciphertext: encryptedClientSecret, - iv: clientSecretIV, - tag: clientSecretTag - } = encryptSymmetric(clientSecret, key); - const oidcCfg = await oidcConfigDAL.create({ issuer, isActive, @@ -548,12 +616,10 @@ export const oidcConfigServiceFactory = ({ tokenEndpoint, userinfoEndpoint, orgId: org.id, - encryptedClientId, - clientIdIV, - clientIdTag, - encryptedClientSecret, - clientSecretIV, - clientSecretTag + manageGroupMemberships, + jwtSignatureAlgorithm, + encryptedOidcClientId: encryptor({ plainText: Buffer.from(clientId) }).cipherTextBlob, + encryptedOidcClientSecret: encryptor({ plainText: Buffer.from(clientSecret) }).cipherTextBlob }); return oidcCfg; @@ -615,7 +681,8 @@ export const oidcConfigServiceFactory = ({ const client = new issuer.Client({ client_id: oidcCfg.clientId, client_secret: oidcCfg.clientSecret, - redirect_uris: [`${appCfg.SITE_URL}/api/v1/sso/oidc/callback`] + redirect_uris: [`${appCfg.SITE_URL}/api/v1/sso/oidc/callback`], + id_token_signed_response_alg: oidcCfg.jwtSignatureAlgorithm }); const strategy = new OpenIdStrategy( @@ -647,7 +714,9 @@ export const oidcConfigServiceFactory = ({ firstName: claims.given_name ?? "", lastName: claims.family_name ?? "", orgId: org.id, - callbackPort + groups: claims.groups as string[] | undefined, + callbackPort, + manageGroupMemberships: oidcCfg.manageGroupMemberships }) .then(({ isUserCompleted, providerAuthToken }) => { cb(null, { isUserCompleted, providerAuthToken }); @@ -661,5 +730,16 @@ export const oidcConfigServiceFactory = ({ return strategy; }; - return { oidcLogin, getOrgAuthStrategy, getOidc, updateOidcCfg, createOidcCfg }; + const isOidcManageGroupMembershipsEnabled = async (orgId: string, actor: OrgServiceActor) => { + await permissionService.getUserOrgPermission(actor.id, orgId, actor.authMethod, actor.orgId); + + const oidcConfig = await oidcConfigDAL.findOne({ + orgId, + isActive: true + }); + + return Boolean(oidcConfig?.manageGroupMemberships); + }; + + return { oidcLogin, getOrgAuthStrategy, getOidc, updateOidcCfg, createOidcCfg, isOidcManageGroupMembershipsEnabled }; }; diff --git a/backend/src/ee/services/oidc/oidc-config-types.ts b/backend/src/ee/services/oidc/oidc-config-types.ts index 6e36b796b..3b2194375 100644 --- a/backend/src/ee/services/oidc/oidc-config-types.ts +++ b/backend/src/ee/services/oidc/oidc-config-types.ts @@ -5,6 +5,12 @@ export enum OIDCConfigurationType { DISCOVERY_URL = "discoveryURL" } +export enum OIDCJWTSignatureAlgorithm { + RS256 = "RS256", + HS256 = "HS256", + RS512 = "RS512" +} + export type TOidcLoginDTO = { externalId: string; email: string; @@ -12,6 +18,8 @@ export type TOidcLoginDTO = { lastName?: string; orgId: string; callbackPort?: string; + groups?: string[]; + manageGroupMemberships?: boolean | null; }; export type TGetOidcCfgDTO = @@ -37,6 +45,8 @@ export type TCreateOidcCfgDTO = { clientSecret: string; isActive: boolean; orgSlug: string; + manageGroupMemberships: boolean; + jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm; } & TGenericPermission; export type TUpdateOidcCfgDTO = Partial<{ @@ -52,5 +62,7 @@ export type TUpdateOidcCfgDTO = Partial<{ clientSecret: string; isActive: boolean; orgSlug: string; + manageGroupMemberships: boolean; + jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm; }> & TGenericPermission; diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index e0da494c6..17b4e7f6c 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -1,4 +1,12 @@ -import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability"; +import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability"; +import { z } from "zod"; + +import { + CASL_ACTION_SCHEMA_ENUM, + CASL_ACTION_SCHEMA_NATIVE_ENUM +} from "@app/ee/services/permission/permission-schemas"; +import { PermissionConditionSchema } from "@app/ee/services/permission/permission-types"; +import { PermissionConditionOperators } from "@app/lib/casl"; export enum OrgPermissionActions { Read = "read", @@ -7,10 +15,57 @@ export enum OrgPermissionActions { Delete = "delete" } +export enum OrgPermissionAppConnectionActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + Connect = "connect" +} + +export enum OrgPermissionKmipActions { + Proxy = "proxy", + Setup = "setup" +} + export enum OrgPermissionAdminConsoleAction { AccessAllProjects = "access-all-projects" } +export enum OrgPermissionSecretShareAction { + ManageSettings = "manage-settings" +} + +export enum OrgPermissionGatewayActions { + // is there a better word for this. This mean can an identity be a gateway + CreateGateways = "create-gateways", + ListGateways = "list-gateways", + EditGateways = "edit-gateways", + DeleteGateways = "delete-gateways" +} + +export enum OrgPermissionIdentityActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + GrantPrivileges = "grant-privileges", + RevokeAuth = "revoke-auth", + CreateToken = "create-token", + GetToken = "get-token", + DeleteToken = "delete-token" +} + +export enum OrgPermissionGroupActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + GrantPrivileges = "grant-privileges", + AddMembers = "add-members", + RemoveMembers = "remove-members" +} + export enum OrgPermissionSubjects { Workspace = "workspace", Role = "role", @@ -27,11 +82,18 @@ export enum OrgPermissionSubjects { Kms = "kms", AdminConsole = "organization-admin-console", AuditLogs = "audit-logs", - ProjectTemplates = "project-templates" + ProjectTemplates = "project-templates", + AppConnections = "app-connections", + Kmip = "kmip", + Gateway = "gateway", + SecretShare = "secret-share" } +export type AppConnectionSubjectFields = { + connectionId: string; +}; + export type OrgPermissionSet = - | [OrgPermissionActions.Read, OrgPermissionSubjects.Workspace] | [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace] | [OrgPermissionActions, OrgPermissionSubjects.Role] | [OrgPermissionActions, OrgPermissionSubjects.Member] @@ -40,19 +102,140 @@ export type OrgPermissionSet = | [OrgPermissionActions, OrgPermissionSubjects.Sso] | [OrgPermissionActions, OrgPermissionSubjects.Scim] | [OrgPermissionActions, OrgPermissionSubjects.Ldap] - | [OrgPermissionActions, OrgPermissionSubjects.Groups] + | [OrgPermissionGroupActions, OrgPermissionSubjects.Groups] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.Billing] - | [OrgPermissionActions, OrgPermissionSubjects.Identity] + | [OrgPermissionIdentityActions, OrgPermissionSubjects.Identity] | [OrgPermissionActions, OrgPermissionSubjects.Kms] | [OrgPermissionActions, OrgPermissionSubjects.AuditLogs] | [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates] - | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]; + | [OrgPermissionGatewayActions, OrgPermissionSubjects.Gateway] + | [ + OrgPermissionAppConnectionActions, + ( + | OrgPermissionSubjects.AppConnections + | (ForcedSubject & AppConnectionSubjectFields) + ) + ] + | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole] + | [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip] + | [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare]; + +const AppConnectionConditionSchema = z + .object({ + connectionId: z.union([ + z.string(), + z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN] + }) + .partial() + ]) + }) + .partial(); + +export const OrgPermissionSchema = z.discriminatedUnion("subject", [ + z.object({ + subject: z.literal(OrgPermissionSubjects.Workspace).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_ENUM([OrgPermissionActions.Create]).describe("Describe what action an entity can take.") + }), + z.object({ + subject: z.literal(OrgPermissionSubjects.Role).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") + }), + z.object({ + subject: z.literal(OrgPermissionSubjects.Member).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") + }), + z.object({ + subject: z.literal(OrgPermissionSubjects.Settings).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") + }), + z.object({ + subject: z.literal(OrgPermissionSubjects.IncidentAccount).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") + }), + z.object({ + subject: z.literal(OrgPermissionSubjects.Sso).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") + }), + z.object({ + subject: z.literal(OrgPermissionSubjects.Scim).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") + }), + z.object({ + subject: z.literal(OrgPermissionSubjects.Ldap).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") + }), + z.object({ + subject: z.literal(OrgPermissionSubjects.Groups).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") + }), + z.object({ + subject: z.literal(OrgPermissionSubjects.SecretScanning).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") + }), + z.object({ + subject: z.literal(OrgPermissionSubjects.Billing).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") + }), + z.object({ + subject: z.literal(OrgPermissionSubjects.Identity).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") + }), + z.object({ + subject: z.literal(OrgPermissionSubjects.Kms).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") + }), + z.object({ + subject: z.literal(OrgPermissionSubjects.AuditLogs).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") + }), + z.object({ + subject: z.literal(OrgPermissionSubjects.ProjectTemplates).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") + }), + z.object({ + subject: z.literal(OrgPermissionSubjects.AppConnections).describe("The entity this permission pertains to."), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionAppConnectionActions).describe( + "Describe what action an entity can take." + ), + conditions: AppConnectionConditionSchema.describe( + "When specified, only matching conditions will be allowed to access given resource." + ).optional() + }), + z.object({ + subject: z.literal(OrgPermissionSubjects.AdminConsole).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionAdminConsoleAction).describe( + "Describe what action an entity can take." + ) + }), + z.object({ + subject: z.literal(OrgPermissionSubjects.SecretShare).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionSecretShareAction).describe( + "Describe what action an entity can take." + ) + }), + z.object({ + subject: z.literal(OrgPermissionSubjects.Kmip).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionKmipActions).describe( + "Describe what action an entity can take." + ) + }), + z.object({ + subject: z.literal(OrgPermissionSubjects.Gateway).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionGatewayActions).describe( + "Describe what action an entity can take." + ) + }) +]); const buildAdminPermission = () => { const { can, rules } = new AbilityBuilder>(createMongoAbility); // ws permissions - can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); // role permission can(OrgPermissionActions.Read, OrgPermissionSubjects.Role); @@ -95,20 +278,28 @@ const buildAdminPermission = () => { can(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Ldap); - can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups); - can(OrgPermissionActions.Create, OrgPermissionSubjects.Groups); - can(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups); - can(OrgPermissionActions.Delete, OrgPermissionSubjects.Groups); + can(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); + can(OrgPermissionGroupActions.Create, OrgPermissionSubjects.Groups); + can(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); + can(OrgPermissionGroupActions.Delete, OrgPermissionSubjects.Groups); + can(OrgPermissionGroupActions.GrantPrivileges, OrgPermissionSubjects.Groups); + can(OrgPermissionGroupActions.AddMembers, OrgPermissionSubjects.Groups); + can(OrgPermissionGroupActions.RemoveMembers, OrgPermissionSubjects.Groups); can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing); - can(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); - can(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); - can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); - can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.GrantPrivileges, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.CreateToken, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.GetToken, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.DeleteToken, OrgPermissionSubjects.Identity); can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms); can(OrgPermissionActions.Create, OrgPermissionSubjects.Kms); @@ -125,8 +316,26 @@ const buildAdminPermission = () => { can(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates); can(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates); + can(OrgPermissionAppConnectionActions.Read, OrgPermissionSubjects.AppConnections); + can(OrgPermissionAppConnectionActions.Create, OrgPermissionSubjects.AppConnections); + can(OrgPermissionAppConnectionActions.Edit, OrgPermissionSubjects.AppConnections); + can(OrgPermissionAppConnectionActions.Delete, OrgPermissionSubjects.AppConnections); + can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections); + + can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway); + can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway); + can(OrgPermissionGatewayActions.EditGateways, OrgPermissionSubjects.Gateway); + can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway); + can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole); + can(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip); + + // the proxy assignment is temporary in order to prevent "more privilege" error during role assignment to MI + can(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); + + can(OrgPermissionSecretShareAction.ManageSettings, OrgPermissionSubjects.SecretShare); + return rules; }; @@ -135,10 +344,9 @@ export const orgAdminPermissions = buildAdminPermission(); const buildMemberPermission = () => { const { can, rules } = new AbilityBuilder>(createMongoAbility); - can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Read, OrgPermissionSubjects.Member); - can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups); + can(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); can(OrgPermissionActions.Read, OrgPermissionSubjects.Role); can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); @@ -149,13 +357,17 @@ const buildMemberPermission = () => { can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning); can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning); - can(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); - can(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); - can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); - can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity); can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs); + can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections); + can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway); + can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway); + return rules; }; diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index 8ad58f528..891d7193e 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { TDbClient } from "@app/db"; import { IdentityProjectMembershipRoleSchema, + OrgMembershipRole, OrgMembershipsSchema, TableName, TProjectRoles, @@ -49,9 +50,11 @@ export const permissionDALFactory = (db: TDbClient) => { .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`) .select( selectAllTableCols(TableName.OrgMembership), + db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization), db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"), db.ref("permissions").withSchema(TableName.OrgRoles), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), + db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"), db.ref("groupId").withSchema("userGroups"), db.ref("groupOrgId").withSchema("userGroups"), db.ref("groupName").withSchema("userGroups"), @@ -70,7 +73,9 @@ export const permissionDALFactory = (db: TDbClient) => { OrgMembershipsSchema.extend({ permissions: z.unknown(), orgAuthEnforced: z.boolean().optional().nullable(), - customRoleSlug: z.string().optional().nullable() + bypassOrgAuthEnabled: z.boolean(), + customRoleSlug: z.string().optional().nullable(), + shouldUseNewPrivilegeSystem: z.boolean() }).parse(el), childrenMapper: [ { @@ -118,23 +123,130 @@ export const permissionDALFactory = (db: TDbClient) => { .select(selectAllTableCols(TableName.IdentityOrgMembership)) .select(db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced")) .select("permissions") + .select(db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization)) .first(); + return membership; } catch (error) { throw new DatabaseError({ error, name: "GetOrgIdentityPermission" }); } }; - const getProjectPermission = async (userId: string, projectId: string) => { + const getProjectGroupPermissions = async (projectId: string) => { + try { + const docs = await db + .replicaNode()(TableName.GroupProjectMembership) + .join(TableName.Groups, `${TableName.Groups}.id`, `${TableName.GroupProjectMembership}.groupId`) + .join( + TableName.GroupProjectMembershipRole, + `${TableName.GroupProjectMembershipRole}.projectMembershipId`, + `${TableName.GroupProjectMembership}.id` + ) + .leftJoin( + { groupCustomRoles: TableName.ProjectRoles }, + `${TableName.GroupProjectMembershipRole}.customRoleId`, + `groupCustomRoles.id` + ) + .where(`${TableName.GroupProjectMembership}.projectId`, "=", projectId) + .select( + db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"), + db.ref("id").withSchema(TableName.Groups).as("groupId"), + db.ref("name").withSchema(TableName.Groups).as("groupName"), + db.ref("slug").withSchema("groupCustomRoles").as("groupProjectMembershipRoleCustomRoleSlug"), + db.ref("permissions").withSchema("groupCustomRoles").as("groupProjectMembershipRolePermission"), + db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("groupProjectMembershipRoleId"), + db.ref("role").withSchema(TableName.GroupProjectMembershipRole).as("groupProjectMembershipRole"), + db + .ref("customRoleId") + .withSchema(TableName.GroupProjectMembershipRole) + .as("groupProjectMembershipRoleCustomRoleId"), + db + .ref("isTemporary") + .withSchema(TableName.GroupProjectMembershipRole) + .as("groupProjectMembershipRoleIsTemporary"), + db + .ref("temporaryMode") + .withSchema(TableName.GroupProjectMembershipRole) + .as("groupProjectMembershipRoleTemporaryMode"), + db + .ref("temporaryRange") + .withSchema(TableName.GroupProjectMembershipRole) + .as("groupProjectMembershipRoleTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.GroupProjectMembershipRole) + .as("groupProjectMembershipRoleTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.GroupProjectMembershipRole) + .as("groupProjectMembershipRoleTemporaryAccessEndTime") + ); + + const groupPermissions = sqlNestRelationships({ + data: docs, + key: "groupId", + parentMapper: ({ groupId, groupName, membershipId }) => ({ + groupId, + username: groupName, + id: membershipId + }), + childrenMapper: [ + { + key: "groupProjectMembershipRoleId", + label: "groupRoles" as const, + mapper: ({ + groupProjectMembershipRoleId, + groupProjectMembershipRole, + groupProjectMembershipRolePermission, + groupProjectMembershipRoleCustomRoleSlug, + groupProjectMembershipRoleIsTemporary, + groupProjectMembershipRoleTemporaryMode, + groupProjectMembershipRoleTemporaryAccessEndTime, + groupProjectMembershipRoleTemporaryAccessStartTime, + groupProjectMembershipRoleTemporaryRange + }) => ({ + id: groupProjectMembershipRoleId, + role: groupProjectMembershipRole, + customRoleSlug: groupProjectMembershipRoleCustomRoleSlug, + permissions: groupProjectMembershipRolePermission, + temporaryRange: groupProjectMembershipRoleTemporaryRange, + temporaryMode: groupProjectMembershipRoleTemporaryMode, + temporaryAccessStartTime: groupProjectMembershipRoleTemporaryAccessStartTime, + temporaryAccessEndTime: groupProjectMembershipRoleTemporaryAccessEndTime, + isTemporary: groupProjectMembershipRoleIsTemporary + }) + } + ] + }); + + return groupPermissions + .map((groupPermission) => { + if (!groupPermission) return undefined; + + const activeGroupRoles = + groupPermission?.groupRoles?.filter( + ({ isTemporary, temporaryAccessEndTime }) => + !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) + ) ?? []; + + return { + ...groupPermission, + roles: activeGroupRoles + }; + }) + .filter((item): item is NonNullable => Boolean(item)); + } catch (error) { + throw new DatabaseError({ error, name: "GetProjectGroupPermissions" }); + } + }; + + const getProjectUserPermissions = async (projectId: string) => { try { const docs = await db .replicaNode()(TableName.Users) - .where(`${TableName.Users}.id`, userId) - .leftJoin(TableName.UserGroupMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) + .where("isGhost", "=", false) .leftJoin(TableName.GroupProjectMembership, (queryBuilder) => { - void queryBuilder - .on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId])) - .andOn(`${TableName.GroupProjectMembership}.groupId`, `${TableName.UserGroupMembership}.groupId`); + void queryBuilder.on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId])); }) .leftJoin( TableName.GroupProjectMembershipRole, @@ -146,7 +258,7 @@ export const permissionDALFactory = (db: TDbClient) => { `${TableName.GroupProjectMembershipRole}.customRoleId`, `groupCustomRoles.id` ) - .leftJoin(TableName.ProjectMembership, (queryBuilder) => { + .join(TableName.ProjectMembership, (queryBuilder) => { void queryBuilder .on(`${TableName.ProjectMembership}.projectId`, db.raw("?", [projectId])) .andOn(`${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`); @@ -268,12 +380,13 @@ export const permissionDALFactory = (db: TDbClient) => { db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("orgId").withSchema(TableName.Project), + db.ref("type").withSchema(TableName.Project).as("projectType"), db.ref("id").withSchema(TableName.Project).as("projectId") ); - const [userPermission] = sqlNestRelationships({ + const userPermissions = sqlNestRelationships({ data: docs, - key: "projectId", + key: "userId", parentMapper: ({ orgId, username, @@ -283,17 +396,327 @@ export const permissionDALFactory = (db: TDbClient) => { membershipCreatedAt, groupMembershipCreatedAt, groupMembershipUpdatedAt, - membershipUpdatedAt + membershipUpdatedAt, + projectType, + userId }) => ({ orgId, orgAuthEnforced, userId, projectId, username, + projectType, id: membershipId || groupMembershipId, createdAt: membershipCreatedAt || groupMembershipCreatedAt, updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt }), + childrenMapper: [ + { + key: "userGroupProjectMembershipRoleId", + label: "userGroupRoles" as const, + mapper: ({ + userGroupProjectMembershipRoleId, + userGroupProjectMembershipRole, + userGroupProjectMembershipRolePermission, + userGroupProjectMembershipRoleCustomRoleSlug, + userGroupProjectMembershipRoleIsTemporary, + userGroupProjectMembershipRoleTemporaryMode, + userGroupProjectMembershipRoleTemporaryAccessEndTime, + userGroupProjectMembershipRoleTemporaryAccessStartTime, + userGroupProjectMembershipRoleTemporaryRange + }) => ({ + id: userGroupProjectMembershipRoleId, + role: userGroupProjectMembershipRole, + customRoleSlug: userGroupProjectMembershipRoleCustomRoleSlug, + permissions: userGroupProjectMembershipRolePermission, + temporaryRange: userGroupProjectMembershipRoleTemporaryRange, + temporaryMode: userGroupProjectMembershipRoleTemporaryMode, + temporaryAccessStartTime: userGroupProjectMembershipRoleTemporaryAccessStartTime, + temporaryAccessEndTime: userGroupProjectMembershipRoleTemporaryAccessEndTime, + isTemporary: userGroupProjectMembershipRoleIsTemporary + }) + }, + { + key: "userProjectMembershipRoleId", + label: "projectMembershipRoles" as const, + mapper: ({ + userProjectMembershipRoleId, + userProjectMembershipRole, + userProjectCustomRolePermission, + userProjectMembershipRoleIsTemporary, + userProjectMembershipRoleTemporaryMode, + userProjectMembershipRoleTemporaryRange, + userProjectMembershipRoleTemporaryAccessEndTime, + userProjectMembershipRoleTemporaryAccessStartTime, + userProjectMembershipRoleCustomRoleSlug + }) => ({ + id: userProjectMembershipRoleId, + role: userProjectMembershipRole, + customRoleSlug: userProjectMembershipRoleCustomRoleSlug, + permissions: userProjectCustomRolePermission, + temporaryRange: userProjectMembershipRoleTemporaryRange, + temporaryMode: userProjectMembershipRoleTemporaryMode, + temporaryAccessStartTime: userProjectMembershipRoleTemporaryAccessStartTime, + temporaryAccessEndTime: userProjectMembershipRoleTemporaryAccessEndTime, + isTemporary: userProjectMembershipRoleIsTemporary + }) + }, + { + key: "userAdditionalPrivilegesId", + label: "additionalPrivileges" as const, + mapper: ({ + userAdditionalPrivilegesId, + userAdditionalPrivilegesPermissions, + userAdditionalPrivilegesIsTemporary, + userAdditionalPrivilegesTemporaryMode, + userAdditionalPrivilegesTemporaryRange, + userAdditionalPrivilegesTemporaryAccessEndTime, + userAdditionalPrivilegesTemporaryAccessStartTime + }) => ({ + id: userAdditionalPrivilegesId, + permissions: userAdditionalPrivilegesPermissions, + temporaryRange: userAdditionalPrivilegesTemporaryRange, + temporaryMode: userAdditionalPrivilegesTemporaryMode, + temporaryAccessStartTime: userAdditionalPrivilegesTemporaryAccessStartTime, + temporaryAccessEndTime: userAdditionalPrivilegesTemporaryAccessEndTime, + isTemporary: userAdditionalPrivilegesIsTemporary + }) + }, + { + key: "metadataId", + label: "metadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) + } + ] + }); + + return userPermissions + .map((userPermission) => { + if (!userPermission) return undefined; + if (!userPermission?.userGroupRoles?.[0] && !userPermission?.projectMembershipRoles?.[0]) return undefined; + + // when introducting cron mode change it here + const activeRoles = + userPermission?.projectMembershipRoles?.filter( + ({ isTemporary, temporaryAccessEndTime }) => + !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) + ) ?? []; + + const activeGroupRoles = + userPermission?.userGroupRoles?.filter( + ({ isTemporary, temporaryAccessEndTime }) => + !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) + ) ?? []; + + const activeAdditionalPrivileges = + userPermission?.additionalPrivileges?.filter( + ({ isTemporary, temporaryAccessEndTime }) => + !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) + ) ?? []; + + return { + ...userPermission, + roles: [...activeRoles, ...activeGroupRoles], + additionalPrivileges: activeAdditionalPrivileges + }; + }) + .filter((item): item is NonNullable => Boolean(item)); + } catch (error) { + throw new DatabaseError({ error, name: "GetProjectUserPermissions" }); + } + }; + + const getProjectPermission = async (userId: string, projectId: string) => { + try { + const subQueryUserGroups = db(TableName.UserGroupMembership).where("userId", userId).select("groupId"); + const docs = await db + .replicaNode()(TableName.Users) + .where(`${TableName.Users}.id`, userId) + .leftJoin(TableName.GroupProjectMembership, (queryBuilder) => { + void queryBuilder + .on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId])) + // @ts-expect-error akhilmhdh: this is valid knexjs query. Its just ts type argument is missing it + .andOnIn(`${TableName.GroupProjectMembership}.groupId`, subQueryUserGroups); + }) + .leftJoin( + TableName.GroupProjectMembershipRole, + `${TableName.GroupProjectMembershipRole}.projectMembershipId`, + `${TableName.GroupProjectMembership}.id` + ) + .leftJoin( + { groupCustomRoles: TableName.ProjectRoles }, + `${TableName.GroupProjectMembershipRole}.customRoleId`, + `groupCustomRoles.id` + ) + .leftJoin(TableName.ProjectMembership, (queryBuilder) => { + void queryBuilder + .on(`${TableName.ProjectMembership}.projectId`, db.raw("?", [projectId])) + .andOn(`${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`); + }) + .leftJoin( + TableName.ProjectUserMembershipRole, + `${TableName.ProjectUserMembershipRole}.projectMembershipId`, + `${TableName.ProjectMembership}.id` + ) + .leftJoin( + TableName.ProjectRoles, + `${TableName.ProjectUserMembershipRole}.customRoleId`, + `${TableName.ProjectRoles}.id` + ) + .leftJoin(TableName.ProjectUserAdditionalPrivilege, (queryBuilder) => { + void queryBuilder + .on(`${TableName.ProjectUserAdditionalPrivilege}.projectId`, db.raw("?", [projectId])) + .andOn(`${TableName.ProjectUserAdditionalPrivilege}.userId`, `${TableName.Users}.id`); + }) + .join(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId])) + .join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) + .join(TableName.OrgMembership, (qb) => { + void qb + .on(`${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .andOn(`${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`); + }) + .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { + void queryBuilder + .on(`${TableName.Users}.id`, `${TableName.IdentityMetadata}.userId`) + .andOn(`${TableName.Organization}.id`, `${TableName.IdentityMetadata}.orgId`); + }) + .select( + db.ref("id").withSchema(TableName.Users).as("userId"), + db.ref("username").withSchema(TableName.Users).as("username"), + // groups specific + db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"), + db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"), + db.ref("updatedAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipUpdatedAt"), + db.ref("slug").withSchema("groupCustomRoles").as("userGroupProjectMembershipRoleCustomRoleSlug"), + db.ref("permissions").withSchema("groupCustomRoles").as("userGroupProjectMembershipRolePermission"), + db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRoleId"), + db.ref("role").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRole"), + db + .ref("customRoleId") + .withSchema(TableName.GroupProjectMembershipRole) + .as("userGroupProjectMembershipRoleCustomRoleId"), + db + .ref("isTemporary") + .withSchema(TableName.GroupProjectMembershipRole) + .as("userGroupProjectMembershipRoleIsTemporary"), + db + .ref("temporaryMode") + .withSchema(TableName.GroupProjectMembershipRole) + .as("userGroupProjectMembershipRoleTemporaryMode"), + db + .ref("temporaryRange") + .withSchema(TableName.GroupProjectMembershipRole) + .as("userGroupProjectMembershipRoleTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.GroupProjectMembershipRole) + .as("userGroupProjectMembershipRoleTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.GroupProjectMembershipRole) + .as("userGroupProjectMembershipRoleTemporaryAccessEndTime"), + // user specific + db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"), + db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"), + db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"), + db.ref("slug").withSchema(TableName.ProjectRoles).as("userProjectMembershipRoleCustomRoleSlug"), + db.ref("permissions").withSchema(TableName.ProjectRoles).as("userProjectCustomRolePermission"), + db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRoleId"), + db.ref("role").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRole"), + db + .ref("temporaryMode") + .withSchema(TableName.ProjectUserMembershipRole) + .as("userProjectMembershipRoleTemporaryMode"), + db + .ref("isTemporary") + .withSchema(TableName.ProjectUserMembershipRole) + .as("userProjectMembershipRoleIsTemporary"), + db + .ref("temporaryRange") + .withSchema(TableName.ProjectUserMembershipRole) + .as("userProjectMembershipRoleTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.ProjectUserMembershipRole) + .as("userProjectMembershipRoleTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.ProjectUserMembershipRole) + .as("userProjectMembershipRoleTemporaryAccessEndTime"), + db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesId"), + db + .ref("permissions") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("userAdditionalPrivilegesPermissions"), + db + .ref("temporaryMode") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("userAdditionalPrivilegesTemporaryMode"), + db + .ref("isTemporary") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("userAdditionalPrivilegesIsTemporary"), + db + .ref("temporaryRange") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("userAdditionalPrivilegesTemporaryRange"), + db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesUserId"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("userAdditionalPrivilegesTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("userAdditionalPrivilegesTemporaryAccessEndTime"), + // general + db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"), + db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), + db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"), + db.ref("role").withSchema(TableName.OrgMembership).as("orgRole"), + db.ref("orgId").withSchema(TableName.Project), + db.ref("type").withSchema(TableName.Project).as("projectType"), + db.ref("id").withSchema(TableName.Project).as("projectId"), + db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization) + ); + + const [userPermission] = sqlNestRelationships({ + data: docs, + key: "projectId", + parentMapper: ({ + orgId, + username, + orgAuthEnforced, + orgRole, + membershipId, + groupMembershipId, + membershipCreatedAt, + groupMembershipCreatedAt, + groupMembershipUpdatedAt, + membershipUpdatedAt, + projectType, + shouldUseNewPrivilegeSystem, + bypassOrgAuthEnabled + }) => ({ + orgId, + orgAuthEnforced, + orgRole: orgRole as OrgMembershipRole, + userId, + projectId, + username, + projectType, + id: membershipId || groupMembershipId, + createdAt: membershipCreatedAt || groupMembershipCreatedAt, + updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt, + shouldUseNewPrivilegeSystem, + bypassOrgAuthEnabled + }), childrenMapper: [ { key: "userGroupProjectMembershipRoleId", @@ -410,7 +833,7 @@ export const permissionDALFactory = (db: TDbClient) => { } }; - const getProjectIdentityPermission = async (identityId: string, projectId: string) => { + const getProjectIdentityPermissions = async (projectId: string) => { try { const docs = await db .replicaNode()(TableName.IdentityProjectMembership) @@ -441,13 +864,14 @@ export const permissionDALFactory = (db: TDbClient) => { .on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`) .andOn(`${TableName.Project}.orgId`, `${TableName.IdentityMetadata}.orgId`); }) - .where(`${TableName.IdentityProjectMembership}.identityId`, identityId) .where(`${TableName.IdentityProjectMembership}.projectId`, projectId) .select(selectAllTableCols(TableName.IdentityProjectMembershipRole)) .select( db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"), + db.ref("id").withSchema(TableName.Identity).as("identityId"), db.ref("name").withSchema(TableName.Identity).as("identityName"), db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project + db.ref("type").withSchema(TableName.Project).as("projectType"), db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"), db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"), db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), @@ -476,10 +900,18 @@ export const permissionDALFactory = (db: TDbClient) => { db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue") ); - const permission = sqlNestRelationships({ + const permissions = sqlNestRelationships({ data: docs, - key: "membershipId", - parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId, identityName }) => ({ + key: "identityId", + parentMapper: ({ + membershipId, + membershipCreatedAt, + membershipUpdatedAt, + orgId, + identityName, + projectType, + identityId + }) => ({ id: membershipId, identityId, username: identityName, @@ -487,6 +919,167 @@ export const permissionDALFactory = (db: TDbClient) => { createdAt: membershipCreatedAt, updatedAt: membershipUpdatedAt, orgId, + projectType, + // just a prefilled value + orgAuthEnforced: false + }), + childrenMapper: [ + { + key: "id", + label: "roles" as const, + mapper: (data) => + IdentityProjectMembershipRoleSchema.extend({ + permissions: z.unknown(), + customRoleSlug: z.string().optional().nullable() + }).parse(data) + }, + { + key: "identityApId", + label: "additionalPrivileges" as const, + mapper: ({ + identityApId, + identityApPermissions, + identityApIsTemporary, + identityApTemporaryMode, + identityApTemporaryRange, + identityApTemporaryAccessEndTime, + identityApTemporaryAccessStartTime + }) => ({ + id: identityApId, + permissions: identityApPermissions, + temporaryRange: identityApTemporaryRange, + temporaryMode: identityApTemporaryMode, + temporaryAccessEndTime: identityApTemporaryAccessEndTime, + temporaryAccessStartTime: identityApTemporaryAccessStartTime, + isTemporary: identityApIsTemporary + }) + }, + { + key: "metadataId", + label: "metadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) + } + ] + }); + + return permissions + .map((permission) => { + if (!permission) { + return undefined; + } + + // when introducting cron mode change it here + const activeRoles = permission?.roles.filter( + ({ isTemporary, temporaryAccessEndTime }) => + !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) + ); + const activeAdditionalPrivileges = permission?.additionalPrivileges?.filter( + ({ isTemporary, temporaryAccessEndTime }) => + !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) + ); + + return { ...permission, roles: activeRoles, additionalPrivileges: activeAdditionalPrivileges }; + }) + .filter((item): item is NonNullable => Boolean(item)); + } catch (error) { + throw new DatabaseError({ error, name: "GetProjectIdentityPermissions" }); + } + }; + + const getProjectIdentityPermission = async (identityId: string, projectId: string) => { + try { + const docs = await db + .replicaNode()(TableName.IdentityProjectMembership) + .join( + TableName.IdentityProjectMembershipRole, + `${TableName.IdentityProjectMembershipRole}.projectMembershipId`, + `${TableName.IdentityProjectMembership}.id` + ) + .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityProjectMembership}.identityId`) + .leftJoin( + TableName.ProjectRoles, + `${TableName.IdentityProjectMembershipRole}.customRoleId`, + `${TableName.ProjectRoles}.id` + ) + .leftJoin( + TableName.IdentityProjectAdditionalPrivilege, + `${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId`, + `${TableName.IdentityProjectMembership}.id` + ) + .join( + // Join the Project table to later select orgId + TableName.Project, + `${TableName.IdentityProjectMembership}.projectId`, + `${TableName.Project}.id` + ) + .join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) + .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { + void queryBuilder + .on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`) + .andOn(`${TableName.Project}.orgId`, `${TableName.IdentityMetadata}.orgId`); + }) + .where(`${TableName.IdentityProjectMembership}.identityId`, identityId) + .where(`${TableName.IdentityProjectMembership}.projectId`, projectId) + .select(selectAllTableCols(TableName.IdentityProjectMembershipRole)) + .select( + db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"), + db.ref("name").withSchema(TableName.Identity).as("identityName"), + db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project + db.ref("type").withSchema(TableName.Project).as("projectType"), + db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"), + db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"), + db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), + db.ref("permissions").withSchema(TableName.ProjectRoles), + db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization), + db.ref("id").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApId"), + db.ref("permissions").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApPermissions"), + db + .ref("temporaryMode") + .withSchema(TableName.IdentityProjectAdditionalPrivilege) + .as("identityApTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApIsTemporary"), + db + .ref("temporaryRange") + .withSchema(TableName.IdentityProjectAdditionalPrivilege) + .as("identityApTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.IdentityProjectAdditionalPrivilege) + .as("identityApTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.IdentityProjectAdditionalPrivilege) + .as("identityApTemporaryAccessEndTime"), + db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue") + ); + + const permission = sqlNestRelationships({ + data: docs, + key: "membershipId", + parentMapper: ({ + membershipId, + membershipCreatedAt, + membershipUpdatedAt, + orgId, + identityName, + projectType, + shouldUseNewPrivilegeSystem + }) => ({ + id: membershipId, + identityId, + username: identityName, + projectId, + createdAt: membershipCreatedAt, + updatedAt: membershipUpdatedAt, + orgId, + projectType, + shouldUseNewPrivilegeSystem, // just a prefilled value orgAuthEnforced: false }), @@ -555,6 +1148,9 @@ export const permissionDALFactory = (db: TDbClient) => { getOrgPermission, getOrgIdentityPermission, getProjectPermission, - getProjectIdentityPermission + getProjectIdentityPermission, + getProjectUserPermissions, + getProjectIdentityPermissions, + getProjectGroupPermissions }; }; diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts index 1ccee129f..d645e2bec 100644 --- a/backend/src/ee/services/permission/permission-fns.ts +++ b/backend/src/ee/services/permission/permission-fns.ts @@ -1,7 +1,111 @@ -import { TOrganizations } from "@app/db/schemas"; -import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; +/* eslint-disable no-nested-ternary */ +import { ForbiddenError, MongoAbility, PureAbility, subject } from "@casl/ability"; +import { z } from "zod"; + +import { OrgMembershipRole, TOrganizations } from "@app/db/schemas"; +import { validatePermissionBoundary } from "@app/lib/casl/boundary"; +import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type"; +import { OrgPermissionSet } from "./org-permission"; +import { + ProjectPermissionSecretActions, + ProjectPermissionSet, + ProjectPermissionSub, + ProjectPermissionV2Schema, + SecretSubjectFields +} from "./project-permission"; + +export function throwIfMissingSecretReadValueOrDescribePermission( + permission: MongoAbility | PureAbility, + action: Extract< + ProjectPermissionSecretActions, + ProjectPermissionSecretActions.ReadValue | ProjectPermissionSecretActions.DescribeSecret + >, + subjectFields?: SecretSubjectFields +) { + try { + if (subjectFields) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.DescribeAndReadValue, + subject(ProjectPermissionSub.Secrets, subjectFields) + ); + } else { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.DescribeAndReadValue, + ProjectPermissionSub.Secrets + ); + } + } catch { + if (subjectFields) { + ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, subjectFields)); + } else { + ForbiddenError.from(permission).throwUnlessCan(action, ProjectPermissionSub.Secrets); + } + } +} + +export function hasSecretReadValueOrDescribePermission( + permission: MongoAbility, + action: Extract< + ProjectPermissionSecretActions, + ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue + >, + subjectFields?: SecretSubjectFields +) { + let canNewPermission = false; + let canOldPermission = false; + + if (subjectFields) { + canNewPermission = permission.can(action, subject(ProjectPermissionSub.Secrets, subjectFields)); + canOldPermission = permission.can( + ProjectPermissionSecretActions.DescribeAndReadValue, + subject(ProjectPermissionSub.Secrets, subjectFields) + ); + } else { + canNewPermission = permission.can(action, ProjectPermissionSub.Secrets); + canOldPermission = permission.can( + ProjectPermissionSecretActions.DescribeAndReadValue, + ProjectPermissionSub.Secrets + ); + } + + return canNewPermission || canOldPermission; +} + +const OptionalArrayPermissionSchema = ProjectPermissionV2Schema.array().optional(); +export function checkForInvalidPermissionCombination(permissions: z.infer) { + if (!permissions) return; + + for (const permission of permissions) { + if (permission.subject === ProjectPermissionSub.Secrets) { + if (permission.action.includes(ProjectPermissionSecretActions.DescribeAndReadValue)) { + const hasReadValue = permission.action.includes(ProjectPermissionSecretActions.ReadValue); + const hasDescribeSecret = permission.action.includes(ProjectPermissionSecretActions.DescribeSecret); + + // eslint-disable-next-line no-continue + if (!hasReadValue && !hasDescribeSecret) continue; + + const hasBothDescribeAndReadValue = hasReadValue && hasDescribeSecret; + + throw new BadRequestError({ + message: `You have selected Read, and ${ + hasBothDescribeAndReadValue + ? "both Read Value and Describe Secret" + : hasReadValue + ? "Read Value" + : hasDescribeSecret + ? "Describe Secret" + : "" + }. You cannot select Read Value or Describe Secret if you have selected Read. The Read permission is a legacy action which has been replaced by Describe Secret and Read Value.` + }); + } + } + } + + return true; +} + function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) { if (!actorAuthMethod) return false; @@ -14,11 +118,20 @@ function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) { ].includes(actorAuthMethod); } -function validateOrgSSO(actorAuthMethod: ActorAuthMethod, isOrgSsoEnforced: TOrganizations["authEnforced"]) { +function validateOrgSSO( + actorAuthMethod: ActorAuthMethod, + isOrgSsoEnforced: TOrganizations["authEnforced"], + isOrgSsoBypassEnabled: TOrganizations["bypassOrgAuthEnabled"], + orgRole: OrgMembershipRole +) { if (actorAuthMethod === undefined) { throw new UnauthorizedError({ name: "No auth method defined" }); } + if (isOrgSsoEnforced && isOrgSsoBypassEnabled && orgRole === OrgMembershipRole.Admin) { + return; + } + if ( isOrgSsoEnforced && actorAuthMethod !== null && @@ -29,4 +142,71 @@ function validateOrgSSO(actorAuthMethod: ActorAuthMethod, isOrgSsoEnforced: TOrg } } -export { isAuthMethodSaml, validateOrgSSO }; +const escapeHandlebarsMissingDict = (obj: Record, key: string) => { + const handler = { + get(target: Record, prop: string) { + if (!Object.hasOwn(target, prop)) { + // eslint-disable-next-line no-param-reassign + target[prop] = `{{${key}.${prop}}}`; // Add missing key as an "own" property + } + return target[prop]; + } + }; + + return new Proxy(obj, handler); +}; + +// This function serves as a transition layer between the old and new privilege management system +// the old privilege management system is based on the actor having more privileges than the managed permission +// the new privilege management system is based on the actor having the appropriate permission to perform the privilege change, +// regardless of the actor's privilege level. +const validatePrivilegeChangeOperation = ( + shouldUseNewPrivilegeSystem: boolean, + opAction: OrgPermissionSet[0] | ProjectPermissionSet[0], + opSubject: OrgPermissionSet[1] | ProjectPermissionSet[1], + actorPermission: MongoAbility, + managedPermission: MongoAbility +) => { + if (shouldUseNewPrivilegeSystem) { + if (actorPermission.can(opAction, opSubject)) { + return { + isValid: true, + missingPermissions: [] + }; + } + + return { + isValid: false, + missingPermissions: [ + { + action: opAction, + subject: opSubject + } + ] + }; + } + + // if not, we check if the actor is indeed more privileged than the managed permission - this is the old system + return validatePermissionBoundary(actorPermission, managedPermission); +}; + +const constructPermissionErrorMessage = ( + baseMessage: string, + shouldUseNewPrivilegeSystem: boolean, + opAction: OrgPermissionSet[0] | ProjectPermissionSet[0], + opSubject: OrgPermissionSet[1] | ProjectPermissionSet[1] +) => { + return `${baseMessage}${ + shouldUseNewPrivilegeSystem + ? `. Actor is missing permission ${opAction as string} on ${opSubject as string}` + : ". Actor privilege level is not high enough to perform this action" + }`; +}; + +export { + constructPermissionErrorMessage, + escapeHandlebarsMissingDict, + isAuthMethodSaml, + validateOrgSSO, + validatePrivilegeChangeOperation +}; diff --git a/backend/src/ee/services/permission/permission-schemas.ts b/backend/src/ee/services/permission/permission-schemas.ts new file mode 100644 index 000000000..fb462e4aa --- /dev/null +++ b/backend/src/ee/services/permission/permission-schemas.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; + +export const CASL_ACTION_SCHEMA_NATIVE_ENUM = (actions: ACTION) => + z + .union([z.nativeEnum(actions), z.nativeEnum(actions).array().min(1)]) + .transform((el) => (typeof el === "string" ? [el] : el)); + +export const CASL_ACTION_SCHEMA_ENUM = (actions: ACTION) => + z.union([z.enum(actions), z.enum(actions).array().min(1)]).transform((el) => (typeof el === "string" ? [el] : el)); diff --git a/backend/src/ee/services/permission/permission-service-types.ts b/backend/src/ee/services/permission/permission-service-types.ts index 620e7a61c..570e2b6b1 100644 --- a/backend/src/ee/services/permission/permission-service-types.ts +++ b/backend/src/ee/services/permission/permission-service-types.ts @@ -1,3 +1,6 @@ +import { ActionProjectType } from "@app/db/schemas"; +import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; + export type TBuildProjectPermissionDTO = { permissions?: unknown; role: string; @@ -7,3 +10,34 @@ export type TBuildOrgPermissionDTO = { permissions?: unknown; role: string; }[]; + +export type TGetUserProjectPermissionArg = { + userId: string; + projectId: string; + authMethod: ActorAuthMethod; + actionProjectType: ActionProjectType; + userOrgId?: string; +}; + +export type TGetIdentityProjectPermissionArg = { + identityId: string; + projectId: string; + identityOrgId?: string; + actionProjectType: ActionProjectType; +}; + +export type TGetServiceTokenProjectPermissionArg = { + serviceTokenId: string; + projectId: string; + actorOrgId?: string; + actionProjectType: ActionProjectType; +}; + +export type TGetProjectPermissionArg = { + actor: ActorType; + actorId: string; + projectId: string; + actorAuthMethod: ActorAuthMethod; + actorOrgId?: string; + actionProjectType: ActionProjectType; +}; diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index e762d00ec..0082c3d17 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -1,9 +1,11 @@ import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability"; import { PackRule, unpackRules } from "@casl/ability/extra"; +import { requestContext } from "@fastify/request-context"; import { MongoQuery } from "@ucast/mongo2js"; import handlebars from "handlebars"; import { + ActionProjectType, OrgMembershipRole, ProjectMembershipRole, ServiceTokenScopes, @@ -21,8 +23,15 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission"; import { TPermissionDALFactory } from "./permission-dal"; -import { validateOrgSSO } from "./permission-fns"; -import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-service-types"; +import { escapeHandlebarsMissingDict, validateOrgSSO } from "./permission-fns"; +import { + TBuildOrgPermissionDTO, + TBuildProjectPermissionDTO, + TGetIdentityProjectPermissionArg, + TGetProjectPermissionArg, + TGetServiceTokenProjectPermissionArg, + TGetUserProjectPermissionArg +} from "./permission-service-types"; import { buildServiceTokenProjectPermission, projectAdminPermissions, @@ -130,7 +139,12 @@ export const permissionServiceFactory = ({ throw new ForbiddenRequestError({ name: "You are not logged into this organization" }); } - validateOrgSSO(authMethod, membership.orgAuthEnforced); + validateOrgSSO( + authMethod, + membership.orgAuthEnforced, + membership.bypassOrgAuthEnabled, + membership.role as OrgMembershipRole + ); const finalPolicyRoles = [{ role: membership.role, permissions: membership.permissions }].concat( membership?.groups?.map(({ role, customRolePermission }) => ({ @@ -192,12 +206,13 @@ export const permissionServiceFactory = ({ }; // user permission for a project in an organization - const getUserProjectPermission = async ( - userId: string, - projectId: string, - authMethod: ActorAuthMethod, - userOrgId?: string - ): Promise> => { + const getUserProjectPermission = async ({ + userId, + projectId, + authMethod, + userOrgId, + actionProjectType + }: TGetUserProjectPermissionArg): Promise> => { const userProjectPermission = await permissionDAL.getProjectPermission(userId, projectId); if (!userProjectPermission) throw new ForbiddenRequestError({ name: "User not a part of the specified project" }); @@ -216,7 +231,18 @@ export const permissionServiceFactory = ({ throw new ForbiddenRequestError({ name: "You are not logged into this organization" }); } - validateOrgSSO(authMethod, userProjectPermission.orgAuthEnforced); + validateOrgSSO( + authMethod, + userProjectPermission.orgAuthEnforced, + userProjectPermission.bypassOrgAuthEnabled, + userProjectPermission.orgRole + ); + + if (actionProjectType !== ActionProjectType.Any && actionProjectType !== userProjectPermission.projectType) { + throw new BadRequestError({ + message: `The project is of type ${userProjectPermission.projectType}. Operations of type ${actionProjectType} are not allowed.` + }); + } // join two permissions and pass to build the final permission set const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; @@ -227,12 +253,14 @@ export const permissionServiceFactory = ({ })) || []; const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges)); - const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false, strict: true }); - const metadataKeyValuePair = objectify( + const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false }); + const unescapedMetadata = objectify( userProjectPermission.metadata, (i) => i.key, (i) => i.value ); + const metadataKeyValuePair = escapeHandlebarsMissingDict(unescapedMetadata, "identity.metadata"); + requestContext.set("identityPermissionMetadata", { metadata: unescapedMetadata }); const interpolateRules = templatedRules( { identity: { @@ -260,11 +288,12 @@ export const permissionServiceFactory = ({ }; }; - const getIdentityProjectPermission = async ( - identityId: string, - projectId: string, - identityOrgId: string | undefined - ): Promise> => { + const getIdentityProjectPermission = async ({ + identityId, + projectId, + identityOrgId, + actionProjectType + }: TGetIdentityProjectPermissionArg): Promise> => { const identityProjectPermission = await permissionDAL.getProjectIdentityPermission(identityId, projectId); if (!identityProjectPermission) throw new ForbiddenRequestError({ @@ -283,6 +312,12 @@ export const permissionServiceFactory = ({ throw new ForbiddenRequestError({ name: "Identity is not a member of the specified organization" }); } + if (actionProjectType !== ActionProjectType.Any && actionProjectType !== identityProjectPermission.projectType) { + throw new BadRequestError({ + message: `The project is of type ${identityProjectPermission.projectType}. Operations of type ${actionProjectType} are not allowed.` + }); + } + const rolePermissions = identityProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; const additionalPrivileges = @@ -292,18 +327,27 @@ export const permissionServiceFactory = ({ })) || []; const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges)); - const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false, strict: true }); - const metadataKeyValuePair = objectify( + const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false }); + const unescapedIdentityAuthInfo = requestContext.get("identityAuthInfo"); + const unescapedMetadata = objectify( identityProjectPermission.metadata, (i) => i.key, (i) => i.value ); + const identityAuthInfo = + unescapedIdentityAuthInfo?.identityId === identityId && unescapedIdentityAuthInfo + ? escapeHandlebarsMissingDict(unescapedIdentityAuthInfo as never, "identity.auth") + : {}; + const metadataKeyValuePair = escapeHandlebarsMissingDict(unescapedMetadata, "identity.metadata"); + + requestContext.set("identityPermissionMetadata", { metadata: unescapedMetadata, auth: unescapedIdentityAuthInfo }); const interpolateRules = templatedRules( { identity: { id: identityProjectPermission.identityId, username: identityProjectPermission.username, - metadata: metadataKeyValuePair + metadata: metadataKeyValuePair, + auth: identityAuthInfo } }, { data: false } @@ -325,11 +369,12 @@ export const permissionServiceFactory = ({ }; }; - const getServiceTokenProjectPermission = async ( - serviceTokenId: string, - projectId: string, - actorOrgId: string | undefined - ) => { + const getServiceTokenProjectPermission = async ({ + serviceTokenId, + projectId, + actorOrgId, + actionProjectType + }: TGetServiceTokenProjectPermissionArg) => { const serviceToken = await serviceTokenDAL.findById(serviceTokenId); if (!serviceToken) throw new NotFoundError({ message: `Service token with ID '${serviceTokenId}' not found` }); @@ -353,17 +398,27 @@ export const permissionServiceFactory = ({ }); } + if (actionProjectType !== ActionProjectType.Any && actionProjectType !== serviceTokenProject.type) { + throw new BadRequestError({ + message: `The project is of type ${serviceTokenProject.type}. Operations of type ${actionProjectType} are not allowed.` + }); + } + const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []); return { permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions), - membership: undefined + membership: { + shouldUseNewPrivilegeSystem: true + } }; }; type TProjectPermissionRT = T extends ActorType.SERVICE ? { permission: MongoAbility; - membership: undefined; + membership: { + shouldUseNewPrivilegeSystem: boolean; + }; hasRole: (arg: string) => boolean; } // service token doesn't have both membership and roles : { @@ -372,24 +427,160 @@ export const permissionServiceFactory = ({ orgAuthEnforced: boolean | null | undefined; orgId: string; roles: Array<{ role: string }>; + shouldUseNewPrivilegeSystem: boolean; }; hasRole: (role: string) => boolean; }; - const getProjectPermission = async ( - type: T, - id: string, - projectId: string, - actorAuthMethod: ActorAuthMethod, - actorOrgId: string | undefined - ): Promise> => { - switch (type) { + const getProjectPermissions = async (projectId: string) => { + // fetch user permissions + const rawUserProjectPermissions = await permissionDAL.getProjectUserPermissions(projectId); + const userPermissions = rawUserProjectPermissions.map((userProjectPermission) => { + const rolePermissions = + userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; + const additionalPrivileges = + userProjectPermission.additionalPrivileges?.map(({ permissions }) => ({ + role: ProjectMembershipRole.Custom, + permissions + })) || []; + + const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges)); + const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false }); + const metadataKeyValuePair = escapeHandlebarsMissingDict( + objectify( + userProjectPermission.metadata, + (i) => i.key, + (i) => i.value + ), + "identity.metadata" + ); + const interpolateRules = templatedRules( + { + identity: { + id: userProjectPermission.userId, + username: userProjectPermission.username, + metadata: metadataKeyValuePair + } + }, + { data: false } + ); + const permission = createMongoAbility( + JSON.parse(interpolateRules) as RawRuleOf>[], + { + conditionsMatcher + } + ); + + return { + permission, + id: userProjectPermission.userId, + name: userProjectPermission.username, + membershipId: userProjectPermission.id + }; + }); + + // fetch identity permissions + const rawIdentityProjectPermissions = await permissionDAL.getProjectIdentityPermissions(projectId); + const identityPermissions = rawIdentityProjectPermissions.map((identityProjectPermission) => { + const rolePermissions = + identityProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; + const additionalPrivileges = + identityProjectPermission.additionalPrivileges?.map(({ permissions }) => ({ + role: ProjectMembershipRole.Custom, + permissions + })) || []; + + const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges)); + const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false }); + const metadataKeyValuePair = escapeHandlebarsMissingDict( + objectify( + identityProjectPermission.metadata, + (i) => i.key, + (i) => i.value + ), + "identity.metadata" + ); + const interpolateRules = templatedRules( + { + identity: { + id: identityProjectPermission.identityId, + username: identityProjectPermission.username, + metadata: metadataKeyValuePair + } + }, + { data: false } + ); + const permission = createMongoAbility( + JSON.parse(interpolateRules) as RawRuleOf>[], + { + conditionsMatcher + } + ); + + return { + permission, + id: identityProjectPermission.identityId, + name: identityProjectPermission.username, + membershipId: identityProjectPermission.id + }; + }); + + // fetch group permissions + const rawGroupProjectPermissions = await permissionDAL.getProjectGroupPermissions(projectId); + const groupPermissions = rawGroupProjectPermissions.map((groupProjectPermission) => { + const rolePermissions = + groupProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; + const rules = buildProjectPermissionRules(rolePermissions); + const permission = createMongoAbility(rules, { + conditionsMatcher + }); + + return { + permission, + id: groupProjectPermission.groupId, + name: groupProjectPermission.username, + membershipId: groupProjectPermission.id + }; + }); + + return { + userPermissions, + identityPermissions, + groupPermissions + }; + }; + + const getProjectPermission = async ({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType + }: TGetProjectPermissionArg): Promise> => { + switch (actor) { case ActorType.USER: - return getUserProjectPermission(id, projectId, actorAuthMethod, actorOrgId) as Promise>; + return getUserProjectPermission({ + userId: actorId, + projectId, + authMethod: actorAuthMethod, + userOrgId: actorOrgId, + actionProjectType + }) as Promise>; case ActorType.SERVICE: - return getServiceTokenProjectPermission(id, projectId, actorOrgId) as Promise>; + return getServiceTokenProjectPermission({ + serviceTokenId: actorId, + projectId, + actorOrgId, + actionProjectType + }) as Promise>; case ActorType.IDENTITY: - return getIdentityProjectPermission(id, projectId, actorOrgId) as Promise>; + return getIdentityProjectPermission({ + identityId: actorId, + projectId, + identityOrgId: actorOrgId, + actionProjectType + }) as Promise>; default: throw new BadRequestError({ message: "Invalid actor provided", @@ -426,6 +617,7 @@ export const permissionServiceFactory = ({ getOrgPermission, getUserProjectPermission, getProjectPermission, + getProjectPermissions, getOrgPermissionByRole, getProjectPermissionByRole, buildOrgPermission, diff --git a/backend/src/ee/services/permission/permission-types.ts b/backend/src/ee/services/permission/permission-types.ts index 8df85054d..1708404f6 100644 --- a/backend/src/ee/services/permission/permission-types.ts +++ b/backend/src/ee/services/permission/permission-types.ts @@ -1,33 +1,10 @@ import picomatch from "picomatch"; import { z } from "zod"; -export enum PermissionConditionOperators { - $IN = "$in", - $ALL = "$all", - $REGEX = "$regex", - $EQ = "$eq", - $NEQ = "$ne", - $GLOB = "$glob" -} +import { PermissionConditionOperators } from "@app/lib/casl"; export const PermissionConditionSchema = { [PermissionConditionOperators.$IN]: z.string().trim().min(1).array(), - [PermissionConditionOperators.$ALL]: z.string().trim().min(1).array(), - [PermissionConditionOperators.$REGEX]: z - .string() - .min(1) - .refine( - (el) => { - try { - // eslint-disable-next-line no-new - new RegExp(el); - return true; - } catch { - return false; - } - }, - { message: "Invalid regex pattern" } - ), [PermissionConditionOperators.$EQ]: z.string().min(1), [PermissionConditionOperators.$NEQ]: z.string().min(1), [PermissionConditionOperators.$GLOB]: z diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 591cdd343..b5cfadbeb 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -1,10 +1,14 @@ import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability"; import { z } from "zod"; -import { conditionsMatcher } from "@app/lib/casl"; -import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; +import { + CASL_ACTION_SCHEMA_ENUM, + CASL_ACTION_SCHEMA_NATIVE_ENUM +} from "@app/ee/services/permission/permission-schemas"; +import { conditionsMatcher, PermissionConditionOperators } from "@app/lib/casl"; +import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; -import { PermissionConditionOperators, PermissionConditionSchema } from "./permission-types"; +import { PermissionConditionSchema } from "./permission-types"; export enum ProjectPermissionActions { Read = "read", @@ -13,13 +17,24 @@ export enum ProjectPermissionActions { Delete = "delete" } +export enum ProjectPermissionSecretActions { + DescribeAndReadValue = "read", + DescribeSecret = "describeSecret", + ReadValue = "readValue", + Create = "create", + Edit = "edit", + Delete = "delete" +} + export enum ProjectPermissionCmekActions { Read = "read", Create = "create", Edit = "edit", Delete = "delete", Encrypt = "encrypt", - Decrypt = "decrypt" + Decrypt = "decrypt", + Sign = "sign", + Verify = "verify" } export enum ProjectPermissionDynamicSecretActions { @@ -30,6 +45,65 @@ export enum ProjectPermissionDynamicSecretActions { Lease = "lease" } +export enum ProjectPermissionIdentityActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + GrantPrivileges = "grant-privileges" +} + +export enum ProjectPermissionMemberActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + GrantPrivileges = "grant-privileges" +} + +export enum ProjectPermissionGroupActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + GrantPrivileges = "grant-privileges" +} + +export enum ProjectPermissionSshHostActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + IssueHostCert = "issue-host-cert" +} + +export enum ProjectPermissionSecretSyncActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + SyncSecrets = "sync-secrets", + ImportSecrets = "import-secrets", + RemoveSecrets = "remove-secrets" +} + +export enum ProjectPermissionSecretRotationActions { + Read = "read", + ReadGeneratedCredentials = "read-generated-credentials", + Create = "create", + Edit = "edit", + Delete = "delete", + RotateSecrets = "rotate-secrets" +} + +export enum ProjectPermissionKmipActions { + CreateClients = "create-clients", + UpdateClients = "update-clients", + DeleteClients = "delete-clients", + ReadClients = "read-clients", + GenerateClientCertificates = "generate-client-certificates" +} + export enum ProjectPermissionSub { Role = "role", Member = "member", @@ -54,10 +128,16 @@ export enum ProjectPermissionSub { CertificateAuthorities = "certificate-authorities", Certificates = "certificates", CertificateTemplates = "certificate-templates", + SshCertificateAuthorities = "ssh-certificate-authorities", + SshCertificates = "ssh-certificates", + SshCertificateTemplates = "ssh-certificate-templates", + SshHosts = "ssh-hosts", PkiAlerts = "pki-alerts", PkiCollections = "pki-collections", Kms = "kms", - Cmek = "cmek" + Cmek = "cmek", + SecretSyncs = "secret-syncs", + Kmip = "kmip" } export type SecretSubjectFields = { @@ -75,6 +155,10 @@ export type SecretFolderSubjectFields = { export type DynamicSecretSubjectFields = { environment: string; secretPath: string; + metadata?: { + key: string; + value: string; + }[]; }; export type SecretImportSubjectFields = { @@ -82,9 +166,22 @@ export type SecretImportSubjectFields = { secretPath: string; }; +export type SecretRotationsSubjectFields = { + environment: string; + secretPath: string; +}; + +export type IdentityManagementSubjectFields = { + identityId: string; +}; + +export type SshHostSubjectFields = { + hostname: string; +}; + export type ProjectPermissionSet = | [ - ProjectPermissionActions, + ProjectPermissionSecretActions, ProjectPermissionSub.Secrets | (ForcedSubject & SecretSubjectFields) ] | [ @@ -110,8 +207,8 @@ export type ProjectPermissionSet = ] | [ProjectPermissionActions, ProjectPermissionSub.Role] | [ProjectPermissionActions, ProjectPermissionSub.Tags] - | [ProjectPermissionActions, ProjectPermissionSub.Member] - | [ProjectPermissionActions, ProjectPermissionSub.Groups] + | [ProjectPermissionMemberActions, ProjectPermissionSub.Member] + | [ProjectPermissionGroupActions, ProjectPermissionSub.Groups] | [ProjectPermissionActions, ProjectPermissionSub.Integrations] | [ProjectPermissionActions, ProjectPermissionSub.Webhooks] | [ProjectPermissionActions, ProjectPermissionSub.AuditLogs] @@ -120,13 +217,31 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.Settings] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] - | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] - | [ProjectPermissionActions, ProjectPermissionSub.Identity] + | [ + ProjectPermissionSecretRotationActions, + ( + | ProjectPermissionSub.SecretRotation + | (ForcedSubject & SecretRotationsSubjectFields) + ) + ] + | [ + ProjectPermissionIdentityActions, + ProjectPermissionSub.Identity | (ForcedSubject & IdentityManagementSubjectFields) + ] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.Certificates] | [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] + | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities] + | [ProjectPermissionActions, ProjectPermissionSub.SshCertificates] + | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates] + | [ + ProjectPermissionSshHostActions, + ProjectPermissionSub.SshHosts | (ForcedSubject & SshHostSubjectFields) + ] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] + | [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs] + | [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip] | [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Project] @@ -134,14 +249,27 @@ export type ProjectPermissionSet = | [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]; -const CASL_ACTION_SCHEMA_NATIVE_ENUM = (actions: ACTION) => +const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'"; +const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([ + z.string().refine((val) => val.startsWith("/"), SECRET_PATH_MISSING_SLASH_ERR_MSG), z - .union([z.nativeEnum(actions), z.nativeEnum(actions).array().min(1)]) - .transform((el) => (typeof el === "string" ? [el] : el)); - -const CASL_ACTION_SCHEMA_ENUM = (actions: ACTION) => - z.union([z.enum(actions), z.enum(actions).array().min(1)]).transform((el) => (typeof el === "string" ? [el] : el)); - + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ].refine( + (val) => val.startsWith("/"), + SECRET_PATH_MISSING_SLASH_ERR_MSG + ), + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ].refine( + (val) => val.startsWith("/"), + SECRET_PATH_MISSING_SLASH_ERR_MSG + ), + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN].refine( + (val) => val.every((el) => el.startsWith("/")), + SECRET_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 @@ -156,17 +284,43 @@ const SecretConditionV1Schema = z }) .partial() ]), - secretPath: z.union([ + secretPath: SECRET_PATH_PERMISSION_OPERATOR_SCHEMA + }) + .partial(); + +const DynamicSecretConditionV2Schema = z + .object({ + environment: 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] + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN] }) .partial() - ]) + ]), + secretPath: SECRET_PATH_PERMISSION_OPERATOR_SCHEMA, + metadata: z.object({ + [PermissionConditionOperators.$ELEMENTMATCH]: z + .object({ + key: z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN] + }) + .partial(), + value: z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN] + }) + .partial() + }) + .partial() + }) }) .partial(); @@ -183,17 +337,7 @@ const SecretConditionV2Schema = z }) .partial() ]), - secretPath: 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() - ]), + secretPath: SECRET_PATH_PERMISSION_OPERATOR_SCHEMA, secretName: z.union([ z.string(), z @@ -213,6 +357,36 @@ const SecretConditionV2Schema = z }) .partial(); +const IdentityManagementConditionSchema = z + .object({ + identityId: z.union([ + z.string(), + z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN] + }) + .partial() + ]) + }) + .partial(); + +const SshHostConditionSchema = z + .object({ + hostname: z.union([ + z.string(), + z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN] + }) + .partial() + ]) + }) + .partial(); + const GeneralPermissionSchema = [ z.object({ subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."), @@ -220,12 +394,6 @@ const GeneralPermissionSchema = [ "Describe what action an entity can take." ) }), - z.object({ - subject: z.literal(ProjectPermissionSub.SecretRotation).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.SecretRollback).describe("The entity this permission pertains to."), action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read, ProjectPermissionActions.Create]).describe( @@ -234,13 +402,13 @@ const GeneralPermissionSchema = [ }), z.object({ subject: z.literal(ProjectPermissionSub.Member).describe("The entity this permission pertains to."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionMemberActions).describe( "Describe what action an entity can take." ) }), z.object({ subject: z.literal(ProjectPermissionSub.Groups).describe("The entity this permission pertains to."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionGroupActions).describe( "Describe what action an entity can take." ) }), @@ -262,12 +430,6 @@ const GeneralPermissionSchema = [ "Describe what action an entity can take." ) }), - z.object({ - subject: z.literal(ProjectPermissionSub.Identity).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.ServiceTokens).describe("The entity this permission pertains to."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( @@ -322,6 +484,28 @@ const GeneralPermissionSchema = [ "Describe what action an entity can take." ) }), + z.object({ + subject: z + .literal(ProjectPermissionSub.SshCertificateAuthorities) + .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.SshCertificates).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.SshCertificateTemplates) + .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.PkiAlerts).describe("The entity this permission pertains to."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( @@ -348,13 +532,25 @@ const GeneralPermissionSchema = [ }), z.object({ subject: z.literal(ProjectPermissionSub.Cmek).describe("The entity this permission pertains to."), - inverted: z.boolean().optional().describe("Whether rule allows or forbids."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCmekActions).describe( "Describe what action an entity can take." ) + }), + z.object({ + subject: z.literal(ProjectPermissionSub.SecretSyncs).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretSyncActions).describe( + "Describe what action an entity can take." + ) + }), + z.object({ + subject: z.literal(ProjectPermissionSub.Kmip).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionKmipActions).describe( + "Describe what action an entity can take." + ) }) ]; +// Do not update this schema anymore, as it's kept purely for backwards compatability. Update V2 schema only. export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [ z.object({ subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."), @@ -373,6 +569,18 @@ export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [ "Describe what action an entity can take." ) }), + z.object({ + subject: z.literal(ProjectPermissionSub.Identity).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.SecretRotation).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( + "Describe what action an entity can take." + ) + }), ...GeneralPermissionSchema ]); @@ -380,7 +588,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [ z.object({ subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."), inverted: z.boolean().optional().describe("Whether rule allows or forbids."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretActions).describe( "Describe what action an entity can take." ), conditions: SecretConditionV2Schema.describe( @@ -413,6 +621,36 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [ action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionDynamicSecretActions).describe( "Describe what action an entity can take." ), + conditions: DynamicSecretConditionV2Schema.describe( + "When specified, only matching conditions will be allowed to access given resource." + ).optional() + }), + z.object({ + subject: z.literal(ProjectPermissionSub.Identity).describe("The entity this permission pertains to."), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionIdentityActions).describe( + "Describe what action an entity can take." + ), + conditions: IdentityManagementConditionSchema.describe( + "When specified, only matching conditions will be allowed to access given resource." + ).optional() + }), + z.object({ + subject: z.literal(ProjectPermissionSub.SshHosts).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSshHostActions).describe( + "Describe what action an entity can take." + ), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), + conditions: SshHostConditionSchema.describe( + "When specified, only matching conditions will be allowed to access given resource." + ).optional() + }), + z.object({ + subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretRotationActions).describe( + "Describe what action an entity can take." + ), conditions: SecretConditionV1Schema.describe( "When specified, only matching conditions will be allowed to access given resource." ).optional() @@ -427,17 +665,12 @@ const buildAdminPermissionRules = () => { // Admins get full access to everything [ - ProjectPermissionSub.Secrets, ProjectPermissionSub.SecretFolders, ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretApproval, - ProjectPermissionSub.SecretRotation, - ProjectPermissionSub.Member, - ProjectPermissionSub.Groups, ProjectPermissionSub.Role, ProjectPermissionSub.Integrations, ProjectPermissionSub.Webhooks, - ProjectPermissionSub.Identity, ProjectPermissionSub.ServiceTokens, ProjectPermissionSub.Settings, ProjectPermissionSub.Environments, @@ -448,7 +681,10 @@ const buildAdminPermissionRules = () => { ProjectPermissionSub.Certificates, ProjectPermissionSub.CertificateTemplates, ProjectPermissionSub.PkiAlerts, - ProjectPermissionSub.PkiCollections + ProjectPermissionSub.PkiCollections, + ProjectPermissionSub.SshCertificateAuthorities, + ProjectPermissionSub.SshCertificates, + ProjectPermissionSub.SshCertificateTemplates ].forEach((el) => { can( [ @@ -457,10 +693,66 @@ const buildAdminPermissionRules = () => { ProjectPermissionActions.Create, ProjectPermissionActions.Delete ], - el as ProjectPermissionSub + el ); }); + can( + [ + ProjectPermissionSshHostActions.Edit, + ProjectPermissionSshHostActions.Read, + ProjectPermissionSshHostActions.Create, + ProjectPermissionSshHostActions.Delete, + ProjectPermissionSshHostActions.IssueHostCert + ], + ProjectPermissionSub.SshHosts + ); + + can( + [ + ProjectPermissionMemberActions.Create, + ProjectPermissionMemberActions.Edit, + ProjectPermissionMemberActions.Delete, + ProjectPermissionMemberActions.Read, + ProjectPermissionMemberActions.GrantPrivileges + ], + ProjectPermissionSub.Member + ); + + can( + [ + ProjectPermissionGroupActions.Create, + ProjectPermissionGroupActions.Edit, + ProjectPermissionGroupActions.Delete, + ProjectPermissionGroupActions.Read, + ProjectPermissionGroupActions.GrantPrivileges + ], + ProjectPermissionSub.Groups + ); + + can( + [ + ProjectPermissionIdentityActions.Create, + ProjectPermissionIdentityActions.Edit, + ProjectPermissionIdentityActions.Delete, + ProjectPermissionIdentityActions.Read, + ProjectPermissionIdentityActions.GrantPrivileges + ], + ProjectPermissionSub.Identity + ); + + can( + [ + ProjectPermissionSecretActions.DescribeAndReadValue, + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.Create, + ProjectPermissionSecretActions.Edit, + ProjectPermissionSecretActions.Delete + ], + ProjectPermissionSub.Secrets + ); + can( [ ProjectPermissionDynamicSecretActions.ReadRootCredential, @@ -482,10 +774,48 @@ const buildAdminPermissionRules = () => { ProjectPermissionCmekActions.Delete, ProjectPermissionCmekActions.Read, ProjectPermissionCmekActions.Encrypt, - ProjectPermissionCmekActions.Decrypt + ProjectPermissionCmekActions.Decrypt, + ProjectPermissionCmekActions.Sign, + ProjectPermissionCmekActions.Verify ], ProjectPermissionSub.Cmek ); + can( + [ + ProjectPermissionSecretSyncActions.Create, + ProjectPermissionSecretSyncActions.Edit, + ProjectPermissionSecretSyncActions.Delete, + ProjectPermissionSecretSyncActions.Read, + ProjectPermissionSecretSyncActions.SyncSecrets, + ProjectPermissionSecretSyncActions.ImportSecrets, + ProjectPermissionSecretSyncActions.RemoveSecrets + ], + ProjectPermissionSub.SecretSyncs + ); + + can( + [ + ProjectPermissionKmipActions.CreateClients, + ProjectPermissionKmipActions.UpdateClients, + ProjectPermissionKmipActions.DeleteClients, + ProjectPermissionKmipActions.ReadClients, + ProjectPermissionKmipActions.GenerateClientCertificates + ], + ProjectPermissionSub.Kmip + ); + + can( + [ + ProjectPermissionSecretRotationActions.Create, + ProjectPermissionSecretRotationActions.Edit, + ProjectPermissionSecretRotationActions.Delete, + ProjectPermissionSecretRotationActions.Read, + ProjectPermissionSecretRotationActions.ReadGeneratedCredentials, + ProjectPermissionSecretRotationActions.RotateSecrets + ], + ProjectPermissionSub.SecretRotation + ); + return rules; }; @@ -496,10 +826,12 @@ const buildMemberPermissionRules = () => { can( [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete + ProjectPermissionSecretActions.DescribeAndReadValue, + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.Edit, + ProjectPermissionSecretActions.Create, + ProjectPermissionSecretActions.Delete ], ProjectPermissionSub.Secrets ); @@ -533,13 +865,13 @@ const buildMemberPermissionRules = () => { ); can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval); - can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretRotation); + can([ProjectPermissionSecretRotationActions.Read], ProjectPermissionSub.SecretRotation); can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback); - can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.Member); + can([ProjectPermissionMemberActions.Read, ProjectPermissionMemberActions.Create], ProjectPermissionSub.Member); - can([ProjectPermissionActions.Read], ProjectPermissionSub.Groups); + can([ProjectPermissionGroupActions.Read], ProjectPermissionSub.Groups); can( [ @@ -563,10 +895,10 @@ const buildMemberPermissionRules = () => { can( [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete + ProjectPermissionIdentityActions.Read, + ProjectPermissionIdentityActions.Edit, + ProjectPermissionIdentityActions.Create, + ProjectPermissionIdentityActions.Delete ], ProjectPermissionSub.Identity ); @@ -633,6 +965,13 @@ const buildMemberPermissionRules = () => { can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts); can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections); + can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateAuthorities); + can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates); + can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates); + can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates); + + can([ProjectPermissionSshHostActions.Read], ProjectPermissionSub.SshHosts); + can( [ ProjectPermissionCmekActions.Create, @@ -640,11 +979,26 @@ const buildMemberPermissionRules = () => { ProjectPermissionCmekActions.Delete, ProjectPermissionCmekActions.Read, ProjectPermissionCmekActions.Encrypt, - ProjectPermissionCmekActions.Decrypt + ProjectPermissionCmekActions.Decrypt, + ProjectPermissionCmekActions.Sign, + ProjectPermissionCmekActions.Verify ], ProjectPermissionSub.Cmek ); + can( + [ + ProjectPermissionSecretSyncActions.Create, + ProjectPermissionSecretSyncActions.Edit, + ProjectPermissionSecretSyncActions.Delete, + ProjectPermissionSecretSyncActions.Read, + ProjectPermissionSecretSyncActions.SyncSecrets, + ProjectPermissionSecretSyncActions.ImportSecrets, + ProjectPermissionSecretSyncActions.RemoveSecrets + ], + ProjectPermissionSub.SecretSyncs + ); + return rules; }; @@ -653,19 +1007,21 @@ export const projectMemberPermissions = buildMemberPermissionRules(); const buildViewerPermissionRules = () => { const { can, rules } = new AbilityBuilder>(createMongoAbility); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + can(ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets); + can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets); + can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); - can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Member); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups); + can(ProjectPermissionSecretRotationActions.Read, ProjectPermissionSub.SecretRotation); + can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); + can(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups); can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); + can(ProjectPermissionIdentityActions.Read, ProjectPermissionSub.Identity); can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); @@ -675,6 +1031,10 @@ const buildViewerPermissionRules = () => { can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities); can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateAuthorities); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates); + can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs); return rules; }; @@ -697,26 +1057,25 @@ export const buildServiceTokenProjectPermission = ( [ProjectPermissionSub.Secrets, ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretFolders].forEach( (subject) => { if (canWrite) { - // TODO: @Akhi - // @ts-expect-error type can(ProjectPermissionActions.Edit, subject, { + // @ts-expect-error type secretPath: { $glob: secretPath }, environment }); - // @ts-expect-error type can(ProjectPermissionActions.Create, subject, { + // @ts-expect-error type secretPath: { $glob: secretPath }, environment }); - // @ts-expect-error type can(ProjectPermissionActions.Delete, subject, { + // @ts-expect-error type secretPath: { $glob: secretPath }, environment }); } if (canRead) { - // @ts-expect-error type can(ProjectPermissionActions.Read, subject, { + // @ts-expect-error type secretPath: { $glob: secretPath }, environment }); @@ -777,7 +1136,17 @@ export const backfillPermissionV1SchemaToV2Schema = ( subject: ProjectPermissionSub.SecretImports as const })); + const secretPolicies = secretSubjects.map(({ subject, ...el }) => ({ + subject: ProjectPermissionSub.Secrets as const, + ...el, + action: + el.action.includes(ProjectPermissionActions.Read) && !el.action.includes(ProjectPermissionSecretActions.ReadValue) + ? el.action.concat(ProjectPermissionSecretActions.ReadValue) + : el.action + })); + const secretFolderPolicies = secretSubjects + .map(({ subject, ...el }) => ({ ...el, // read permission is not needed anymore @@ -819,6 +1188,7 @@ export const backfillPermissionV1SchemaToV2Schema = ( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore-error this is valid ts secretImportPolicies, + secretPolicies, dynamicSecretPolicies, hasReadOnlyFolder.length ? [] : secretFolderPolicies ); diff --git a/backend/src/ee/services/project-template/project-template-service.ts b/backend/src/ee/services/project-template/project-template-service.ts index 5afa58caf..b2430ac14 100644 --- a/backend/src/ee/services/project-template/project-template-service.ts +++ b/backend/src/ee/services/project-template/project-template-service.ts @@ -15,7 +15,7 @@ import { } from "@app/ee/services/project-template/project-template-types"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; -import { unpackPermissions } from "@app/server/routes/santizedSchemas/permission"; +import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission"; import { getPredefinedRoles } from "@app/services/project-role/project-role-fns"; import { TProjectTemplateDALFactory } from "./project-template-dal"; diff --git a/backend/src/ee/services/project-template/project-template-types.ts b/backend/src/ee/services/project-template/project-template-types.ts index 6b600f386..c2764dc53 100644 --- a/backend/src/ee/services/project-template/project-template-types.ts +++ b/backend/src/ee/services/project-template/project-template-types.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { TProjectEnvironments } from "@app/db/schemas"; import { TProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; -import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; +import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; export type TProjectTemplateEnvironment = Pick; diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts index 86d8e652a..965e25344 100644 --- a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts +++ b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts @@ -1,16 +1,21 @@ import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability"; import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; -import ms from "ms"; -import { TableName } from "@app/db/schemas"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; -import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; -import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; +import { ActionProjectType, TableName } from "@app/db/schemas"; +import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; +import { ms } from "@app/lib/ms"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; +import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { ActorType } from "@app/services/auth/auth-type"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; +import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSet, ProjectPermissionSub } from "../permission/project-permission"; +import { + ProjectPermissionMemberActions, + ProjectPermissionSet, + ProjectPermissionSub +} from "../permission/project-permission"; import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal"; import { ProjectUserAdditionalPrivilegeTemporaryMode, @@ -55,28 +60,44 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ if (!projectMembership) throw new NotFoundError({ message: `Project membership with ID ${projectMembershipId} found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - projectMembership.projectId, + projectId: projectMembership.projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); - const { permission: targetUserPermission } = await permissionService.getProjectPermission( - ActorType.USER, - projectMembership.userId, - projectMembership.projectId, + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); + const { permission: targetUserPermission, membership } = await permissionService.getProjectPermission({ + actor: ActorType.USER, + actorId: projectMembership.userId, + projectId: projectMembership.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); // we need to validate that the privilege given is not higher than the assigning users permission // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules targetUserPermission.update(targetUserPermission.rules.concat(customPermission)); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetUserPermission); - if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionMemberActions.GrantPrivileges, + ProjectPermissionSub.Member, + permission, + targetUserPermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update more privileged user", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionMemberActions.GrantPrivileges, + ProjectPermissionSub.Member + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({ slug, @@ -86,6 +107,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ if (existingSlug) throw new BadRequestError({ message: `Additional privilege with provided slug ${slug} already exists` }); + validateHandlebarTemplate("User Additional Privilege Create", JSON.stringify(customPermission || []), { + allowedExpressions: (val) => val.includes("identity.") + }); + const packedPermission = JSON.stringify(packRules(customPermission)); if (!dto.isTemporary) { const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({ @@ -140,28 +165,44 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ message: `Project membership for user with ID '${userPrivilege.userId}' not found in project with ID '${userPrivilege.projectId}'` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, membership } = await permissionService.getProjectPermission({ actor, actorId, - projectMembership.projectId, + projectId: projectMembership.projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); - const { permission: targetUserPermission } = await permissionService.getProjectPermission( - ActorType.USER, - projectMembership.userId, - projectMembership.projectId, + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); + const { permission: targetUserPermission } = await permissionService.getProjectPermission({ + actor: ActorType.USER, + actorId: projectMembership.userId, + projectId: projectMembership.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); // we need to validate that the privilege given is not higher than the assigning users permission // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules targetUserPermission.update(targetUserPermission.rules.concat(dto.permissions || [])); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetUserPermission); - if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionMemberActions.GrantPrivileges, + ProjectPermissionSub.Member, + permission, + targetUserPermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update more privileged user", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionMemberActions.GrantPrivileges, + ProjectPermissionSub.Member + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); if (dto?.slug) { const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({ @@ -173,6 +214,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ throw new BadRequestError({ message: `Additional privilege with provided slug ${dto.slug} already exists` }); } + validateHandlebarTemplate("User Additional Privilege Update", JSON.stringify(dto.permissions || []), { + allowedExpressions: (val) => val.includes("identity.") + }); + const isTemporary = typeof dto?.isTemporary !== "undefined" ? dto.isTemporary : userPrivilege.isTemporary; const packedPermission = dto.permissions && JSON.stringify(packRules(dto.permissions)); @@ -224,14 +269,15 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ message: `Project membership for user with ID '${userPrivilege.userId}' not found in project with ID '${userPrivilege.projectId}'` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - projectMembership.projectId, + projectId: projectMembership.projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id); return { @@ -260,14 +306,15 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ message: `Project membership for user with ID '${userPrivilege.userId}' not found in project with ID '${userPrivilege.projectId}'` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - projectMembership.projectId, + projectId: projectMembership.projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); return { ...userPrivilege, @@ -286,14 +333,15 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ if (!projectMembership) throw new NotFoundError({ message: `Project membership with ID ${projectMembershipId} not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - projectMembership.projectId, + projectId: projectMembership.projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); const userPrivileges = await projectUserAdditionalPrivilegeDAL.find( { diff --git a/backend/src/ee/services/rate-limit/rate-limit-service.ts b/backend/src/ee/services/rate-limit/rate-limit-service.ts index 208fa8428..61b18be91 100644 --- a/backend/src/ee/services/rate-limit/rate-limit-service.ts +++ b/backend/src/ee/services/rate-limit/rate-limit-service.ts @@ -46,7 +46,7 @@ export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateL } return rateLimit; } catch (err) { - logger.error("Error fetching rate limits %o", err); + logger.error(err, "Error fetching rate limits"); return undefined; } }; @@ -69,12 +69,12 @@ export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateL mfaRateLimit: rateLimit.mfaRateLimit }; - logger.info(`syncRateLimitConfiguration: rate limit configuration: %o`, newRateLimitMaxConfiguration); + logger.info(newRateLimitMaxConfiguration, "syncRateLimitConfiguration: rate limit configuration"); Object.freeze(newRateLimitMaxConfiguration); rateLimitMaxConfiguration = newRateLimitMaxConfiguration; } } catch (error) { - logger.error(`Error syncing rate limit configurations: %o`, error); + logger.error(error, "Error syncing rate limit configurations"); } }; diff --git a/backend/src/ee/services/saml-config/saml-config-dal.ts b/backend/src/ee/services/saml-config/saml-config-dal.ts index aff42230f..c82adcb89 100644 --- a/backend/src/ee/services/saml-config/saml-config-dal.ts +++ b/backend/src/ee/services/saml-config/saml-config-dal.ts @@ -1,6 +1,5 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; -import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; export type TSamlConfigDALFactory = ReturnType; @@ -8,25 +7,5 @@ export type TSamlConfigDALFactory = ReturnType; export const samlConfigDALFactory = (db: TDbClient) => { const samlCfgOrm = ormify(db, TableName.SamlConfig); - const findEnforceableSamlCfg = async (orgId: string) => { - try { - const samlCfg = await db - .replicaNode()(TableName.SamlConfig) - .where({ - orgId, - isActive: true - }) - .whereNotNull("lastUsed") - .first(); - - return samlCfg; - } catch (error) { - throw new DatabaseError({ error, name: "Find org by id" }); - } - }; - - return { - ...samlCfgOrm, - findEnforceableSamlCfg - }; + return samlCfgOrm; }; 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 2930d06f9..601347862 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -1,29 +1,15 @@ import { ForbiddenError } from "@casl/ability"; import jwt from "jsonwebtoken"; -import { - OrgMembershipStatus, - SecretKeyEncoding, - TableName, - TSamlConfigs, - TSamlConfigsUpdate, - TUsers -} from "@app/db/schemas"; +import { OrgMembershipStatus, TableName, TSamlConfigs, TSamlConfigsUpdate, TUsers } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; -import { - decryptSymmetric, - encryptSymmetric, - generateAsymmetricKeyPair, - generateSymmetricKey, - infisicalSymmetricDecrypt, - infisicalSymmetricEncypt -} from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; 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 { TIdentityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal"; -import { TOrgBotDALFactory } from "@app/services/org/org-bot-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"; @@ -52,21 +38,19 @@ type TSamlConfigServiceFactoryDep = { TOrgDALFactory, "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" >; - identityMetadataDAL: Pick; orgMembershipDAL: Pick; - orgBotDAL: Pick; permissionService: Pick; licenseService: Pick; tokenService: Pick; smtpService: Pick; + kmsService: Pick; }; export type TSamlConfigServiceFactory = ReturnType; export const samlConfigServiceFactory = ({ samlConfigDAL, - orgBotDAL, orgDAL, orgMembershipDAL, userDAL, @@ -75,10 +59,11 @@ export const samlConfigServiceFactory = ({ licenseService, tokenService, smtpService, - identityMetadataDAL + identityMetadataDAL, + kmsService }: TSamlConfigServiceFactoryDep) => { const createSamlCfg = async ({ - cert, + idpCert, actor, actorAuthMethod, actorOrgId, @@ -99,70 +84,18 @@ export const samlConfigServiceFactory = ({ "Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to create SSO configuration." }); - const orgBot = await orgBotDAL.transaction(async (tx) => { - const doc = await orgBotDAL.findOne({ orgId }, tx); - if (doc) return doc; - - const { privateKey, publicKey } = generateAsymmetricKeyPair(); - const key = generateSymmetricKey(); - const { - ciphertext: encryptedPrivateKey, - iv: privateKeyIV, - tag: privateKeyTag, - encoding: privateKeyKeyEncoding, - algorithm: privateKeyAlgorithm - } = infisicalSymmetricEncypt(privateKey); - const { - ciphertext: encryptedSymmetricKey, - iv: symmetricKeyIV, - tag: symmetricKeyTag, - encoding: symmetricKeyKeyEncoding, - algorithm: symmetricKeyAlgorithm - } = infisicalSymmetricEncypt(key); - - return orgBotDAL.create( - { - name: "Infisical org bot", - publicKey, - privateKeyIV, - encryptedPrivateKey, - symmetricKeyIV, - symmetricKeyTag, - encryptedSymmetricKey, - symmetricKeyAlgorithm, - orgId, - privateKeyTag, - privateKeyAlgorithm, - privateKeyKeyEncoding, - symmetricKeyKeyEncoding - }, - tx - ); + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId }); - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding - }); - - const { ciphertext: encryptedEntryPoint, iv: entryPointIV, tag: entryPointTag } = encryptSymmetric(entryPoint, key); - const { ciphertext: encryptedIssuer, iv: issuerIV, tag: issuerTag } = encryptSymmetric(issuer, key); - const { ciphertext: encryptedCert, iv: certIV, tag: certTag } = encryptSymmetric(cert, key); const samlConfig = await samlConfigDAL.create({ orgId, authProvider, isActive, - encryptedEntryPoint, - entryPointIV, - entryPointTag, - encryptedIssuer, - issuerIV, - issuerTag, - encryptedCert, - certIV, - certTag + encryptedSamlCertificate: encryptor({ plainText: Buffer.from(idpCert) }).cipherTextBlob, + encryptedSamlEntryPoint: encryptor({ plainText: Buffer.from(entryPoint) }).cipherTextBlob, + encryptedSamlIssuer: encryptor({ plainText: Buffer.from(issuer) }).cipherTextBlob }); return samlConfig; @@ -173,7 +106,7 @@ export const samlConfigServiceFactory = ({ actor, actorOrgId, actorAuthMethod, - cert, + idpCert, actorId, issuer, isActive, @@ -190,40 +123,21 @@ export const samlConfigServiceFactory = ({ }); const updateQuery: TSamlConfigsUpdate = { authProvider, isActive, lastUsed: null }; - const orgBot = await orgBotDAL.findOne({ orgId }); - if (!orgBot) - throw new NotFoundError({ - message: `Organization bot not found for organization with ID '${orgId}'`, - name: "OrgBotNotFound" - }); - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId }); if (entryPoint !== undefined) { - const { - ciphertext: encryptedEntryPoint, - iv: entryPointIV, - tag: entryPointTag - } = encryptSymmetric(entryPoint, key); - updateQuery.encryptedEntryPoint = encryptedEntryPoint; - updateQuery.entryPointIV = entryPointIV; - updateQuery.entryPointTag = entryPointTag; + updateQuery.encryptedSamlEntryPoint = encryptor({ plainText: Buffer.from(entryPoint) }).cipherTextBlob; } + if (issuer !== undefined) { - const { ciphertext: encryptedIssuer, iv: issuerIV, tag: issuerTag } = encryptSymmetric(issuer, key); - updateQuery.encryptedIssuer = encryptedIssuer; - updateQuery.issuerIV = issuerIV; - updateQuery.issuerTag = issuerTag; + updateQuery.encryptedSamlIssuer = encryptor({ plainText: Buffer.from(issuer) }).cipherTextBlob; } - if (cert !== undefined) { - const { ciphertext: encryptedCert, iv: certIV, tag: certTag } = encryptSymmetric(cert, key); - updateQuery.encryptedCert = encryptedCert; - updateQuery.certIV = certIV; - updateQuery.certTag = certTag; + + if (idpCert !== undefined) { + updateQuery.encryptedSamlCertificate = encryptor({ plainText: Buffer.from(idpCert) }).cipherTextBlob; } const [ssoConfig] = await samlConfigDAL.update({ orgId }, updateQuery); @@ -233,14 +147,14 @@ export const samlConfigServiceFactory = ({ }; const getSaml = async (dto: TGetSamlCfgDTO) => { - let ssoConfig: TSamlConfigs | undefined; + let samlConfig: TSamlConfigs | undefined; if (dto.type === "org") { - ssoConfig = await samlConfigDAL.findOne({ orgId: dto.orgId }); - if (!ssoConfig) return; + samlConfig = await samlConfigDAL.findOne({ orgId: dto.orgId }); + if (!samlConfig) return; } else if (dto.type === "orgSlug") { const org = await orgDAL.findOne({ slug: dto.orgSlug }); if (!org) return; - ssoConfig = await samlConfigDAL.findOne({ orgId: org.id }); + samlConfig = await samlConfigDAL.findOne({ orgId: org.id }); } else if (dto.type === "ssoId") { // TODO: // We made this change because saml config ids were not moved over during the migration @@ -259,81 +173,51 @@ export const samlConfigServiceFactory = ({ const id = UUIDToMongoId[dto.id] ?? dto.id; - ssoConfig = await samlConfigDAL.findById(id); + samlConfig = await samlConfigDAL.findById(id); } - if (!ssoConfig) throw new NotFoundError({ message: `Failed to find SSO data` }); + if (!samlConfig) throw new NotFoundError({ message: `Failed to find SSO data` }); // when dto is type id means it's internally used if (dto.type === "org") { const { permission } = await permissionService.getOrgPermission( dto.actor, dto.actorId, - ssoConfig.orgId, + samlConfig.orgId, dto.actorAuthMethod, dto.actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); } - const { - entryPointTag, - entryPointIV, - encryptedEntryPoint, - certTag, - certIV, - encryptedCert, - issuerTag, - issuerIV, - encryptedIssuer - } = ssoConfig; - - const orgBot = await orgBotDAL.findOne({ orgId: ssoConfig.orgId }); - if (!orgBot) - throw new NotFoundError({ - message: `Organization bot not found in organization with ID '${ssoConfig.orgId}'`, - name: "OrgBotNotFound" - }); - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: samlConfig.orgId }); let entryPoint = ""; - if (encryptedEntryPoint && entryPointIV && entryPointTag) { - entryPoint = decryptSymmetric({ - ciphertext: encryptedEntryPoint, - key, - tag: entryPointTag, - iv: entryPointIV - }); + if (samlConfig.encryptedSamlEntryPoint) { + entryPoint = decryptor({ cipherTextBlob: samlConfig.encryptedSamlEntryPoint }).toString(); } let issuer = ""; - if (encryptedIssuer && issuerTag && issuerIV) { - issuer = decryptSymmetric({ - key, - tag: issuerTag, - iv: issuerIV, - ciphertext: encryptedIssuer - }); + if (samlConfig.encryptedSamlIssuer) { + issuer = decryptor({ cipherTextBlob: samlConfig.encryptedSamlIssuer }).toString(); } let cert = ""; - if (encryptedCert && certTag && certIV) { - cert = decryptSymmetric({ key, tag: certTag, iv: certIV, ciphertext: encryptedCert }); + if (samlConfig.encryptedSamlCertificate) { + cert = decryptor({ cipherTextBlob: samlConfig.encryptedSamlCertificate }).toString(); } return { - id: ssoConfig.id, - organization: ssoConfig.orgId, - orgId: ssoConfig.orgId, - authProvider: ssoConfig.authProvider, - isActive: ssoConfig.isActive, + id: samlConfig.id, + organization: samlConfig.orgId, + orgId: samlConfig.orgId, + authProvider: samlConfig.authProvider, + isActive: samlConfig.isActive, entryPoint, issuer, cert, - lastUsed: ssoConfig.lastUsed + lastUsed: samlConfig.lastUsed }; }; @@ -421,14 +305,14 @@ export const samlConfigServiceFactory = ({ }); } else { const plan = await licenseService.getPlan(orgId); - if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) { + if (plan?.slug !== "enterprise" && plan?.memberLimit && plan.membersUsed >= plan.memberLimit) { // limit imposed on number of members allowed / number of members used exceeds the number of members allowed throw new BadRequestError({ message: "Failed to create new member via SAML due to member limit reached. Upgrade plan to add more members." }); } - if (plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { + if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed throw new BadRequestError({ message: "Failed to create new member via SAML due to member limit reached. Upgrade plan to add more members." 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 96cb91035..444839a21 100644 --- a/backend/src/ee/services/saml-config/saml-config-types.ts +++ b/backend/src/ee/services/saml-config/saml-config-types.ts @@ -6,7 +6,8 @@ export enum SamlProviders { AZURE_SAML = "azure-saml", JUMPCLOUD_SAML = "jumpcloud-saml", GOOGLE_SAML = "google-saml", - KEYCLOAK_SAML = "keycloak-saml" + KEYCLOAK_SAML = "keycloak-saml", + AUTH0_SAML = "auth0-saml" } export type TCreateSamlCfgDTO = { @@ -14,7 +15,7 @@ export type TCreateSamlCfgDTO = { isActive: boolean; entryPoint: string; issuer: string; - cert: string; + idpCert: string; } & TOrgPermission; export type TUpdateSamlCfgDTO = Partial<{ @@ -22,7 +23,7 @@ export type TUpdateSamlCfgDTO = Partial<{ isActive: boolean; entryPoint: string; issuer: string; - cert: string; + idpCert: string; }> & TOrgPermission; diff --git a/backend/src/ee/services/scim/scim-fns.ts b/backend/src/ee/services/scim/scim-fns.ts index 3ade1a117..d3af24f61 100644 --- a/backend/src/ee/services/scim/scim-fns.ts +++ b/backend/src/ee/services/scim/scim-fns.ts @@ -29,15 +29,9 @@ export const parseScimFilter = (filterToParse: string | undefined) => { attributeName = "name"; } - return { [attributeName]: parsedValue.replace(/"/g, "") }; + return { [attributeName]: parsedValue.replaceAll('"', "") }; }; -export function extractScimValueFromPath(path: string): string | null { - const regex = /members\[value eq "([^"]+)"\]/; - const match = path.match(regex); - return match ? match[1] : null; -} - export const buildScimUser = ({ orgMembershipId, username, diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts index 0f814d2ac..84cced88f 100644 --- a/backend/src/ee/services/scim/scim-service.ts +++ b/backend/src/ee/services/scim/scim-service.ts @@ -18,6 +18,7 @@ import { TGroupProjectDALFactory } from "@app/services/group-project/group-proje import { TOrgDALFactory } from "@app/services/org/org-dal"; import { deleteOrgMembershipFn } from "@app/services/org/org-fns"; import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; +import { OrgAuthMethod } from "@app/services/org/org-types"; 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"; @@ -71,6 +72,7 @@ type TScimServiceFactoryDep = { | "deleteMembershipById" | "transaction" | "updateMembershipById" + | "findOrgById" >; orgMembershipDAL: Pick< TOrgMembershipDALFactory, @@ -288,8 +290,7 @@ export const scimServiceFactory = ({ const createScimUser = async ({ externalId, email, firstName, lastName, orgId }: TCreateScimUserDTO) => { if (!email) throw new ScimRequestError({ detail: "Invalid request. Missing email.", status: 400 }); - const org = await orgDAL.findById(orgId); - + const org = await orgDAL.findOrgById(orgId); if (!org) throw new ScimRequestError({ detail: "Organization not found", @@ -302,13 +303,24 @@ export const scimServiceFactory = ({ status: 403 }); + if (!org.orgAuthMethod) { + throw new ScimRequestError({ + detail: "Neither SAML or OIDC SSO is configured", + status: 400 + }); + } + const appCfg = getConfig(); const serverCfg = await getServerCfg(); + const aliasType = org.orgAuthMethod === OrgAuthMethod.OIDC ? UserAliasType.OIDC : UserAliasType.SAML; + const trustScimEmails = + org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails; + const userAlias = await userAliasDAL.findOne({ externalId, orgId, - aliasType: UserAliasType.SAML + aliasType }); const { user: createdUser, orgMembership: createdOrgMembership } = await userDAL.transaction(async (tx) => { @@ -349,7 +361,7 @@ export const scimServiceFactory = ({ ); } } else { - if (serverCfg.trustSamlEmails) { + if (trustScimEmails) { user = await userDAL.findOne( { email, @@ -367,9 +379,9 @@ export const scimServiceFactory = ({ ); user = await userDAL.create( { - username: serverCfg.trustSamlEmails ? email : uniqueUsername, + username: trustScimEmails ? email : uniqueUsername, email, - isEmailVerified: serverCfg.trustSamlEmails, + isEmailVerified: trustScimEmails, firstName, lastName, authMethods: [], @@ -382,7 +394,7 @@ export const scimServiceFactory = ({ await userAliasDAL.create( { userId: user.id, - aliasType: UserAliasType.SAML, + aliasType, externalId, emails: email ? [email] : [], orgId @@ -437,7 +449,7 @@ export const scimServiceFactory = ({ recipients: [email], substitutions: { organizationName: org.name, - callback_url: `${appCfg.SITE_URL}/api/v1/sso/redirect/saml2/organizations/${org.slug}` + callback_url: `${appCfg.SITE_URL}/api/v1/sso/redirect/organizations/${org.slug}` } }); } @@ -456,6 +468,14 @@ export const scimServiceFactory = ({ // partial const updateScimUser = async ({ orgMembershipId, orgId, operations }: TUpdateScimUserDTO) => { + const org = await orgDAL.findOrgById(orgId); + if (!org.orgAuthMethod) { + throw new ScimRequestError({ + detail: "Neither SAML or OIDC SSO is configured", + status: 400 + }); + } + const [membership] = await orgDAL .findMembership({ [`${TableName.OrgMembership}.id` as "id"]: orgMembershipId, @@ -493,6 +513,9 @@ export const scimServiceFactory = ({ scimPatch(scimUser, operations); const serverCfg = await getServerCfg(); + const trustScimEmails = + org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails; + await userDAL.transaction(async (tx) => { await orgMembershipDAL.updateById( membership.id, @@ -508,7 +531,7 @@ export const scimServiceFactory = ({ firstName: scimUser.name.givenName, email: scimUser.emails[0].value, lastName: scimUser.name.familyName, - isEmailVerified: hasEmailChanged ? serverCfg.trustSamlEmails : true + isEmailVerified: hasEmailChanged ? trustScimEmails : undefined }, tx ); @@ -526,6 +549,14 @@ export const scimServiceFactory = ({ email, externalId }: TReplaceScimUserDTO) => { + const org = await orgDAL.findOrgById(orgId); + if (!org.orgAuthMethod) { + throw new ScimRequestError({ + detail: "Neither SAML or OIDC SSO is configured", + status: 400 + }); + } + const [membership] = await orgDAL .findMembership({ [`${TableName.OrgMembership}.id` as "id"]: orgMembershipId, @@ -555,7 +586,7 @@ export const scimServiceFactory = ({ await userAliasDAL.update( { orgId, - aliasType: UserAliasType.SAML, + aliasType: org.orgAuthMethod === OrgAuthMethod.OIDC ? UserAliasType.OIDC : UserAliasType.SAML, userId: membership.userId }, { @@ -563,6 +594,7 @@ export const scimServiceFactory = ({ }, tx ); + await orgMembershipDAL.updateById( membership.id, { @@ -576,7 +608,8 @@ export const scimServiceFactory = ({ firstName, email, lastName, - isEmailVerified: serverCfg.trustSamlEmails + isEmailVerified: + org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails }, tx ); @@ -758,6 +791,21 @@ export const scimServiceFactory = ({ }); const newGroup = await groupDAL.transaction(async (tx) => { + const conflictingGroup = await groupDAL.findOne( + { + name: displayName, + orgId + }, + tx + ); + + if (conflictingGroup) { + throw new ScimRequestError({ + detail: `Group with name '${displayName}' already exists in the organization`, + status: 409 + }); + } + const group = await groupDAL.create( { name: displayName, diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts index bb77660aa..6644b14b8 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts @@ -177,5 +177,10 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => { } }; - return { ...secretApprovalPolicyOrm, findById, find }; + const softDeleteById = async (policyId: string, tx?: Knex) => { + const softDeletedPolicy = await secretApprovalPolicyOrm.updateById(policyId, { deletedAt: new Date() }, tx); + return softDeletedPolicy; + }; + + return { ...secretApprovalPolicyOrm, findById, find, softDeleteById }; }; diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts index cb3452685..4c212e6cd 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import picomatch from "picomatch"; +import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -11,6 +12,8 @@ import { TUserDALFactory } from "@app/services/user/user-dal"; import { ApproverType } from "../access-approval-policy/access-approval-policy-types"; import { TLicenseServiceFactory } from "../license/license-service"; +import { TSecretApprovalRequestDALFactory } from "../secret-approval-request/secret-approval-request-dal"; +import { RequestState } from "../secret-approval-request/secret-approval-request-types"; import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal"; import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal"; import { @@ -34,6 +37,7 @@ type TSecretApprovalPolicyServiceFactoryDep = { userDAL: Pick; secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory; licenseService: Pick; + secretApprovalRequestDAL: Pick; }; export type TSecretApprovalPolicyServiceFactory = ReturnType; @@ -44,7 +48,8 @@ export const secretApprovalPolicyServiceFactory = ({ secretApprovalPolicyApproverDAL, projectEnvDAL, userDAL, - licenseService + licenseService, + secretApprovalRequestDAL }: TSecretApprovalPolicyServiceFactoryDep) => { const createSecretApprovalPolicy = async ({ name, @@ -57,7 +62,8 @@ export const secretApprovalPolicyServiceFactory = ({ projectId, secretPath, environment, - enforcementLevel + enforcementLevel, + allowedSelfApprovals }: TCreateSapDTO) => { const groupApprovers = approvers ?.filter((approver) => approver.type === ApproverType.Group) @@ -74,13 +80,14 @@ export const secretApprovalPolicyServiceFactory = ({ if (!groupApprovers.length && approvals > approvers.length) throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval @@ -107,7 +114,8 @@ export const secretApprovalPolicyServiceFactory = ({ approvals, secretPath, name, - enforcementLevel + enforcementLevel, + allowedSelfApprovals }, tx ); @@ -166,7 +174,8 @@ export const secretApprovalPolicyServiceFactory = ({ actorAuthMethod, approvals, secretPolicyId, - enforcementLevel + enforcementLevel, + allowedSelfApprovals }: TUpdateSapDTO) => { const groupApprovers = approvers ?.filter((approver) => approver.type === ApproverType.Group) @@ -187,13 +196,14 @@ export const secretApprovalPolicyServiceFactory = ({ }); } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - secretApprovalPolicy.projectId, + projectId: secretApprovalPolicy.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); const plan = await licenseService.getPlan(actorOrgId); @@ -211,7 +221,8 @@ export const secretApprovalPolicyServiceFactory = ({ approvals, secretPath, name, - enforcementLevel + enforcementLevel, + allowedSelfApprovals }, tx ); @@ -281,13 +292,14 @@ export const secretApprovalPolicyServiceFactory = ({ if (!sapPolicy) throw new NotFoundError({ message: `Secret approval policy with ID '${secretPolicyId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - sapPolicy.projectId, + projectId: sapPolicy.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval @@ -301,8 +313,16 @@ export const secretApprovalPolicyServiceFactory = ({ }); } - await secretApprovalPolicyDAL.deleteById(secretPolicyId); - return sapPolicy; + const deletedPolicy = await secretApprovalPolicyDAL.transaction(async (tx) => { + await secretApprovalRequestDAL.update( + { policyId: secretPolicyId, status: RequestState.Open }, + { status: RequestState.Closed }, + tx + ); + const updatedPolicy = await secretApprovalPolicyDAL.softDeleteById(secretPolicyId, tx); + return updatedPolicy; + }); + return { ...deletedPolicy, projectId: sapPolicy.projectId, environment: sapPolicy.environment }; }; const getSecretApprovalPolicyByProjectId = async ({ @@ -312,16 +332,17 @@ export const secretApprovalPolicyServiceFactory = ({ actorAuthMethod, projectId }: TListSapDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); - const sapPolicies = await secretApprovalPolicyDAL.find({ projectId }); + const sapPolicies = await secretApprovalPolicyDAL.find({ projectId, deletedAt: null }); return sapPolicies; }; @@ -334,7 +355,7 @@ export const secretApprovalPolicyServiceFactory = ({ }); } - const policies = await secretApprovalPolicyDAL.find({ envId: env.id }); + const policies = await secretApprovalPolicyDAL.find({ envId: env.id, deletedAt: null }); if (!policies.length) return; // this will filter policies either without scoped to secret path or the one that matches with secret path const policiesFilteredByPath = policies.filter( @@ -356,7 +377,14 @@ export const secretApprovalPolicyServiceFactory = ({ environment, secretPath }: TGetBoardSapDTO) => { - await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId); + await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); return getSecretApprovalPolicy(projectId, environment, secretPath); }; @@ -376,13 +404,14 @@ export const secretApprovalPolicyServiceFactory = ({ }); } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - sapPolicy.projectId, + projectId: sapPolicy.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts index 863f1c926..a6fea6956 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts @@ -10,6 +10,7 @@ export type TCreateSapDTO = { projectId: string; name: string; enforcementLevel: EnforcementLevel; + allowedSelfApprovals: boolean; } & Omit; export type TUpdateSapDTO = { @@ -19,6 +20,7 @@ export type TUpdateSapDTO = { approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[]; name?: string; enforcementLevel?: EnforcementLevel; + allowedSelfApprovals?: boolean; } & Omit; export type TDeleteSapDTO = { diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts index 803b9464c..be4a7ab3b 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts @@ -100,6 +100,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { tx.ref("lastName").withSchema("committerUser").as("committerUserLastName"), tx.ref("reviewerUserId").withSchema(TableName.SecretApprovalRequestReviewer), tx.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"), + tx.ref("comment").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerComment"), tx.ref("email").withSchema("secretApprovalReviewerUser").as("reviewerEmail"), tx.ref("username").withSchema("secretApprovalReviewerUser").as("reviewerUsername"), tx.ref("firstName").withSchema("secretApprovalReviewerUser").as("reviewerFirstName"), @@ -111,7 +112,9 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"), tx.ref("envId").withSchema(TableName.SecretApprovalPolicy).as("policyEnvId"), tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"), - tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals") + tx.ref("allowedSelfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policyAllowedSelfApprovals"), + tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"), + tx.ref("deletedAt").withSchema(TableName.SecretApprovalPolicy).as("policyDeletedAt") ); const findById = async (id: string, tx?: Knex) => { @@ -147,7 +150,9 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { approvals: el.policyApprovals, secretPath: el.policySecretPath, enforcementLevel: el.policyEnforcementLevel, - envId: el.policyEnvId + envId: el.policyEnvId, + deletedAt: el.policyDeletedAt, + allowedSelfApprovals: el.policyAllowedSelfApprovals } }), childrenMapper: [ @@ -160,8 +165,10 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { reviewerEmail: email, reviewerLastName: lastName, reviewerUsername: username, - reviewerFirstName: firstName - }) => (userId ? { userId, status, email, firstName, lastName, username } : undefined) + reviewerFirstName: firstName, + reviewerComment: comment + }) => + userId ? { userId, status, email, firstName, lastName, username, comment: comment ?? "" } : undefined }, { key: "approverUserId", @@ -222,6 +229,11 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { `${TableName.SecretApprovalRequest}.policyId`, `${TableName.SecretApprovalPolicyApprover}.policyId` ) + .join( + TableName.SecretApprovalPolicy, + `${TableName.SecretApprovalRequest}.policyId`, + `${TableName.SecretApprovalPolicy}.id` + ) .where({ projectId }) .andWhere( (bd) => @@ -229,6 +241,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { .where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId) .orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId) ) + .andWhere((bd) => void bd.where(`${TableName.SecretApprovalPolicy}.deletedAt`, null)) .select("status", `${TableName.SecretApprovalRequest}.id`) .groupBy(`${TableName.SecretApprovalRequest}.id`, "status") .count("status") @@ -325,6 +338,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { ), db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"), db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"), + db.ref("allowedSelfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policyAllowedSelfApprovals"), db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"), db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover), db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"), @@ -353,7 +367,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { name: el.policyName, approvals: el.policyApprovals, secretPath: el.policySecretPath, - enforcementLevel: el.policyEnforcementLevel + enforcementLevel: el.policyEnforcementLevel, + allowedSelfApprovals: el.policyAllowedSelfApprovals }, committerUser: { userId: el.committerUserId, @@ -471,6 +486,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { `DENSE_RANK() OVER (partition by ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."id" DESC) as rank` ), db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"), + db.ref("allowedSelfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policyAllowedSelfApprovals"), db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"), db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"), db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover), @@ -500,7 +516,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { name: el.policyName, approvals: el.policyApprovals, secretPath: el.policySecretPath, - enforcementLevel: el.policyEnforcementLevel + enforcementLevel: el.policyEnforcementLevel, + allowedSelfApprovals: el.policyAllowedSelfApprovals }, committerUser: { userId: el.committerUserId, diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-fns.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-fns.ts index 05b7280b2..58a39dfa7 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-fns.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-fns.ts @@ -36,7 +36,7 @@ export const sendApprovalEmailsFn = async ({ firstName: reviewerUser.firstName, projectName: project.name, organizationName: project.organization.name, - approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval?requestId=${secretApprovalRequest.id}` + approvalUrl: `${cfg.SITE_URL}/secret-manager/${project.id}/approval?requestId=${secretApprovalRequest.id}` }, template: SmtpTemplates.SecretApprovalRequestNeedsReview }); diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts index 77f38dd53..c1b18e43d 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts @@ -256,6 +256,12 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => { `${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`, db.ref("id").withSchema("secVerTag") ) + .leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`) + .leftJoin( + TableName.SecretRotationV2SecretMapping, + `${TableName.SecretV2}.id`, + `${TableName.SecretRotationV2SecretMapping}.secretId` + ) .select(selectAllTableCols(TableName.SecretApprovalRequestSecretV2)) .select({ secVerTagId: "secVerTag.id", @@ -279,7 +285,13 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => { db.ref("key").withSchema(TableName.SecretVersionV2).as("secVerKey"), db.ref("encryptedValue").withSchema(TableName.SecretVersionV2).as("secVerValue"), db.ref("encryptedComment").withSchema(TableName.SecretVersionV2).as("secVerComment") - ); + ) + .select( + db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue") + ) + .select(db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping)); const formatedDoc = sqlNestRelationships({ data: doc, key: "id", @@ -298,14 +310,16 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => { { key: "secretId", label: "secret" as const, - mapper: ({ orgSecVersion, orgSecKey, orgSecValue, orgSecComment, secretId }) => + mapper: ({ orgSecVersion, orgSecKey, orgSecValue, orgSecComment, secretId, rotationId }) => secretId ? { id: secretId, version: orgSecVersion, key: orgSecKey, encryptedValue: orgSecValue, - encryptedComment: orgSecComment + encryptedComment: orgSecComment, + isRotatedSecret: Boolean(rotationId), + rotationId } : undefined }, @@ -338,9 +352,19 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => { }) } ] + }, + { + key: "metadataId", + label: "oldSecretMetadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) } ] }); + return formatedDoc?.map(({ secret, secretVersion, ...el }) => ({ ...el, secret: secret?.[0], diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index a39f44fd6..2f340626b 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -1,10 +1,12 @@ import { ForbiddenError, subject } from "@casl/ability"; import { + ActionProjectType, ProjectMembershipRole, SecretEncryptionAlgo, SecretKeyEncoding, SecretType, + TableName, TSecretApprovalRequestsSecretsInsert, TSecretApprovalRequestsSecretsV2Insert } from "@app/db/schemas"; @@ -21,6 +23,8 @@ import { KmsDataKey } from "@app/services/kms/kms-types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; +import { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal"; +import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema"; import { TSecretDALFactory } from "@app/services/secret/secret-dal"; import { decryptSecretWithBot, @@ -54,8 +58,9 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { TUserDALFactory } from "@app/services/user/user-dal"; import { TLicenseServiceFactory } from "../license/license-service"; +import { throwIfMissingSecretReadValueOrDescribePermission } from "../permission/permission-fns"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { ProjectPermissionSecretActions, ProjectPermissionSub } from "../permission/project-permission"; import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal"; import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service"; import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal"; @@ -85,11 +90,17 @@ type TSecretApprovalRequestServiceFactoryDep = { secretDAL: TSecretDALFactory; secretTagDAL: Pick< TSecretTagDALFactory, - "findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret" | "saveTagsToSecretV2" | "deleteTagsToSecretV2" + | "findManyTagsById" + | "saveTagsToSecret" + | "deleteTagsManySecret" + | "saveTagsToSecretV2" + | "deleteTagsToSecretV2" + | "find" >; secretBlindIndexDAL: Pick; snapshotService: Pick; secretVersionDAL: Pick; + resourceMetadataDAL: Pick; secretVersionTagDAL: Pick; smtpService: Pick; userDAL: Pick; @@ -102,7 +113,13 @@ type TSecretApprovalRequestServiceFactoryDep = { kmsService: Pick; secretV2BridgeDAL: Pick< TSecretV2BridgeDALFactory, - "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" + | "insertMany" + | "upsertSecretReferences" + | "findBySecretKeys" + | "bulkUpdate" + | "deleteMany" + | "find" + | "invalidateSecretCacheByProjectId" >; secretVersionV2BridgeDAL: Pick; secretVersionTagV2BridgeDAL: Pick; @@ -137,18 +154,20 @@ export const secretApprovalRequestServiceFactory = ({ secretVersionV2BridgeDAL, secretVersionTagV2BridgeDAL, licenseService, - projectSlackConfigDAL + projectSlackConfigDAL, + resourceMetadataDAL }: TSecretApprovalRequestServiceFactoryDep) => { const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => { if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); - await permissionService.getProjectPermission( - actor as ActorType.USER, + await permissionService.getProjectPermission({ + actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); const count = await secretApprovalRequestDAL.findProjectRequestCount(projectId, actorId); return count; @@ -168,7 +187,14 @@ export const secretApprovalRequestServiceFactory = ({ }: TListApprovalsDTO) => { if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); - await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId); + await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); if (shouldUseSecretV2Bridge) { @@ -211,13 +237,14 @@ export const secretApprovalRequestServiceFactory = ({ const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); const { policy } = secretApprovalRequest; - const { hasRole } = await permissionService.getProjectPermission( + const { hasRole } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); if ( !hasRole(ProjectMembershipRole.Admin) && secretApprovalRequest.committerUserId !== actorId && @@ -232,15 +259,23 @@ export const secretApprovalRequestServiceFactory = ({ type: KmsDataKey.SecretManager, projectId }); - const encrypedSecrets = await secretApprovalRequestSecretDAL.findByRequestIdBridgeSecretV2( + const encryptedSecrets = await secretApprovalRequestSecretDAL.findByRequestIdBridgeSecretV2( secretApprovalRequest.id ); - secrets = encrypedSecrets.map((el) => ({ + secrets = encryptedSecrets.map((el) => ({ ...el, secretKey: el.key, id: el.id, version: el.version, - secretValue: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + secretMetadata: el.secretMetadata as ResourceMetadataDTO, + isRotatedSecret: el.secret?.isRotatedSecret ?? false, + secretValue: + // eslint-disable-next-line no-nested-ternary + el.secret && el.secret.isRotatedSecret + ? undefined + : el.encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() + : "", secretComment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "", @@ -268,14 +303,15 @@ export const secretApprovalRequestServiceFactory = ({ secretComment: el.secretVersion.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString() : "", - tags: el.secretVersion.tags + tags: el.secretVersion.tags, + secretMetadata: el.oldSecretMetadata as ResourceMetadataDTO } : undefined })); } else { if (!botKey) throw new NotFoundError({ message: `Project bot key not found`, name: "BotKeyNotFound" }); // CLI depends on this error message. TODO(daniel): Make API check for name BotKeyNotFound instead of message - const encrypedSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id); - secrets = encrypedSecrets.map((el) => ({ + const encryptedSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id); + secrets = encryptedSecrets.map((el) => ({ ...el, ...decryptSecretWithBot(el, botKey), secret: el.secret @@ -304,6 +340,7 @@ export const secretApprovalRequestServiceFactory = ({ approvalId, actor, status, + comment, actorId, actorAuthMethod, actorOrgId @@ -323,13 +360,25 @@ export const secretApprovalRequestServiceFactory = ({ } const { policy } = secretApprovalRequest; - const { hasRole } = await permissionService.getProjectPermission( - ActorType.USER, + if (policy.deletedAt) { + throw new BadRequestError({ + message: "The policy associated with this secret approval request has been deleted." + }); + } + if (!policy.allowedSelfApprovals && actorId === secretApprovalRequest.committerUserId) { + throw new BadRequestError({ + message: "Failed to review secret approval request. Users are not authorized to review their own request." + }); + } + + const { hasRole } = await permissionService.getProjectPermission({ + actor: ActorType.USER, actorId, - secretApprovalRequest.projectId, + projectId: secretApprovalRequest.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); if ( !hasRole(ProjectMembershipRole.Admin) && secretApprovalRequest.committerUserId !== actorId && @@ -349,15 +398,18 @@ export const secretApprovalRequestServiceFactory = ({ return secretApprovalRequestReviewerDAL.create( { status, + comment, requestId: secretApprovalRequest.id, reviewerUserId: actorId }, tx ); } - return secretApprovalRequestReviewerDAL.updateById(review.id, { status }, tx); + + return secretApprovalRequestReviewerDAL.updateById(review.id, { status, comment }, tx); }); - return reviewStatus; + + return { ...reviewStatus, projectId: secretApprovalRequest.projectId }; }; const updateApprovalStatus = async ({ @@ -383,13 +435,20 @@ export const secretApprovalRequestServiceFactory = ({ } const { policy } = secretApprovalRequest; - const { hasRole } = await permissionService.getProjectPermission( - ActorType.USER, + if (policy.deletedAt) { + throw new BadRequestError({ + message: "The policy associated with this secret approval request has been deleted." + }); + } + + const { hasRole } = await permissionService.getProjectPermission({ + actor: ActorType.USER, actorId, - secretApprovalRequest.projectId, + projectId: secretApprovalRequest.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); if ( !hasRole(ProjectMembershipRole.Admin) && secretApprovalRequest.committerUserId !== actorId && @@ -433,13 +492,20 @@ export const secretApprovalRequestServiceFactory = ({ } const { policy, folderId, projectId } = secretApprovalRequest; - const { hasRole } = await permissionService.getProjectPermission( - ActorType.USER, + if (policy.deletedAt) { + throw new BadRequestError({ + message: "The policy associated with this secret approval request has been deleted." + }); + } + + const { hasRole } = await permissionService.getProjectPermission({ + actor: ActorType.USER, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); if ( !hasRole(ProjectMembershipRole.Admin) && @@ -462,7 +528,7 @@ export const secretApprovalRequestServiceFactory = ({ if (!hasMinApproval && !isSoftEnforcement) throw new BadRequestError({ message: "Doesn't have minimum approvals needed" }); - const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); + const { botKey, shouldUseSecretV2Bridge, project } = await projectBotService.getBotKey(projectId); let mergeStatus; if (shouldUseSecretV2Bridge) { // this cycle if for bridged secrets @@ -524,6 +590,7 @@ export const secretApprovalRequestServiceFactory = ({ ? await fnSecretV2BridgeBulkInsert({ tx, folderId, + orgId: actorOrgId, inputSecrets: secretCreationCommits.map((el) => ({ tagIds: el?.tags.map(({ id }) => id), version: 1, @@ -531,6 +598,7 @@ export const secretApprovalRequestServiceFactory = ({ encryptedValue: el.encryptedValue, skipMultilineEncoding: el.skipMultilineEncoding, key: el.key, + secretMetadata: el.secretMetadata as ResourceMetadataDTO, references: el.encryptedValue ? getAllSecretReferencesV2Bridge( secretManagerDecryptor({ @@ -540,6 +608,7 @@ export const secretApprovalRequestServiceFactory = ({ : [], type: SecretType.Shared })), + resourceMetadataDAL, secretDAL: secretV2BridgeDAL, secretVersionDAL: secretVersionV2BridgeDAL, secretTagDAL, @@ -549,10 +618,11 @@ export const secretApprovalRequestServiceFactory = ({ const updatedSecrets = secretUpdationCommits.length ? await fnSecretV2BridgeBulkUpdate({ folderId, + orgId: actorOrgId, tx, inputSecrets: secretUpdationCommits.map((el) => { const encryptedValue = - typeof el.encryptedValue !== "undefined" + !el.secret?.isRotatedSecret && typeof el.encryptedValue !== "undefined" ? { encryptedValue: el.encryptedValue as Buffer, references: el.encryptedValue @@ -573,6 +643,7 @@ export const secretApprovalRequestServiceFactory = ({ skipMultilineEncoding: el.skipMultilineEncoding, key: el.key, tags: el?.tags.map(({ id }) => id), + secretMetadata: el.secretMetadata as ResourceMetadataDTO, ...encryptedValue } }; @@ -580,7 +651,8 @@ export const secretApprovalRequestServiceFactory = ({ secretDAL: secretV2BridgeDAL, secretVersionDAL: secretVersionV2BridgeDAL, secretTagDAL, - secretVersionTagDAL: secretVersionTagV2BridgeDAL + secretVersionTagDAL: secretVersionTagV2BridgeDAL, + resourceMetadataDAL }) : []; const deletedSecret = secretDeletionCommits.length @@ -798,6 +870,7 @@ export const secretApprovalRequestServiceFactory = ({ }); } + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId); await snapshotService.performSnapshot(folderId); const [folder] = await folderDAL.findSecretPathByFolderIds(projectId, [folderId]); if (!folder) { @@ -805,6 +878,7 @@ export const secretApprovalRequestServiceFactory = ({ } await secretQueueService.syncSecrets({ projectId, + orgId: actorOrgId, secretPath: folder.path, environmentSlug: folder.environmentSlug, actorId, @@ -813,7 +887,6 @@ export const secretApprovalRequestServiceFactory = ({ if (isSoftEnforcement) { const cfg = getConfig(); - const project = await projectDAL.findProjectById(projectId); const env = await projectEnvDAL.findOne({ id: policy.envId }); const requestedByUser = await userDAL.findOne({ id: actorId }); const approverUsers = await userDAL.find({ @@ -833,7 +906,7 @@ export const secretApprovalRequestServiceFactory = ({ bypassReason, secretPath: policy.secretPath, environment: env.name, - approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval` + approvalUrl: `${cfg.SITE_URL}/secret-manager/${project.id}/approval` }, template: SmtpTemplates.AccessSecretRequestBypassed }); @@ -857,17 +930,19 @@ export const secretApprovalRequestServiceFactory = ({ }: TGenerateSecretApprovalRequestDTO) => { if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath + }); await projectDAL.checkProjectUpgradeStatus(projectId); @@ -952,6 +1027,7 @@ export const secretApprovalRequestServiceFactory = ({ : keyName2BlindIndex[secretName]; // add tags if (tagIds?.length) commitTagIds[keyName2BlindIndex[secretName]] = tagIds; + return { ...latestSecretVersions[secretId], ...el, @@ -1107,7 +1183,8 @@ export const secretApprovalRequestServiceFactory = ({ environment: env.name, secretPath, projectId, - requestId: secretApprovalRequest.id + requestId: secretApprovalRequest.id, + secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretName) ?? []))] } } }); @@ -1137,14 +1214,14 @@ export const secretApprovalRequestServiceFactory = ({ if (actor === ActorType.SERVICE || actor === ActorType.Machine) throw new BadRequestError({ message: "Cannot use service token or machine token over protected branches" }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); - + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) throw new NotFoundError({ @@ -1188,6 +1265,7 @@ export const secretApprovalRequestServiceFactory = ({ ), skipMultilineEncoding: createdSecret.skipMultilineEncoding, key: createdSecret.secretKey, + secretMetadata: createdSecret.secretMetadata, type: SecretType.Shared })) ); @@ -1221,9 +1299,10 @@ export const secretApprovalRequestServiceFactory = ({ type: SecretType.Shared })) ); - if (secrets.length) + + if (secrets.length !== secretsWithNewName.length) throw new NotFoundError({ - message: `Secret does not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}` + message: `Secret does not exist: ${secrets.map((el) => el.key).join(",")}` }); } @@ -1243,12 +1322,14 @@ export const secretApprovalRequestServiceFactory = ({ reminderNote, secretComment, metadata, - skipMultilineEncoding + skipMultilineEncoding, + secretMetadata }) => { const secretId = updatingSecretsGroupByKey[secretKey][0].id; - if (tagIds?.length) commitTagIds[secretKey] = tagIds; + if (tagIds?.length) commitTagIds[newSecretName ?? secretKey] = tagIds; return { ...latestSecretVersions[secretId], + secretMetadata, key: newSecretName || secretKey, encryptedComment: setKnexStringValue( secretComment, @@ -1274,17 +1355,48 @@ export const secretApprovalRequestServiceFactory = ({ // deleted secrets const deletedSecrets = data[SecretOperations.Delete]; if (deletedSecrets && deletedSecrets.length) { - const secretsToDeleteInDB = await secretV2BridgeDAL.findBySecretKeys( + const secretsToDeleteInDB = await secretV2BridgeDAL.find({ folderId, - deletedSecrets.map((el) => ({ - key: el.secretKey, - type: SecretType.Shared - })) - ); + $complex: { + operator: "and", + value: [ + { + operator: "or", + value: deletedSecrets.map((el) => ({ + operator: "and", + value: [ + { + operator: "eq", + field: `${TableName.SecretV2}.key` as "key", + value: el.secretKey + }, + { + operator: "eq", + field: "type", + value: SecretType.Shared + } + ] + })) + } + ] + } + }); if (secretsToDeleteInDB.length !== deletedSecrets.length) throw new NotFoundError({ message: `Secret does not exist: ${secretsToDeleteInDB.map((el) => el.key).join(",")}` }); + secretsToDeleteInDB.forEach((el) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.Delete, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: el.key, + secretTags: el.tags?.map((i) => i.slug) + }) + ); + }); + const secretsGroupedByKey = groupBy(secretsToDeleteInDB, (i) => i.key); const deletedSecretIds = deletedSecrets.map((el) => secretsGroupedByKey[el.secretKey][0].id); const latestSecretVersions = await secretVersionV2BridgeDAL.findLatestVersionMany(folderId, deletedSecretIds); @@ -1310,9 +1422,9 @@ export const secretApprovalRequestServiceFactory = ({ const tagsGroupById = groupBy(tags, (i) => i.id); commits.forEach((commit) => { - let action = ProjectPermissionActions.Create; - if (commit.op === SecretOperations.Update) action = ProjectPermissionActions.Edit; - if (commit.op === SecretOperations.Delete) action = ProjectPermissionActions.Delete; + let action = ProjectPermissionSecretActions.Create; + if (commit.op === SecretOperations.Update) action = ProjectPermissionSecretActions.Edit; + if (commit.op === SecretOperations.Delete) return; // we do the validation on top ForbiddenError.from(permission).throwUnlessCan( action, @@ -1350,7 +1462,8 @@ export const secretApprovalRequestServiceFactory = ({ reminderRepeatDays, encryptedValue, secretId, - secretVersion + secretVersion, + secretMetadata }) => ({ version, requestId: doc.id, @@ -1363,7 +1476,8 @@ export const secretApprovalRequestServiceFactory = ({ reminderRepeatDays, reminderNote, encryptedComment, - key + key, + secretMetadata: JSON.stringify(secretMetadata) }) ), tx @@ -1401,7 +1515,8 @@ export const secretApprovalRequestServiceFactory = ({ environment: env.name, secretPath, projectId, - requestId: secretApprovalRequest.id + requestId: secretApprovalRequest.id, + secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretKey) ?? []))] } } }); diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts index 50a70fd60..5d6358072 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts @@ -1,5 +1,6 @@ import { TImmutableDBKeys, TSecretApprovalPolicies, TSecretApprovalRequestsSecrets } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; +import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema"; import { SecretOperations } from "@app/services/secret/secret-types"; export enum RequestState { @@ -34,6 +35,7 @@ export type TApprovalCreateSecretV2Bridge = { reminderRepeatDays?: number | null; skipMultilineEncoding?: boolean; metadata?: Record; + secretMetadata?: ResourceMetadataDTO; tagIds?: string[]; }; @@ -78,6 +80,7 @@ export type TStatusChangeDTO = { export type TReviewRequestDTO = { approvalId: string; status: ApprovalStatus; + comment?: string; } & Omit; export type TApprovalRequestCountDTO = TProjectPermission; diff --git a/backend/src/ee/services/secret-replication/secret-replication-service.ts b/backend/src/ee/services/secret-replication/secret-replication-service.ts index 81d467bab..90fdf561e 100644 --- a/backend/src/ee/services/secret-replication/secret-replication-service.ts +++ b/backend/src/ee/services/secret-replication/secret-replication-service.ts @@ -13,6 +13,8 @@ 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 { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; +import { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal"; +import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema"; import { TSecretDALFactory } from "@app/services/secret/secret-dal"; import { fnSecretBulkInsert, fnSecretBulkUpdate } from "@app/services/secret/secret-fns"; import { TSecretQueueFactory, uniqueSecretQueueKey } from "@app/services/secret/secret-queue"; @@ -43,7 +45,14 @@ type TSecretReplicationServiceFactoryDep = { secretVersionDAL: Pick; secretV2BridgeDAL: Pick< TSecretV2BridgeDALFactory, - "find" | "findBySecretKeys" | "insertMany" | "bulkUpdate" | "delete" | "upsertSecretReferences" | "transaction" + | "find" + | "findBySecretKeys" + | "insertMany" + | "bulkUpdate" + | "delete" + | "upsertSecretReferences" + | "transaction" + | "invalidateSecretCacheByProjectId" >; secretVersionV2BridgeDAL: Pick< TSecretVersionV2DALFactory, @@ -56,6 +65,7 @@ type TSecretReplicationServiceFactoryDep = { >; secretVersionTagDAL: Pick; secretVersionV2TagBridgeDAL: Pick; + resourceMetadataDAL: Pick; secretQueueService: Pick; queueService: Pick; secretApprovalPolicyService: Pick; @@ -121,7 +131,8 @@ export const secretReplicationServiceFactory = ({ secretVersionV2TagBridgeDAL, secretVersionV2BridgeDAL, secretV2BridgeDAL, - kmsService + kmsService, + resourceMetadataDAL }: TSecretReplicationServiceFactoryDep) => { const $getReplicatedSecrets = ( botKey: string, @@ -151,8 +162,10 @@ export const secretReplicationServiceFactory = ({ }; const $getReplicatedSecretsV2 = ( - localSecrets: (TSecretsV2 & { secretKey: string; secretValue?: string })[], - importedSecrets: { secrets: (TSecretsV2 & { secretKey: string; secretValue?: string })[] }[] + localSecrets: (TSecretsV2 & { secretKey: string; secretValue?: string; secretMetadata?: ResourceMetadataDTO })[], + importedSecrets: { + secrets: (TSecretsV2 & { secretKey: string; secretValue?: string; secretMetadata?: ResourceMetadataDTO })[]; + }[] ) => { const deDupe = new Set(); const secrets = [...localSecrets]; @@ -178,6 +191,7 @@ export const secretReplicationServiceFactory = ({ secretPath, environmentSlug, projectId, + orgId, actorId, actor, pickOnlyImportIds, @@ -222,6 +236,7 @@ export const secretReplicationServiceFactory = ({ .map(({ folderId }) => secretQueueService.replicateSecrets({ projectId, + orgId, secretPath: foldersGroupedById[folderId][0]?.path as string, environmentSlug: foldersGroupedById[folderId][0]?.environmentSlug as string, actorId, @@ -257,6 +272,7 @@ export const secretReplicationServiceFactory = ({ folderDAL, secretImportDAL, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), + viewSecretValue: true, hasSecretAccess: () => true }); // secrets that gets replicated across imports @@ -267,6 +283,7 @@ export const secretReplicationServiceFactory = ({ ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined })); + const sourceSecrets = $getReplicatedSecretsV2(sourceDecryptedLocalSecrets, sourceImportedSecrets); const sourceSecretsGroupByKey = groupBy(sourceSecrets, (i) => i.key); @@ -333,13 +350,29 @@ export const secretReplicationServiceFactory = ({ .map((el) => ({ ...el, operation: SecretOperations.Create })); // rewrite update ops to create const locallyUpdatedSecrets = sourceSecrets - .filter( - ({ key, secretKey, secretValue }) => + .filter(({ key, secretKey, secretValue, secretMetadata }) => { + const sourceSecretMetadataJson = JSON.stringify( + (secretMetadata ?? []).map((entry) => ({ + key: entry.key, + value: entry.value + })) + ); + + const destinationSecretMetadataJson = JSON.stringify( + (destinationLocalSecretsGroupedByKey[key]?.[0]?.secretMetadata ?? []).map((entry) => ({ + key: entry.key, + value: entry.value + })) + ); + + return ( destinationLocalSecretsGroupedByKey[key]?.[0] && // if key or value changed (destinationLocalSecretsGroupedByKey[key]?.[0]?.secretKey !== secretKey || - destinationLocalSecretsGroupedByKey[key]?.[0]?.secretValue !== secretValue) - ) + destinationLocalSecretsGroupedByKey[key]?.[0]?.secretValue !== secretValue || + sourceSecretMetadataJson !== destinationSecretMetadataJson) + ); + }) .map((el) => ({ ...el, operation: SecretOperations.Update })); // rewrite update ops to create const locallyDeletedSecrets = destinationLocalSecrets @@ -387,6 +420,7 @@ export const secretReplicationServiceFactory = ({ op: operation, requestId: approvalRequestDoc.id, metadata: doc.metadata, + secretMetadata: JSON.stringify(doc.secretMetadata), key: doc.key, encryptedValue: doc.encryptedValue, encryptedComment: doc.encryptedComment, @@ -406,10 +440,12 @@ export const secretReplicationServiceFactory = ({ if (locallyCreatedSecrets.length) { await fnSecretV2BridgeBulkInsert({ folderId: destinationReplicationFolderId, + orgId, secretVersionDAL: secretVersionV2BridgeDAL, secretDAL: secretV2BridgeDAL, tx, secretTagDAL, + resourceMetadataDAL, secretVersionTagDAL: secretVersionV2TagBridgeDAL, inputSecrets: locallyCreatedSecrets.map((doc) => { return { @@ -419,6 +455,7 @@ export const secretReplicationServiceFactory = ({ encryptedValue: doc.encryptedValue, encryptedComment: doc.encryptedComment, skipMultilineEncoding: doc.skipMultilineEncoding, + secretMetadata: doc.secretMetadata, references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : [] }; }) @@ -426,10 +463,12 @@ export const secretReplicationServiceFactory = ({ } if (locallyUpdatedSecrets.length) { await fnSecretV2BridgeBulkUpdate({ + orgId, folderId: destinationReplicationFolderId, secretVersionDAL: secretVersionV2BridgeDAL, secretDAL: secretV2BridgeDAL, tx, + resourceMetadataDAL, secretTagDAL, secretVersionTagDAL: secretVersionV2TagBridgeDAL, inputSecrets: locallyUpdatedSecrets.map((doc) => { @@ -445,6 +484,7 @@ export const secretReplicationServiceFactory = ({ encryptedValue: doc.encryptedValue as Buffer, encryptedComment: doc.encryptedComment, skipMultilineEncoding: doc.skipMultilineEncoding, + secretMetadata: doc.secretMetadata, references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : [] } }; @@ -464,8 +504,10 @@ export const secretReplicationServiceFactory = ({ } }); + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId); await secretQueueService.syncSecrets({ projectId, + orgId, secretPath: destinationFolder.path, environmentSlug: destinationFolder.environmentSlug, actorId, @@ -751,6 +793,7 @@ export const secretReplicationServiceFactory = ({ await secretQueueService.syncSecrets({ projectId, + orgId, secretPath: destinationFolder.path, environmentSlug: destinationFolder.environmentSlug, actorId, diff --git a/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-constants.ts b/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-constants.ts new file mode 100644 index 000000000..a4d0a956c --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-constants.ts @@ -0,0 +1,15 @@ +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +export const AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = { + name: "Auth0 Client Secret", + type: SecretRotation.Auth0ClientSecret, + connection: AppConnection.Auth0, + template: { + secretsMapping: { + clientId: "AUTH0_CLIENT_ID", + clientSecret: "AUTH0_CLIENT_SECRET" + } + } +}; diff --git a/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-fns.ts new file mode 100644 index 000000000..6debd2402 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-fns.ts @@ -0,0 +1,104 @@ +import { + TAuth0ClientSecretRotationGeneratedCredentials, + TAuth0ClientSecretRotationWithConnection +} from "@app/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-types"; +import { + TRotationFactory, + TRotationFactoryGetSecretsPayload, + TRotationFactoryIssueCredentials, + TRotationFactoryRevokeCredentials, + TRotationFactoryRotateCredentials +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { request } from "@app/lib/config/request"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; +import { getAuth0ConnectionAccessToken } from "@app/services/app-connection/auth0"; + +import { generatePassword } from "../shared/utils"; + +export const auth0ClientSecretRotationFactory: TRotationFactory< + TAuth0ClientSecretRotationWithConnection, + TAuth0ClientSecretRotationGeneratedCredentials +> = (secretRotation, appConnectionDAL, kmsService) => { + const { + connection, + parameters: { clientId }, + secretsMapping + } = secretRotation; + + const $rotateClientSecret = async () => { + const accessToken = await getAuth0ConnectionAccessToken(connection, appConnectionDAL, kmsService); + const { audience } = connection.credentials; + await blockLocalAndPrivateIpAddresses(audience); + const clientSecret = generatePassword(); + + await request.request({ + method: "PATCH", + url: `${audience}clients/${clientId}`, + headers: { authorization: `Bearer ${accessToken}` }, + data: { + client_secret: clientSecret + } + }); + + return { clientId, clientSecret }; + }; + + const issueCredentials: TRotationFactoryIssueCredentials = async ( + callback + ) => { + const credentials = await $rotateClientSecret(); + + return callback(credentials); + }; + + const revokeCredentials: TRotationFactoryRevokeCredentials = async ( + _, + callback + ) => { + const accessToken = await getAuth0ConnectionAccessToken(connection, appConnectionDAL, kmsService); + const { audience } = connection.credentials; + await blockLocalAndPrivateIpAddresses(audience); + + // we just trigger an auth0 rotation to negate our credentials + await request.request({ + method: "POST", + url: `${audience}clients/${clientId}/rotate-secret`, + headers: { authorization: `Bearer ${accessToken}` } + }); + + return callback(); + }; + + const rotateCredentials: TRotationFactoryRotateCredentials = async ( + _, + callback + ) => { + const credentials = await $rotateClientSecret(); + + return callback(credentials); + }; + + const getSecretsPayload: TRotationFactoryGetSecretsPayload = ( + generatedCredentials + ) => { + const secrets = [ + { + key: secretsMapping.clientId, + value: generatedCredentials.clientId + }, + { + key: secretsMapping.clientSecret, + value: generatedCredentials.clientSecret + } + ]; + + return secrets; + }; + + return { + issueCredentials, + revokeCredentials, + rotateCredentials, + getSecretsPayload + }; +}; diff --git a/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-schemas.ts b/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-schemas.ts new file mode 100644 index 000000000..3a0ba265b --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-schemas.ts @@ -0,0 +1,67 @@ +import { z } from "zod"; + +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { + BaseCreateSecretRotationSchema, + BaseSecretRotationSchema, + BaseUpdateSecretRotationSchema +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas"; +import { SecretRotations } from "@app/lib/api-docs"; +import { SecretNameSchema } from "@app/server/lib/schemas"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +export const Auth0ClientSecretRotationGeneratedCredentialsSchema = z + .object({ + clientId: z.string(), + clientSecret: z.string() + }) + .array() + .min(1) + .max(2); + +const Auth0ClientSecretRotationParametersSchema = z.object({ + clientId: z + .string() + .trim() + .min(1, "Client ID Required") + .describe(SecretRotations.PARAMETERS.AUTH0_CLIENT_SECRET.clientId) +}); + +const Auth0ClientSecretRotationSecretsMappingSchema = z.object({ + clientId: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AUTH0_CLIENT_SECRET.clientId), + clientSecret: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AUTH0_CLIENT_SECRET.clientSecret) +}); + +export const Auth0ClientSecretRotationTemplateSchema = z.object({ + secretsMapping: z.object({ + clientId: z.string(), + clientSecret: z.string() + }) +}); + +export const Auth0ClientSecretRotationSchema = BaseSecretRotationSchema(SecretRotation.Auth0ClientSecret).extend({ + type: z.literal(SecretRotation.Auth0ClientSecret), + parameters: Auth0ClientSecretRotationParametersSchema, + secretsMapping: Auth0ClientSecretRotationSecretsMappingSchema +}); + +export const CreateAuth0ClientSecretRotationSchema = BaseCreateSecretRotationSchema( + SecretRotation.Auth0ClientSecret +).extend({ + parameters: Auth0ClientSecretRotationParametersSchema, + secretsMapping: Auth0ClientSecretRotationSecretsMappingSchema +}); + +export const UpdateAuth0ClientSecretRotationSchema = BaseUpdateSecretRotationSchema( + SecretRotation.Auth0ClientSecret +).extend({ + parameters: Auth0ClientSecretRotationParametersSchema.optional(), + secretsMapping: Auth0ClientSecretRotationSecretsMappingSchema.optional() +}); + +export const Auth0ClientSecretRotationListItemSchema = z.object({ + name: z.literal("Auth0 Client Secret"), + connection: z.literal(AppConnection.Auth0), + type: z.literal(SecretRotation.Auth0ClientSecret), + template: Auth0ClientSecretRotationTemplateSchema +}); diff --git a/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-types.ts b/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-types.ts new file mode 100644 index 000000000..b3bb4ec35 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-types.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; + +import { TAuth0Connection } from "@app/services/app-connection/auth0"; + +import { + Auth0ClientSecretRotationGeneratedCredentialsSchema, + Auth0ClientSecretRotationListItemSchema, + Auth0ClientSecretRotationSchema, + CreateAuth0ClientSecretRotationSchema +} from "./auth0-client-secret-rotation-schemas"; + +export type TAuth0ClientSecretRotation = z.infer; + +export type TAuth0ClientSecretRotationInput = z.infer; + +export type TAuth0ClientSecretRotationListItem = z.infer; + +export type TAuth0ClientSecretRotationWithConnection = TAuth0ClientSecretRotation & { + connection: TAuth0Connection; +}; + +export type TAuth0ClientSecretRotationGeneratedCredentials = z.infer< + typeof Auth0ClientSecretRotationGeneratedCredentialsSchema +>; diff --git a/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/index.ts b/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/index.ts new file mode 100644 index 000000000..0c595be48 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/auth0-client-secret/index.ts @@ -0,0 +1,3 @@ +export * from "./auth0-client-secret-rotation-constants"; +export * from "./auth0-client-secret-rotation-schemas"; +export * from "./auth0-client-secret-rotation-types"; diff --git a/backend/src/ee/services/secret-rotation-v2/mssql-credentials/index.ts b/backend/src/ee/services/secret-rotation-v2/mssql-credentials/index.ts new file mode 100644 index 000000000..3ee1cf450 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/mssql-credentials/index.ts @@ -0,0 +1,3 @@ +export * from "./mssql-credentials-rotation-constants"; +export * from "./mssql-credentials-rotation-schemas"; +export * from "./mssql-credentials-rotation-types"; diff --git a/backend/src/ee/services/secret-rotation-v2/mssql-credentials/mssql-credentials-rotation-constants.ts b/backend/src/ee/services/secret-rotation-v2/mssql-credentials/mssql-credentials-rotation-constants.ts new file mode 100644 index 000000000..b256bd77f --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/mssql-credentials/mssql-credentials-rotation-constants.ts @@ -0,0 +1,29 @@ +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +export const MSSQL_CREDENTIALS_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = { + name: "Microsoft SQL Server Credentials", + type: SecretRotation.MsSqlCredentials, + connection: AppConnection.MsSql, + template: { + createUserStatement: `-- Create login at the server level +CREATE LOGIN [infisical_user] WITH PASSWORD = 'my-password'; + +-- Grant server-level connect permission +GRANT CONNECT SQL TO [infisical_user]; + +-- Switch to the database where you want to create the user +USE my_database; + +-- Create the database user mapped to the login +CREATE USER [infisical_user] FOR LOGIN [infisical_user]; + +-- Grant permissions to the user on the schema in this database +GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::dbo TO [infisical_user];`, + secretsMapping: { + username: "MSSQL_DB_USERNAME", + password: "MSSQL_DB_PASSWORD" + } + } +}; diff --git a/backend/src/ee/services/secret-rotation-v2/mssql-credentials/mssql-credentials-rotation-schemas.ts b/backend/src/ee/services/secret-rotation-v2/mssql-credentials/mssql-credentials-rotation-schemas.ts new file mode 100644 index 000000000..3f02d8144 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/mssql-credentials/mssql-credentials-rotation-schemas.ts @@ -0,0 +1,41 @@ +import { z } from "zod"; + +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { + BaseCreateSecretRotationSchema, + BaseSecretRotationSchema, + BaseUpdateSecretRotationSchema +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas"; +import { + SqlCredentialsRotationParametersSchema, + SqlCredentialsRotationSecretsMappingSchema, + SqlCredentialsRotationTemplateSchema +} from "@app/ee/services/secret-rotation-v2/shared/sql-credentials"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +export const MsSqlCredentialsRotationSchema = BaseSecretRotationSchema(SecretRotation.MsSqlCredentials).extend({ + type: z.literal(SecretRotation.MsSqlCredentials), + parameters: SqlCredentialsRotationParametersSchema, + secretsMapping: SqlCredentialsRotationSecretsMappingSchema +}); + +export const CreateMsSqlCredentialsRotationSchema = BaseCreateSecretRotationSchema( + SecretRotation.MsSqlCredentials +).extend({ + parameters: SqlCredentialsRotationParametersSchema, + secretsMapping: SqlCredentialsRotationSecretsMappingSchema +}); + +export const UpdateMsSqlCredentialsRotationSchema = BaseUpdateSecretRotationSchema( + SecretRotation.MsSqlCredentials +).extend({ + parameters: SqlCredentialsRotationParametersSchema.optional(), + secretsMapping: SqlCredentialsRotationSecretsMappingSchema.optional() +}); + +export const MsSqlCredentialsRotationListItemSchema = z.object({ + name: z.literal("Microsoft SQL Server Credentials"), + connection: z.literal(AppConnection.MsSql), + type: z.literal(SecretRotation.MsSqlCredentials), + template: SqlCredentialsRotationTemplateSchema +}); diff --git a/backend/src/ee/services/secret-rotation-v2/mssql-credentials/mssql-credentials-rotation-types.ts b/backend/src/ee/services/secret-rotation-v2/mssql-credentials/mssql-credentials-rotation-types.ts new file mode 100644 index 000000000..ed707c4e4 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/mssql-credentials/mssql-credentials-rotation-types.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +import { TMsSqlConnection } from "@app/services/app-connection/mssql"; + +import { + CreateMsSqlCredentialsRotationSchema, + MsSqlCredentialsRotationListItemSchema, + MsSqlCredentialsRotationSchema +} from "./mssql-credentials-rotation-schemas"; + +export type TMsSqlCredentialsRotation = z.infer; + +export type TMsSqlCredentialsRotationInput = z.infer; + +export type TMsSqlCredentialsRotationListItem = z.infer; + +export type TMsSqlCredentialsRotationWithConnection = TMsSqlCredentialsRotation & { + connection: TMsSqlConnection; +}; diff --git a/backend/src/ee/services/secret-rotation-v2/postgres-credentials/index.ts b/backend/src/ee/services/secret-rotation-v2/postgres-credentials/index.ts new file mode 100644 index 000000000..aba568d1d --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/postgres-credentials/index.ts @@ -0,0 +1,3 @@ +export * from "./postgres-credentials-rotation-constants"; +export * from "./postgres-credentials-rotation-schemas"; +export * from "./postgres-credentials-rotation-types"; diff --git a/backend/src/ee/services/secret-rotation-v2/postgres-credentials/postgres-credentials-rotation-constants.ts b/backend/src/ee/services/secret-rotation-v2/postgres-credentials/postgres-credentials-rotation-constants.ts new file mode 100644 index 000000000..395ed46d1 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/postgres-credentials/postgres-credentials-rotation-constants.ts @@ -0,0 +1,23 @@ +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +export const POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = { + name: "PostgreSQL Credentials", + type: SecretRotation.PostgresCredentials, + connection: AppConnection.Postgres, + template: { + createUserStatement: `-- create user role +CREATE USER infisical_user WITH ENCRYPTED PASSWORD 'temporary_password'; + +-- grant database connection permissions +GRANT CONNECT ON DATABASE my_database TO infisical_user; + +-- grant relevant table permissions +GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO infisical_user;`, + secretsMapping: { + username: "POSTGRES_DB_USERNAME", + password: "POSTGRES_DB_PASSWORD" + } + } +}; diff --git a/backend/src/ee/services/secret-rotation-v2/postgres-credentials/postgres-credentials-rotation-schemas.ts b/backend/src/ee/services/secret-rotation-v2/postgres-credentials/postgres-credentials-rotation-schemas.ts new file mode 100644 index 000000000..0527a6116 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/postgres-credentials/postgres-credentials-rotation-schemas.ts @@ -0,0 +1,41 @@ +import { z } from "zod"; + +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { + BaseCreateSecretRotationSchema, + BaseSecretRotationSchema, + BaseUpdateSecretRotationSchema +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas"; +import { + SqlCredentialsRotationParametersSchema, + SqlCredentialsRotationSecretsMappingSchema, + SqlCredentialsRotationTemplateSchema +} from "@app/ee/services/secret-rotation-v2/shared/sql-credentials"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +export const PostgresCredentialsRotationSchema = BaseSecretRotationSchema(SecretRotation.PostgresCredentials).extend({ + type: z.literal(SecretRotation.PostgresCredentials), + parameters: SqlCredentialsRotationParametersSchema, + secretsMapping: SqlCredentialsRotationSecretsMappingSchema +}); + +export const CreatePostgresCredentialsRotationSchema = BaseCreateSecretRotationSchema( + SecretRotation.PostgresCredentials +).extend({ + parameters: SqlCredentialsRotationParametersSchema, + secretsMapping: SqlCredentialsRotationSecretsMappingSchema +}); + +export const UpdatePostgresCredentialsRotationSchema = BaseUpdateSecretRotationSchema( + SecretRotation.PostgresCredentials +).extend({ + parameters: SqlCredentialsRotationParametersSchema.optional(), + secretsMapping: SqlCredentialsRotationSecretsMappingSchema.optional() +}); + +export const PostgresCredentialsRotationListItemSchema = z.object({ + name: z.literal("PostgreSQL Credentials"), + connection: z.literal(AppConnection.Postgres), + type: z.literal(SecretRotation.PostgresCredentials), + template: SqlCredentialsRotationTemplateSchema +}); diff --git a/backend/src/ee/services/secret-rotation-v2/postgres-credentials/postgres-credentials-rotation-types.ts b/backend/src/ee/services/secret-rotation-v2/postgres-credentials/postgres-credentials-rotation-types.ts new file mode 100644 index 000000000..28e9fb29f --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/postgres-credentials/postgres-credentials-rotation-types.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +import { TPostgresConnection } from "@app/services/app-connection/postgres"; + +import { + CreatePostgresCredentialsRotationSchema, + PostgresCredentialsRotationListItemSchema, + PostgresCredentialsRotationSchema +} from "./postgres-credentials-rotation-schemas"; + +export type TPostgresCredentialsRotation = z.infer; + +export type TPostgresCredentialsRotationInput = z.infer; + +export type TPostgresCredentialsRotationListItem = z.infer; + +export type TPostgresCredentialsRotationWithConnection = TPostgresCredentialsRotation & { + connection: TPostgresConnection; +}; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-dal.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-dal.ts new file mode 100644 index 000000000..c3e7a12fb --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-dal.ts @@ -0,0 +1,467 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { TSecretRotationsV2 } from "@app/db/schemas/secret-rotations-v2"; +import { DatabaseError } from "@app/lib/errors"; +import { + buildFindFilter, + ormify, + prependTableNameToFindFilter, + selectAllTableCols, + sqlNestRelationships, + TFindOpt +} from "@app/lib/knex"; +import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; + +export type TSecretRotationV2DALFactory = ReturnType; + +type TSecretRotationFindFilter = Parameters>[0]; +type TSecretRotationFindOptions = TFindOpt; + +const baseSecretRotationV2Query = ({ + filter = {}, + options, + db, + tx +}: { + db: TDbClient; + filter?: { projectId?: string } & TSecretRotationFindFilter; + options?: TSecretRotationFindOptions; + tx?: Knex; +}) => { + const { projectId, ...filters } = filter; + + const query = (tx || db.replicaNode())(TableName.SecretRotationV2) + .join(TableName.SecretFolder, `${TableName.SecretRotationV2}.folderId`, `${TableName.SecretFolder}.id`) + .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) + .join(TableName.AppConnection, `${TableName.SecretRotationV2}.connectionId`, `${TableName.AppConnection}.id`) + .select(selectAllTableCols(TableName.SecretRotationV2)) + .select( + // environment + db.ref("name").withSchema(TableName.Environment).as("envName"), + db.ref("id").withSchema(TableName.Environment).as("envId"), + db.ref("slug").withSchema(TableName.Environment).as("envSlug"), + db.ref("projectId").withSchema(TableName.Environment), + // entire connection + db.ref("name").withSchema(TableName.AppConnection).as("connectionName"), + db.ref("method").withSchema(TableName.AppConnection).as("connectionMethod"), + db.ref("app").withSchema(TableName.AppConnection).as("connectionApp"), + db.ref("orgId").withSchema(TableName.AppConnection).as("connectionOrgId"), + db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("connectionEncryptedCredentials"), + db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"), + db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"), + db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"), + db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"), + db + .ref("isPlatformManagedCredentials") + .withSchema(TableName.AppConnection) + .as("connectionIsPlatformManagedCredentials") + ); + + if (filter) { + /* eslint-disable @typescript-eslint/no-misused-promises */ + void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.SecretRotationV2, filters))); + } + + if (projectId) { + void query.where(`${TableName.Environment}.projectId`, projectId); + } + + if (options) { + const { offset, limit, sort, count, countDistinct } = options; + if (countDistinct) { + void query.countDistinct(countDistinct); + } else if (count) { + void query.select(db.raw("COUNT(*) OVER() AS count")); + void query.select("*"); + } + if (limit) void query.limit(limit); + if (offset) void query.offset(offset); + if (sort) { + void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); + } + } + + return query; +}; + +const expandSecretRotation = >[number]>( + secretRotation: T, + folder: Awaited>[number] +) => { + const { + envId, + envName, + envSlug, + connectionApp, + connectionName, + connectionId, + connectionOrgId, + connectionEncryptedCredentials, + connectionMethod, + connectionDescription, + connectionCreatedAt, + connectionUpdatedAt, + connectionVersion, + connectionIsPlatformManagedCredentials, + ...el + } = secretRotation; + + return { + ...el, + connectionId, + environment: { id: envId, name: envName, slug: envSlug }, + connection: { + app: connectionApp, + id: connectionId, + name: connectionName, + orgId: connectionOrgId, + encryptedCredentials: connectionEncryptedCredentials, + method: connectionMethod, + description: connectionDescription, + createdAt: connectionCreatedAt, + updatedAt: connectionUpdatedAt, + version: connectionVersion, + isPlatformManagedCredentials: connectionIsPlatformManagedCredentials + }, + folder: { + id: folder!.id, + path: folder!.path + } + }; +}; + +export const secretRotationV2DALFactory = ( + db: TDbClient, + folderDAL: Pick +) => { + const secretRotationV2Orm = ormify(db, TableName.SecretRotationV2); + const secretRotationV2SecretMappingOrm = ormify(db, TableName.SecretRotationV2SecretMapping); + + const find = async ( + filter: Parameters<(typeof secretRotationV2Orm)["find"]>[0] & { projectId: string }, + options?: TSecretRotationFindOptions, + tx?: Knex + ) => { + try { + const secretRotations = await baseSecretRotationV2Query({ filter, db, tx, options }); + + if (!secretRotations.length) return []; + + const foldersWithPath = await folderDAL.findSecretPathByFolderIds( + filter.projectId, + secretRotations.map((rotation) => rotation.folderId), + tx + ); + + const folderRecord: Record = {}; + + foldersWithPath.forEach((folder) => { + if (folder) folderRecord[folder.id] = folder; + }); + + return secretRotations.map((rotation) => expandSecretRotation(rotation, folderRecord[rotation.folderId])); + } catch (error) { + throw new DatabaseError({ error, name: "Find - Secret Rotation V2" }); + } + }; + + const findWithMappedSecretsCount = async ( + { + search, + projectId, + ...filter + }: Parameters<(typeof secretRotationV2Orm)["find"]>[0] & { projectId: string; search?: string }, + tx?: Knex + ) => { + const query = (tx || db.replicaNode())(TableName.SecretRotationV2) + .join(TableName.SecretFolder, `${TableName.SecretRotationV2}.folderId`, `${TableName.SecretFolder}.id`) + .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) + .join( + TableName.SecretRotationV2SecretMapping, + `${TableName.SecretRotationV2SecretMapping}.rotationId`, + `${TableName.SecretRotationV2}.id` + ) + .join(TableName.SecretV2, `${TableName.SecretRotationV2SecretMapping}.secretId`, `${TableName.SecretV2}.id`) + .where(`${TableName.Environment}.projectId`, projectId) + .where(buildFindFilter(prependTableNameToFindFilter(TableName.SecretRotationV2, filter))) + .countDistinct(`${TableName.SecretRotationV2}.name`); + + if (search) { + void query.where((qb) => { + void qb + .whereILike(`${TableName.SecretV2}.key`, `%${search}%`) + .orWhereILike(`${TableName.SecretRotationV2}.name`, `%${search}%`); + }); + } + + const result = await query; + + // @ts-expect-error knex infers wrong type... + return Number(result[0]?.count ?? 0); + }; + + const findWithMappedSecrets = async ( + { search, ...filter }: Parameters<(typeof secretRotationV2Orm)["find"]>[0] & { projectId: string; search?: string }, + options?: TSecretRotationFindOptions, + tx?: Knex + ) => { + try { + const extendedQuery = baseSecretRotationV2Query({ filter, db, tx, options }) + .join( + TableName.SecretRotationV2SecretMapping, + `${TableName.SecretRotationV2SecretMapping}.rotationId`, + `${TableName.SecretRotationV2}.id` + ) + .join(TableName.SecretV2, `${TableName.SecretV2}.id`, `${TableName.SecretRotationV2SecretMapping}.secretId`) + .leftJoin( + TableName.SecretV2JnTag, + `${TableName.SecretV2}.id`, + `${TableName.SecretV2JnTag}.${TableName.SecretV2}Id` + ) + .leftJoin( + TableName.SecretTag, + `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, + `${TableName.SecretTag}.id` + ) + .leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`) + .select( + db.ref("id").withSchema(TableName.SecretV2).as("secretId"), + db.ref("key").withSchema(TableName.SecretV2).as("secretKey"), + db.ref("version").withSchema(TableName.SecretV2).as("secretVersion"), + db.ref("type").withSchema(TableName.SecretV2).as("secretType"), + db.ref("encryptedValue").withSchema(TableName.SecretV2).as("secretEncryptedValue"), + db.ref("encryptedComment").withSchema(TableName.SecretV2).as("secretEncryptedComment"), + db.ref("reminderNote").withSchema(TableName.SecretV2).as("secretReminderNote"), + db.ref("reminderRepeatDays").withSchema(TableName.SecretV2).as("secretReminderRepeatDays"), + db.ref("skipMultilineEncoding").withSchema(TableName.SecretV2).as("secretSkipMultilineEncoding"), + db.ref("metadata").withSchema(TableName.SecretV2).as("secretMetadata"), + db.ref("userId").withSchema(TableName.SecretV2).as("secretUserId"), + db.ref("folderId").withSchema(TableName.SecretV2).as("secretFolderId"), + db.ref("createdAt").withSchema(TableName.SecretV2).as("secretCreatedAt"), + db.ref("updatedAt").withSchema(TableName.SecretV2).as("secretUpdatedAt"), + db.ref("id").withSchema(TableName.SecretTag).as("tagId"), + db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), + db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), + db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue") + ); + + if (search) { + void extendedQuery.where((query) => { + void query + .whereILike(`${TableName.SecretV2}.key`, `%${search}%`) + .orWhereILike(`${TableName.SecretRotationV2}.name`, `%${search}%`); + }); + } + + const secretRotations = await extendedQuery; + + if (!secretRotations.length) return []; + + const foldersWithPath = await folderDAL.findSecretPathByFolderIds( + filter.projectId, + secretRotations.map((rotation) => rotation.folderId), + tx + ); + + const folderRecord: Record = {}; + + foldersWithPath.forEach((folder) => { + if (folder) folderRecord[folder.id] = folder; + }); + + return sqlNestRelationships({ + data: secretRotations, + key: "id", + parentMapper: (rotation) => expandSecretRotation(rotation, folderRecord[rotation.folderId]), + childrenMapper: [ + { + key: "secretId", + label: "secrets" as const, + mapper: ({ + secretId, + secretKey, + secretVersion, + secretType, + secretEncryptedValue, + secretEncryptedComment, + secretReminderNote, + secretReminderRepeatDays, + secretSkipMultilineEncoding, + secretMetadata, + secretUserId, + secretFolderId, + secretCreatedAt, + secretUpdatedAt, + id + }) => ({ + id: secretId, + key: secretKey, + version: secretVersion, + type: secretType, + encryptedValue: secretEncryptedValue, + encryptedComment: secretEncryptedComment, + reminderNote: secretReminderNote, + reminderRepeatDays: secretReminderRepeatDays, + skipMultilineEncoding: secretSkipMultilineEncoding, + metadata: secretMetadata, + userId: secretUserId, + folderId: secretFolderId, + createdAt: secretCreatedAt, + updatedAt: secretUpdatedAt, + rotationId: id, + isRotatedSecret: true + }), + childrenMapper: [ + { + key: "tagId", + label: "tags" as const, + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ + id, + color, + slug, + name: slug + }) + }, + { + key: "metadataId", + label: "secretMetadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) + } + ] + } + ] + }); + } catch (error) { + throw new DatabaseError({ error, name: "Find with Mapped Secrets - Secret Rotation V2" }); + } + }; + + const findById = async (id: string, tx?: Knex) => { + try { + const secretRotation = await baseSecretRotationV2Query({ + filter: { id }, + db, + tx + }).first(); + + if (secretRotation) { + const [folderWithPath] = await folderDAL.findSecretPathByFolderIds( + secretRotation.projectId, + [secretRotation.folderId], + tx + ); + return expandSecretRotation(secretRotation, folderWithPath); + } + } catch (error) { + throw new DatabaseError({ error, name: "Find by ID - Secret Rotation V2" }); + } + }; + + const create = async (data: Parameters<(typeof secretRotationV2Orm)["create"]>[0], tx?: Knex) => { + const rotation = await secretRotationV2Orm.create(data, tx); + + const secretRotation = (await baseSecretRotationV2Query({ + filter: { id: rotation.id }, + db, + tx + }).first())!; + + const [folderWithPath] = await folderDAL.findSecretPathByFolderIds( + secretRotation.projectId, + [secretRotation.folderId], + tx + ); + + return expandSecretRotation(secretRotation, folderWithPath); + }; + + const updateById = async ( + rotationId: string, + data: Parameters<(typeof secretRotationV2Orm)["updateById"]>[1], + tx?: Knex + ) => { + const rotation = await secretRotationV2Orm.updateById(rotationId, data, tx); + + const secretRotation = (await baseSecretRotationV2Query({ + filter: { id: rotation.id }, + db, + tx + }).first())!; + + const [folderWithPath] = await folderDAL.findSecretPathByFolderIds( + secretRotation.projectId, + [secretRotation.folderId], + tx + ); + + return expandSecretRotation(secretRotation, folderWithPath); + }; + + const deleteById = async (rotationId: string, tx?: Knex) => { + const secretRotation = (await baseSecretRotationV2Query({ + filter: { id: rotationId }, + db, + tx + }).first())!; + + await secretRotationV2Orm.deleteById(rotationId, tx); + + const [folderWithPath] = await folderDAL.findSecretPathByFolderIds( + secretRotation.projectId, + [secretRotation.folderId], + tx + ); + + return expandSecretRotation(secretRotation, folderWithPath); + }; + + const findOne = async (filter: Parameters<(typeof secretRotationV2Orm)["findOne"]>[0], tx?: Knex) => { + try { + const secretRotation = await baseSecretRotationV2Query({ filter, db, tx }).first(); + + if (secretRotation) { + const [folderWithPath] = await folderDAL.findSecretPathByFolderIds( + secretRotation.projectId, + [secretRotation.folderId], + tx + ); + + return expandSecretRotation(secretRotation, folderWithPath); + } + } catch (error) { + throw new DatabaseError({ error, name: "Find One - Secret Rotation V2" }); + } + }; + + const findSecretRotationsToQueue = async (rotateBy: Date, tx?: Knex) => { + const secretRotations = await (tx || db.replicaNode())(TableName.SecretRotationV2) + .where(`${TableName.SecretRotationV2}.isAutoRotationEnabled`, true) + .whereNotNull(`${TableName.SecretRotationV2}.nextRotationAt`) + .andWhereRaw(`"nextRotationAt" <= ?`, [rotateBy]) + .select(selectAllTableCols(TableName.SecretRotationV2)); + + return secretRotations; + }; + + return { + ...secretRotationV2Orm, + find, + create, + findById, + updateById, + deleteById, + findOne, + insertSecretMappings: secretRotationV2SecretMappingOrm.insertMany, + findWithMappedSecrets, + findWithMappedSecretsCount, + findSecretRotationsToQueue + }; +}; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts new file mode 100644 index 000000000..d43cacb3a --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts @@ -0,0 +1,10 @@ +export enum SecretRotation { + PostgresCredentials = "postgres-credentials", + MsSqlCredentials = "mssql-credentials", + Auth0ClientSecret = "auth0-client-secret" +} + +export enum SecretRotationStatus { + Success = "success", + Failed = "failed" +} diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts new file mode 100644 index 000000000..603b77cc1 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts @@ -0,0 +1,224 @@ +import { AxiosError } from "axios"; + +import { getConfig } from "@app/lib/config/env"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret"; +import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials"; +import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials"; +import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums"; +import { TSecretRotationV2ServiceFactoryDep } from "./secret-rotation-v2-service"; +import { + TSecretRotationV2, + TSecretRotationV2GeneratedCredentials, + TSecretRotationV2ListItem, + TSecretRotationV2Raw +} from "./secret-rotation-v2-types"; + +const SECRET_ROTATION_LIST_OPTIONS: Record = { + [SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION, + [SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION, + [SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION +}; + +export const listSecretRotationOptions = () => { + return Object.values(SECRET_ROTATION_LIST_OPTIONS).sort((a, b) => a.name.localeCompare(b.name)); +}; + +const getNextUTCDayInterval = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"] = { hours: 0, minutes: 0 }) => { + const now = new Date(); + + return new Date( + Date.UTC( + now.getUTCFullYear(), + now.getUTCMonth(), + now.getUTCDate() + 1, // Add 1 day to get tomorrow + hours, + minutes, + 0, + 0 + ) + ); +}; + +const getNextUTCMinuteInterval = ({ minutes }: TSecretRotationV2["rotateAtUtc"] = { hours: 0, minutes: 0 }) => { + const now = new Date(); + return new Date( + Date.UTC( + now.getUTCFullYear(), + now.getUTCMonth(), + now.getUTCDate(), + now.getUTCHours(), + now.getUTCMinutes() + 1, // Add 1 minute to get the next minute + minutes, // use minutes as seconds in dev + 0 + ) + ); +}; + +export const getNextUtcRotationInterval = (rotateAtUtc?: TSecretRotationV2["rotateAtUtc"]) => { + const appCfg = getConfig(); + + if (appCfg.isRotationDevelopmentMode) { + return getNextUTCMinuteInterval(rotateAtUtc); + } + + return getNextUTCDayInterval(rotateAtUtc); +}; + +export const encryptSecretRotationCredentials = async ({ + projectId, + generatedCredentials, + kmsService +}: { + projectId: string; + generatedCredentials: TSecretRotationV2GeneratedCredentials; + kmsService: TSecretRotationV2ServiceFactoryDep["kmsService"]; +}) => { + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + + const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({ + plainText: Buffer.from(JSON.stringify(generatedCredentials)) + }); + + return encryptedCredentialsBlob; +}; + +export const decryptSecretRotationCredentials = async ({ + projectId, + encryptedGeneratedCredentials, + kmsService +}: { + projectId: string; + encryptedGeneratedCredentials: Buffer; + kmsService: TSecretRotationV2ServiceFactoryDep["kmsService"]; +}) => { + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + + const decryptedPlainTextBlob = decryptor({ + cipherTextBlob: encryptedGeneratedCredentials + }); + + return JSON.parse(decryptedPlainTextBlob.toString()) as TSecretRotationV2GeneratedCredentials; +}; + +export const getSecretRotationRotateSecretJobOptions = ({ + id, + nextRotationAt +}: Pick) => { + const appCfg = getConfig(); + + return { + jobId: `secret-rotation-v2-rotate-${id}`, + retryLimit: appCfg.isRotationDevelopmentMode ? 3 : 5, + retryBackoff: true, + startAfter: nextRotationAt ?? undefined + }; +}; + +export const calculateNextRotationAt = ({ + rotateAtUtc, + isAutoRotationEnabled, + rotationInterval, + rotationStatus, + isManualRotation, + ...params +}: Pick< + TSecretRotationV2, + "isAutoRotationEnabled" | "lastRotatedAt" | "rotateAtUtc" | "rotationInterval" | "rotationStatus" +> & { isManualRotation: boolean }) => { + if (!isAutoRotationEnabled) return null; + + if (rotationStatus === SecretRotationStatus.Failed) { + return getNextUtcRotationInterval(rotateAtUtc); + } + + const lastRotatedAt = new Date(params.lastRotatedAt); + + const appCfg = getConfig(); + + if (appCfg.isRotationDevelopmentMode) { + // treat interval as minute + const nextRotation = new Date(lastRotatedAt.getTime() + rotationInterval * 60 * 1000); + + // in development mode we use rotateAtUtc.minutes as seconds + nextRotation.setUTCSeconds(rotateAtUtc.minutes); + nextRotation.setUTCMilliseconds(0); + + // If creation/manual rotation seconds are after the configured seconds we pad an additional minute + // to ensure a full interval has elapsed before rotation + if (isManualRotation && lastRotatedAt.getUTCSeconds() >= rotateAtUtc.minutes) { + nextRotation.setUTCMinutes(nextRotation.getUTCMinutes() + 1); + } + + return nextRotation; + } + + // production mode - rotationInterval = days + + const nextRotation = new Date(lastRotatedAt); + + nextRotation.setUTCHours(rotateAtUtc.hours); + nextRotation.setUTCMinutes(rotateAtUtc.minutes); + nextRotation.setUTCSeconds(0); + nextRotation.setUTCMilliseconds(0); + + // If creation/manual rotation was after the daily rotation time, + // we need pad an additional day to ensure full rotation interval + if ( + isManualRotation && + (lastRotatedAt.getUTCHours() > rotateAtUtc.hours || + (lastRotatedAt.getUTCHours() === rotateAtUtc.hours && lastRotatedAt.getUTCMinutes() >= rotateAtUtc.minutes)) + ) { + nextRotation.setUTCDate(nextRotation.getUTCDate() + rotationInterval + 1); + } else { + nextRotation.setUTCDate(nextRotation.getUTCDate() + rotationInterval); + } + + return nextRotation; +}; + +export const expandSecretRotation = async ( + { encryptedLastRotationMessage, ...secretRotation }: TSecretRotationV2Raw, + kmsService: TSecretRotationV2ServiceFactoryDep["kmsService"] +) => { + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: secretRotation.projectId + }); + + const lastRotationMessage = encryptedLastRotationMessage + ? decryptor({ + cipherTextBlob: encryptedLastRotationMessage + }).toString() + : null; + + return { + ...secretRotation, + lastRotationMessage + } as TSecretRotationV2; +}; + +const MAX_MESSAGE_LENGTH = 1024; + +export const parseRotationErrorMessage = (err: unknown): string => { + let errorMessage = `Infisical encountered an issue while generating credentials with the configured inputs: `; + + if (err instanceof AxiosError) { + errorMessage += err?.response?.data + ? JSON.stringify(err?.response?.data) + : err?.message ?? "An unknown error occurred."; + } else { + errorMessage += (err as Error)?.message || "An unknown error occurred."; + } + + return errorMessage.length <= MAX_MESSAGE_LENGTH + ? errorMessage + : `${errorMessage.substring(0, MAX_MESSAGE_LENGTH - 3)}...`; +}; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts new file mode 100644 index 000000000..1050c3419 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts @@ -0,0 +1,14 @@ +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +export const SECRET_ROTATION_NAME_MAP: Record = { + [SecretRotation.PostgresCredentials]: "PostgreSQL Credentials", + [SecretRotation.MsSqlCredentials]: "Microsoft SQL Sever Credentials", + [SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret" +}; + +export const SECRET_ROTATION_CONNECTION_MAP: Record = { + [SecretRotation.PostgresCredentials]: AppConnection.Postgres, + [SecretRotation.MsSqlCredentials]: AppConnection.MsSql, + [SecretRotation.Auth0ClientSecret]: AppConnection.Auth0 +}; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-queue.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-queue.ts new file mode 100644 index 000000000..f15cc4974 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-queue.ts @@ -0,0 +1,193 @@ +import { ProjectMembershipRole } from "@app/db/schemas"; +import { TSecretRotationV2DALFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-dal"; +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { + getNextUtcRotationInterval, + getSecretRotationRotateSecretJobOptions +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-fns"; +import { SECRET_ROTATION_NAME_MAP } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps"; +import { TSecretRotationV2ServiceFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-service"; +import { + TSecretRotationRotateSecretsJobPayload, + TSecretRotationSendNotificationJobPayload +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { getConfig } from "@app/lib/config/env"; +import { logger } from "@app/lib/logger"; +import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; +import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; + +type TSecretRotationV2QueueServiceFactoryDep = { + queueService: TQueueServiceFactory; + secretRotationV2DAL: Pick; + secretRotationV2Service: Pick; + smtpService: Pick; + projectMembershipDAL: Pick; + projectDAL: Pick; +}; + +export const secretRotationV2QueueServiceFactory = async ({ + queueService, + secretRotationV2DAL, + secretRotationV2Service, + projectMembershipDAL, + projectDAL, + smtpService +}: TSecretRotationV2QueueServiceFactoryDep) => { + const appCfg = getConfig(); + + if (appCfg.isRotationDevelopmentMode) { + logger.warn("Secret Rotation V2 is in development mode."); + } + + await queueService.startPg( + QueueJobs.SecretRotationV2QueueRotations, + async () => { + try { + const rotateBy = getNextUtcRotationInterval(); + + const currentTime = new Date(); + + const secretRotations = await secretRotationV2DAL.findSecretRotationsToQueue(rotateBy); + + logger.info( + `secretRotationV2Queue: Queue Rotations [currentTime=${currentTime.toISOString()}] [rotateBy=${rotateBy.toISOString()}] [count=${ + secretRotations.length + }]` + ); + + for await (const rotation of secretRotations) { + logger.info( + `secretRotationV2Queue: Queue Rotation [rotationId=${rotation.id}] [lastRotatedAt=${new Date( + rotation.lastRotatedAt + ).toISOString()}] [rotateAt=${new Date(rotation.nextRotationAt!).toISOString()}]` + ); + await queueService.queuePg( + QueueJobs.SecretRotationV2RotateSecrets, + { + rotationId: rotation.id, + queuedAt: currentTime + }, + getSecretRotationRotateSecretJobOptions(rotation) + ); + } + } catch (error) { + logger.error(error, "secretRotationV2Queue: Queue Rotations Error:"); + throw error; + } + }, + { + batchSize: 1, + workerCount: 1, + pollingIntervalSeconds: appCfg.isRotationDevelopmentMode ? 0.5 : 30 + } + ); + + await queueService.startPg( + QueueJobs.SecretRotationV2RotateSecrets, + async ([job]) => { + const { rotationId, queuedAt, isManualRotation } = job.data as TSecretRotationRotateSecretsJobPayload; + const { retryCount, retryLimit } = job; + + const logDetails = `[rotationId=${rotationId}] [jobId=${job.id}] retryCount=[${retryCount}/${retryLimit}]`; + + try { + const secretRotation = await secretRotationV2DAL.findById(rotationId); + + if (!secretRotation) throw new Error(`Secret rotation ${rotationId} not found`); + + if (!secretRotation.isAutoRotationEnabled) { + logger.info(`secretRotationV2Queue: Skipping Rotation - Auto-Rotation Disabled Since Queue ${logDetails}`); + } + + if (new Date(secretRotation.lastRotatedAt).getTime() >= new Date(queuedAt).getTime()) { + // rotated since being queued, skip rotation + logger.info(`secretRotationV2Queue: Skipping Rotation - Rotated Since Queue ${logDetails}`); + return; + } + + await secretRotationV2Service.rotateGeneratedCredentials(secretRotation, { + jobId: job.id, + shouldSendNotification: true, + isFinalAttempt: retryCount === retryLimit, + isManualRotation + }); + + logger.info(`secretRotationV2Queue: Secrets Rotated ${logDetails}`); + } catch (error) { + logger.error(error, `secretRotationV2Queue: Failed to Rotate Secrets ${logDetails}`); + throw error; + } + }, + { + batchSize: 1, + workerCount: 2, + pollingIntervalSeconds: 0.5 + } + ); + + await queueService.startPg( + QueueJobs.SecretRotationV2SendNotification, + async ([job]) => { + const { secretRotation } = job.data as TSecretRotationSendNotificationJobPayload; + try { + const { + name: rotationName, + type, + projectId, + lastRotationAttemptedAt, + folder, + environment, + id: rotationId + } = secretRotation; + + logger.info(`secretRotationV2Queue: Sending Status Notification [rotationId=${rotationId}]`); + + const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId); + const project = await projectDAL.findById(projectId); + + const projectAdmins = projectMembers.filter((member) => + member.roles.some((role) => role.role === ProjectMembershipRole.Admin) + ); + + const rotationType = SECRET_ROTATION_NAME_MAP[type as SecretRotation]; + + await smtpService.sendMail({ + recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean), + template: SmtpTemplates.SecretRotationFailed, + subjectLine: `Secret Rotation Failed`, + substitutions: { + rotationName, + rotationType, + content: `Your ${rotationType} Rotation failed to rotate during it's scheduled rotation. The last rotation attempt occurred at ${new Date( + lastRotationAttemptedAt + ).toISOString()}. Please check the rotation status in Infisical for more details.`, + secretPath: folder.path, + environment: environment.name, + projectName: project.name, + rotationUrl: encodeURI(`${appCfg.SITE_URL}/secret-manager/${projectId}/secrets/${environment.slug}`) + } + }); + } catch (error) { + logger.error( + error, + `secretRotationV2Queue: Failed to Send Status Notification [rotationId=${secretRotation.id}]` + ); + throw error; + } + }, + { + batchSize: 1, + workerCount: 2, + pollingIntervalSeconds: 1 + } + ); + + await queueService.schedulePg( + QueueJobs.SecretRotationV2QueueRotations, + appCfg.isRotationDevelopmentMode ? "* * * * *" : "0 0 * * *", + undefined, + { tz: "UTC" } + ); +}; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-schemas.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-schemas.ts new file mode 100644 index 000000000..b1be4ea22 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-schemas.ts @@ -0,0 +1,76 @@ +import { z } from "zod"; + +import { SecretRotationsV2Schema } from "@app/db/schemas/secret-rotations-v2"; +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { SECRET_ROTATION_CONNECTION_MAP } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps"; +import { SecretRotations } from "@app/lib/api-docs"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { slugSchema } from "@app/server/lib/schemas"; + +const RotateAtUtcSchema = z.object({ + hours: z.number().min(0).max(23), + minutes: z.number().min(0).max(59) +}); + +export const BaseSecretRotationSchema = (type: SecretRotation) => + SecretRotationsV2Schema.omit({ + encryptedGeneratedCredentials: true, + encryptedLastRotationMessage: true, + rotateAtUtc: true, + // unique to provider + type: true, + parameters: true, + secretMappings: true + }).extend({ + connection: z.object({ + app: z.literal(SECRET_ROTATION_CONNECTION_MAP[type]), + name: z.string(), + id: z.string().uuid() + }), + environment: z.object({ slug: z.string(), name: z.string(), id: z.string().uuid() }), + projectId: z.string(), + folder: z.object({ id: z.string(), path: z.string() }), + rotateAtUtc: RotateAtUtcSchema, + lastRotationMessage: z.string().nullish() + }); + +export const BaseCreateSecretRotationSchema = (type: SecretRotation) => + z.object({ + name: slugSchema({ field: "name" }).describe(SecretRotations.CREATE(type).name), + projectId: z.string().trim().min(1, "Project ID required").describe(SecretRotations.CREATE(type).projectId), + description: z + .string() + .trim() + .max(256, "Description cannot exceed 256 characters") + .nullish() + .describe(SecretRotations.CREATE(type).description), + connectionId: z.string().uuid().describe(SecretRotations.CREATE(type).connectionId), + environment: slugSchema({ field: "environment", max: 64 }).describe(SecretRotations.CREATE(type).environment), + secretPath: z + .string() + .trim() + .min(1, "Secret path required") + .transform(removeTrailingSlash) + .describe(SecretRotations.CREATE(type).secretPath), + isAutoRotationEnabled: z + .boolean() + .optional() + .default(true) + .describe(SecretRotations.CREATE(type).isAutoRotationEnabled), + rotationInterval: z.coerce.number().min(1).describe(SecretRotations.CREATE(type).rotationInterval), + rotateAtUtc: RotateAtUtcSchema.optional().describe(SecretRotations.CREATE(type).rotateAtUtc) + }); + +export const BaseUpdateSecretRotationSchema = (type: SecretRotation) => + z.object({ + name: slugSchema({ field: "name" }).describe(SecretRotations.UPDATE(type).name).optional(), + description: z + .string() + .trim() + .max(256, "Description cannot exceed 256 characters") + .nullish() + .describe(SecretRotations.UPDATE(type).description), + isAutoRotationEnabled: z.boolean().optional().describe(SecretRotations.UPDATE(type).isAutoRotationEnabled), + rotationInterval: z.coerce.number().min(1).optional().describe(SecretRotations.UPDATE(type).rotationInterval), + rotateAtUtc: RotateAtUtcSchema.optional().describe(SecretRotations.UPDATE(type).rotateAtUtc) + }); diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts new file mode 100644 index 000000000..a828acb32 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts @@ -0,0 +1,1317 @@ +import { ForbiddenError, subject } from "@casl/ability"; +import { Knex } from "knex"; +import isEqual from "lodash.isequal"; + +import { ActionProjectType, SecretType, TableName } from "@app/db/schemas"; +import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { hasSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { + ProjectPermissionSecretActions, + ProjectPermissionSecretRotationActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; +import { auth0ClientSecretRotationFactory } from "@app/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-fns"; +import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { + calculateNextRotationAt, + decryptSecretRotationCredentials, + encryptSecretRotationCredentials, + expandSecretRotation, + getNextUtcRotationInterval, + getSecretRotationRotateSecretJobOptions, + listSecretRotationOptions, + parseRotationErrorMessage +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-fns"; +import { + SECRET_ROTATION_CONNECTION_MAP, + SECRET_ROTATION_NAME_MAP +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps"; +import { + TCreateSecretRotationV2DTO, + TDeleteSecretRotationV2DTO, + TFindSecretRotationV2ByIdDTO, + TFindSecretRotationV2ByNameDTO, + TGetDashboardSecretRotationsV2, + TGetDashboardSecretRotationV2Count, + TListSecretRotationsV2ByProjectId, + TQuickSearchSecretRotationsV2, + TRotateSecretRotationV2, + TRotationFactory, + TSecretRotationRotateGeneratedCredentials, + TSecretRotationV2, + TSecretRotationV2GeneratedCredentials, + TSecretRotationV2Raw, + TSecretRotationV2WithConnection, + TUpdateSecretRotationV2DTO +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { sqlCredentialsRotationFactory } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials"; +import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; +import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; +import { getConfig } from "@app/lib/config/env"; +import { DatabaseErrorCode } from "@app/lib/error-codes"; +import { BadRequestError, DatabaseError, InternalServerError, NotFoundError } from "@app/lib/errors"; +import { OrderByDirection, OrgServiceActor } from "@app/lib/types"; +import { QueueJobs, TQueueServiceFactory } from "@app/queue"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns"; +import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; +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 { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; +import { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal"; +import { TSecretQueueFactory } from "@app/services/secret/secret-queue"; +import { SecretsOrderBy } from "@app/services/secret/secret-types"; +import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; +import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; +import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal"; +import { + fnSecretBulkDelete, + fnSecretBulkInsert, + fnSecretBulkUpdate, + reshapeBridgeSecret +} from "@app/services/secret-v2-bridge/secret-v2-bridge-fns"; +import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal"; +import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; + +import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal"; + +export type TSecretRotationV2ServiceFactoryDep = { + secretRotationV2DAL: TSecretRotationV2DALFactory; + appConnectionService: Pick; + permissionService: Pick; + projectBotService: Pick; + kmsService: Pick; + licenseService: Pick; + auditLogService: Pick; + keyStore: Pick; + folderDAL: Pick; + secretV2BridgeDAL: Pick< + TSecretV2BridgeDALFactory, + "bulkUpdate" | "insertMany" | "deleteMany" | "upsertSecretReferences" | "find" | "invalidateSecretCacheByProjectId" + >; + secretVersionV2BridgeDAL: Pick; + secretVersionTagV2BridgeDAL: Pick; + resourceMetadataDAL: Pick; + secretTagDAL: Pick; + secretQueueService: Pick; + snapshotService: Pick; + queueService: Pick; + appConnectionDAL: Pick; +}; + +export type TSecretRotationV2ServiceFactory = ReturnType; + +const MAX_GENERATED_CREDENTIALS_LENGTH = 2; + +type TRotationFactoryImplementation = TRotationFactory< + TSecretRotationV2WithConnection, + TSecretRotationV2GeneratedCredentials +>; +const SECRET_ROTATION_FACTORY_MAP: Record = { + [SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation, + [SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation, + [SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation +}; + +export const secretRotationV2ServiceFactory = ({ + secretRotationV2DAL, + folderDAL, + secretV2BridgeDAL, + secretVersionV2BridgeDAL, + secretVersionTagV2BridgeDAL, + secretTagDAL, + resourceMetadataDAL, + permissionService, + appConnectionService, + projectBotService, + licenseService, + kmsService, + auditLogService, + secretQueueService, + snapshotService, + keyStore, + queueService, + appConnectionDAL +}: TSecretRotationV2ServiceFactoryDep) => { + const $queueSendSecretRotationStatusNotification = async (secretRotation: TSecretRotationV2Raw) => { + const appCfg = getConfig(); + if (!appCfg.isSmtpConfigured) return; // comment out if testing email sending + + await queueService.queuePg( + QueueJobs.SecretRotationV2SendNotification, + { secretRotation }, + { + jobId: `secret-rotation-v2-notification-${secretRotation.id}`, + retryLimit: 5, + retryBackoff: true + } + ); + }; + + const $throwOnConflictingSecrets = async ({ + secretKeys, + folderId, + tx, + secretPath + }: { + secretKeys: string[]; + folderId: string; + tx: Knex; + secretPath: string; + }) => { + if (new Set(secretKeys).size !== secretKeys.length) { + throw new BadRequestError({ + message: `Secrets mapping keys must be unique. "${secretKeys.join(", ")}" contains duplicate keys.` + }); + } + + const conflictingSecrets = await secretV2BridgeDAL.find( + { + $in: { + [`${TableName.SecretV2}.key` as "key"]: secretKeys + }, + [`${TableName.SecretV2}.folderId` as "folderId"]: folderId, + [`${TableName.SecretV2}.type` as "type"]: SecretType.Shared + }, + { tx } + ); + + if (conflictingSecrets.length) { + throw new BadRequestError({ + message: `The following secrets already exist at the path "${secretPath}": ${conflictingSecrets + .map(({ key }) => key) + .join(", ")}` + }); + } + }; + + const listSecretRotationsByProjectId = async ( + { projectId, type }: TListSecretRotationsV2ByProjectId, + actor: OrgServiceActor + ) => { + const plan = await licenseService.getPlan(actor.orgId); + + if (!plan.secretRotation) + throw new BadRequestError({ + message: "Failed to access secret rotations due to plan restriction. Upgrade plan to access secret rotations." + }); + + 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( + ProjectPermissionSecretRotationActions.Read, + ProjectPermissionSub.SecretRotation + ); + + const secretRotations = await secretRotationV2DAL.find({ + ...(type && { type }), + projectId + }); + + return Promise.all( + secretRotations + .filter((rotation) => + permission.can( + ProjectPermissionSecretRotationActions.Read, + subject(ProjectPermissionSub.SecretRotation, { + environment: rotation.environment.slug, + secretPath: rotation.folder.path + }) + ) + ) + .map((rotation) => expandSecretRotation(rotation, kmsService)) + ); + }; + + const findSecretRotationById = async ({ type, rotationId }: TFindSecretRotationV2ByIdDTO, actor: OrgServiceActor) => { + const plan = await licenseService.getPlan(actor.orgId); + + if (!plan.secretRotation) + throw new BadRequestError({ + message: "Failed to access secret rotation due to plan restriction. Upgrade plan to access secret rotations." + }); + + const secretRotation = await secretRotationV2DAL.findById(rotationId); + + if (!secretRotation) + throw new NotFoundError({ + message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID "${rotationId}"` + }); + + const { projectId, environment, folder, connection } = secretRotation; + + 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( + ProjectPermissionSecretRotationActions.Read, + subject(ProjectPermissionSub.SecretRotation, { + environment: environment.slug, + secretPath: folder.path + }) + ); + + if (connection.app !== SECRET_ROTATION_CONNECTION_MAP[type]) + throw new BadRequestError({ + message: `Secret Rotation with ID "${rotationId}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}` + }); + + return expandSecretRotation(secretRotation, kmsService); + }; + + const findSecretRotationGeneratedCredentialsById = async ( + { type, rotationId }: TFindSecretRotationV2ByIdDTO, + actor: OrgServiceActor + ) => { + const plan = await licenseService.getPlan(actor.orgId); + + if (!plan.secretRotation) + throw new BadRequestError({ + message: + "Failed to access secret rotation credentials due to plan restriction. Upgrade plan to access secret rotations credentials." + }); + + const secretRotation = await secretRotationV2DAL.findById(rotationId); + + if (!secretRotation) + throw new NotFoundError({ + message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID "${rotationId}"` + }); + + const { projectId, environment, folder, connection, encryptedGeneratedCredentials } = secretRotation; + + 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( + ProjectPermissionSecretRotationActions.ReadGeneratedCredentials, + subject(ProjectPermissionSub.SecretRotation, { + environment: environment.slug, + secretPath: folder.path + }) + ); + + if (connection.app !== SECRET_ROTATION_CONNECTION_MAP[type]) + throw new BadRequestError({ + message: `Secret Rotation with ID "${rotationId}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}` + }); + + const generatedCredentials = await decryptSecretRotationCredentials({ + projectId, + encryptedGeneratedCredentials, + kmsService + }); + + return { + generatedCredentials, + secretRotation: secretRotation as TSecretRotationV2 + }; + }; + + const findSecretRotationByName = async ( + { type, rotationName, secretPath, environment, projectId }: TFindSecretRotationV2ByNameDTO, + actor: OrgServiceActor + ) => { + const plan = await licenseService.getPlan(actor.orgId); + + if (!plan.secretRotation) + throw new BadRequestError({ + message: "Failed to access secret rotation due to plan restriction. Upgrade plan to access secret rotations." + }); + + const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); + + if (!folder) + throw new BadRequestError({ + message: `Could not find folder with path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"` + }); + + // we prevent conflicting names within a folder + const secretRotation = await secretRotationV2DAL.findOne({ + name: rotationName, + folderId: folder.id + }); + + if (!secretRotation) + throw new NotFoundError({ + message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with name "${rotationName}"` + }); + + const { connection, id: rotationId } = secretRotation; + + 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( + ProjectPermissionSecretRotationActions.Read, + subject(ProjectPermissionSub.SecretRotation, { + environment, + secretPath + }) + ); + + if (connection.app !== SECRET_ROTATION_CONNECTION_MAP[type]) + throw new BadRequestError({ + message: `Secret Rotation with ID "${rotationId}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}` + }); + + return expandSecretRotation(secretRotation, kmsService); + }; + + const createSecretRotation = async ( + { + projectId, + secretPath, + environment, + rotateAtUtc = { hours: 0, minutes: 0 }, + secretsMapping, + ...payload + }: TCreateSecretRotationV2DTO, + actor: OrgServiceActor + ) => { + const plan = await licenseService.getPlan(actor.orgId); + + if (!plan.secretRotation) + throw new BadRequestError({ + message: "Failed to create secret rotation due to plan restriction. Upgrade plan to create secret rotations." + }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager, + projectId + }); + + const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); + + if (!shouldUseSecretV2Bridge) + throw new BadRequestError({ + message: + "Project version does not support Secret Rotation V2. Please upgrade your project via the Infiscal Dashboard to gain access." + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretRotationActions.Create, + subject(ProjectPermissionSub.SecretRotation, { environment, secretPath }) + ); + + const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); + + if (!folder) + throw new BadRequestError({ + message: `Could not find folder with path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"` + }); + + const typeApp = SECRET_ROTATION_CONNECTION_MAP[payload.type]; + + // validates permission to connect and app is valid for rotation type + const connection = await appConnectionService.connectAppConnectionById(typeApp, payload.connectionId, actor); + + const rotationFactory = SECRET_ROTATION_FACTORY_MAP[payload.type]( + { + parameters: payload.parameters, + secretsMapping, + connection + } as TSecretRotationV2WithConnection, + appConnectionDAL, + kmsService + ); + + try { + const currentTime = new Date(); + + // callback structure to support transactional rollback when possible + const secretRotation = await rotationFactory.issueCredentials(async (newCredentials) => { + const encryptedGeneratedCredentials = await encryptSecretRotationCredentials({ + generatedCredentials: [newCredentials] as TSecretRotationV2GeneratedCredentials, + projectId, + kmsService + }); + + return secretRotationV2DAL.transaction(async (tx) => { + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.SecretRotationV2Creation(folder.id)]); + + await $throwOnConflictingSecrets({ + secretPath, + secretKeys: Object.values(secretsMapping), + tx, + folderId: folder.id + }); + + const createdRotation = await secretRotationV2DAL.create( + { + folderId: folder.id, + secretsMapping, + ...payload, + encryptedGeneratedCredentials, + rotateAtUtc, + rotationStatus: SecretRotationStatus.Success, + lastRotationAttemptedAt: currentTime, + lastRotatedAt: currentTime, + nextRotationAt: calculateNextRotationAt({ + lastRotatedAt: currentTime, + isAutoRotationEnabled: Boolean(payload.isAutoRotationEnabled), + rotateAtUtc, + rotationInterval: payload.rotationInterval, + rotationStatus: SecretRotationStatus.Success, + isManualRotation: true + }) + }, + tx + ); + + const secretsPayload = rotationFactory.getSecretsPayload(newCredentials); + + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + + const mappedSecrets = await fnSecretBulkInsert({ + folderId: folder.id, + orgId: connection.orgId, + tx, + inputSecrets: secretsPayload.map(({ key, value }) => ({ + key, + encryptedValue: encryptor({ + plainText: Buffer.from(value) + }).cipherTextBlob, + references: [] + })), + secretDAL: secretV2BridgeDAL, + secretVersionDAL: secretVersionV2BridgeDAL, + secretVersionTagDAL: secretVersionTagV2BridgeDAL, + secretTagDAL, + resourceMetadataDAL + }); + + await secretRotationV2DAL.insertSecretMappings( + mappedSecrets.map((secret) => ({ + secretId: secret.id, + rotationId: createdRotation.id + })), + tx + ); + + return createdRotation; + }); + }); + + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId); + await snapshotService.performSnapshot(folder.id); + await secretQueueService.syncSecrets({ + orgId: connection.orgId, + secretPath, + projectId, + environmentSlug: environment, + excludeReplication: true + }); + + return await expandSecretRotation(secretRotation, kmsService); + } catch (err) { + if (err instanceof DatabaseError) { + const error = err.error as { code: string; message: string; table: string }; + + if (error.code === DatabaseErrorCode.UniqueViolation) { + switch (error.table) { + case TableName.SecretRotationV2: + throw new BadRequestError({ + message: `A Secret Rotation with the name "${payload.name}" already exists at the secret path "${secretPath}"` + }); + default: + throw err; + } + } + + throw err; + } + + if (err instanceof BadRequestError) throw err; + + throw new BadRequestError({ + message: parseRotationErrorMessage(err) + }); + } + }; + + const updateSecretRotation = async ( + { type, rotationId, ...payload }: TUpdateSecretRotationV2DTO, + actor: OrgServiceActor + ) => { + const plan = await licenseService.getPlan(actor.orgId); + + if (!plan.secretRotation) + throw new BadRequestError({ + message: "Failed to update secret rotation due to plan restriction. Upgrade plan to update secret rotations." + }); + + const secretRotation = await secretRotationV2DAL.findById(rotationId); + + if (!secretRotation) + throw new NotFoundError({ + message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID ${rotationId}` + }); + + const { folder, environment, projectId, folderId, connection } = secretRotation; + const secretsMapping = secretRotation.secretsMapping as TSecretRotationV2["secretsMapping"]; + + 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( + ProjectPermissionSecretRotationActions.Edit, + subject(ProjectPermissionSub.SecretRotation, { + environment: environment.slug, + secretPath: folder.path + }) + ); + + if (connection.app !== SECRET_ROTATION_CONNECTION_MAP[type]) + throw new BadRequestError({ + message: `Secret Rotation with ID "${rotationId}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}` + }); + + const nextRotationAt = calculateNextRotationAt({ + ...(secretRotation as TSecretRotationV2), + ...payload, + isManualRotation: secretRotation.isLastRotationManual + }); + + let secretsMappingUpdated = false; + + try { + const updatedSecretRotation = await secretRotationV2DAL.transaction(async (tx) => { + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.SecretRotationV2Creation(folder.id)]); + + if (payload.secretsMapping && !isEqual(payload.secretsMapping, secretsMapping)) { + const currentMappingKeys = Object.values(secretsMapping); + await $throwOnConflictingSecrets({ + secretPath: folder.path, + secretKeys: Object.values(payload.secretsMapping).filter((key) => !currentMappingKeys.includes(key)), + tx, + folderId: folder.id + }); + + // update mapped secrets names + await fnSecretBulkUpdate({ + folderId, + orgId: connection.orgId, + tx, + inputSecrets: Object.entries(secretsMapping).map(([mappingKey, secretKey]) => ({ + filter: { + key: secretKey, + folderId, + type: SecretType.Shared + }, + data: { + key: payload.secretsMapping![mappingKey as keyof TSecretRotationV2["secretsMapping"]] + } + })), + secretDAL: secretV2BridgeDAL, + secretVersionDAL: secretVersionV2BridgeDAL, + secretVersionTagDAL: secretVersionTagV2BridgeDAL, + secretTagDAL, + resourceMetadataDAL + }); + + secretsMappingUpdated = true; + } + + return secretRotationV2DAL.updateById( + rotationId, + { + ...payload, + nextRotationAt + }, + tx + ); + }); + + if (secretsMappingUpdated) { + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId); + await snapshotService.performSnapshot(folder.id); + await secretQueueService.syncSecrets({ + orgId: connection.orgId, + secretPath: folder.path, + projectId, + environmentSlug: environment.slug, + excludeReplication: true + }); + } + + // queue for rotation if adjusted time falls before next cron + if (nextRotationAt && nextRotationAt.getTime() < getNextUtcRotationInterval().getTime()) { + await queueService.queuePg( + QueueJobs.SecretRotationV2RotateSecrets, + { rotationId, queuedAt: new Date(), isManualRotation: true }, + getSecretRotationRotateSecretJobOptions(updatedSecretRotation) + ); + } + + return await expandSecretRotation(updatedSecretRotation, kmsService); + } catch (err) { + if (err instanceof DatabaseError) { + const error = err.error as { code: string; message: string; table: string }; + + if (error.code === DatabaseErrorCode.UniqueViolation) { + switch (error.table) { + case TableName.SecretRotationV2: + if (payload.name) + throw new BadRequestError({ + message: `A Secret Rotation with the name "${payload.name}" already exists at the secret path "${folder.path}"` + }); + break; + default: + throw err; + } + } + } + + if (err instanceof BadRequestError) throw err; + + throw err; + } + }; + + const deleteSecretRotation = async ( + { type, rotationId, deleteSecrets, revokeGeneratedCredentials }: TDeleteSecretRotationV2DTO, + actor: OrgServiceActor + ) => { + const plan = await licenseService.getPlan(actor.orgId); + + if (!plan.secretRotation) + throw new BadRequestError({ + message: "Failed to delete secret rotation due to plan restriction. Upgrade plan to delete secret rotation." + }); + + const secretRotation = await secretRotationV2DAL.findById(rotationId); + + if (!secretRotation) + throw new NotFoundError({ + message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID "${rotationId}"` + }); + + const { folder, environment, projectId, encryptedGeneratedCredentials, connection, folderId, secretsMapping } = + secretRotation; + + 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( + ProjectPermissionSecretRotationActions.Delete, + subject(ProjectPermissionSub.SecretRotation, { + environment: environment.slug, + secretPath: folder.path + }) + ); + + if (connection.app !== SECRET_ROTATION_CONNECTION_MAP[type]) + throw new BadRequestError({ + message: `Secret Rotation with ID "${rotationId}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}` + }); + + const deleteTransaction = async () => + secretRotationV2DAL.transaction(async (tx) => { + if (deleteSecrets) { + await fnSecretBulkDelete({ + secretDAL: secretV2BridgeDAL, + secretQueueService, + inputSecrets: Object.values(secretsMapping as TSecretRotationV2["secretsMapping"]).map((secretKey) => ({ + secretKey, + type: SecretType.Shared + })), + projectId, + folderId, + actorId: actor.id, // not actually used since rotated secrets are shared + tx + }); + } + + return secretRotationV2DAL.deleteById(rotationId, tx); + }); + + if (revokeGeneratedCredentials) { + const appConnection = await decryptAppConnection(connection, kmsService); + + const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type]( + { + ...secretRotation, + connection: appConnection + } as TSecretRotationV2WithConnection, + appConnectionDAL, + kmsService + ); + + const generatedCredentials = await decryptSecretRotationCredentials({ + encryptedGeneratedCredentials, + projectId, + kmsService + }); + + await rotationFactory.revokeCredentials(generatedCredentials, deleteTransaction); + } else { + await deleteTransaction(); + } + + if (deleteSecrets) { + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId); + await snapshotService.performSnapshot(folder.id); + await secretQueueService.syncSecrets({ + orgId: connection.orgId, + secretPath: folder.path, + projectId, + environmentSlug: environment.slug, + excludeReplication: true + }); + } + + return expandSecretRotation(secretRotation, kmsService); + }; + + const rotateGeneratedCredentials = async ( + secretRotation: TSecretRotationV2Raw, + { + auditLogInfo, + jobId, + shouldSendNotification, + isFinalAttempt = true, + isManualRotation = false + }: TSecretRotationRotateGeneratedCredentials = {} + ) => { + const { + connection, + folder, + environment, + encryptedGeneratedCredentials, + activeIndex, + projectId, + type, + folderId, + id: rotationId, + parameters, + secretsMapping + } = secretRotation; + + let lock: Awaited> | undefined; + + try { + try { + lock = await keyStore.acquireLock([KeyStorePrefixes.SecretRotationLock(rotationId)], 60 * 1000); + } catch (e) { + throw new InternalServerError({ + message: "Failed to acquire rotation lock." + }); + } + + const appConnection = await decryptAppConnection(connection, kmsService); + + const generatedCredentials = await decryptSecretRotationCredentials({ + projectId, + encryptedGeneratedCredentials, + kmsService + }); + + const inactiveIndex = (activeIndex + 1) % MAX_GENERATED_CREDENTIALS_LENGTH; + + const inactiveCredentials = generatedCredentials[inactiveIndex]; + + const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type as SecretRotation]( + { + ...secretRotation, + connection: appConnection + } as TSecretRotationV2WithConnection, + appConnectionDAL, + kmsService + ); + + const updatedRotation = await rotationFactory.rotateCredentials(inactiveCredentials, async (newCredentials) => { + const updatedCredentials = [...generatedCredentials]; + updatedCredentials[inactiveIndex] = newCredentials; + + const encryptedUpdatedCredentials = await encryptSecretRotationCredentials({ + projectId, + generatedCredentials: updatedCredentials as TSecretRotationV2GeneratedCredentials, + kmsService + }); + + return secretRotationV2DAL.transaction(async (tx) => { + const secretsPayload = rotationFactory.getSecretsPayload(newCredentials); + + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + + // update mapped secrets with new credential values + await fnSecretBulkUpdate({ + folderId, + orgId: connection.orgId, + tx, + inputSecrets: secretsPayload.map(({ key, value }) => ({ + filter: { + key, + folderId, + type: SecretType.Shared + }, + data: { + encryptedValue: encryptor({ + plainText: Buffer.from(value) + }).cipherTextBlob, + references: [] + } + })), + secretDAL: secretV2BridgeDAL, + secretVersionDAL: secretVersionV2BridgeDAL, + secretVersionTagDAL: secretVersionTagV2BridgeDAL, + secretTagDAL, + resourceMetadataDAL + }); + + const currentTime = new Date(); + + return secretRotationV2DAL.updateById( + secretRotation.id, + { + encryptedGeneratedCredentials: encryptedUpdatedCredentials, + activeIndex: inactiveIndex, + isLastRotationManual: isManualRotation, + lastRotatedAt: currentTime, + lastRotationAttemptedAt: currentTime, + nextRotationAt: calculateNextRotationAt({ + ...(secretRotation as TSecretRotationV2), + rotationStatus: SecretRotationStatus.Success, + lastRotatedAt: currentTime, + isManualRotation + }), + rotationStatus: SecretRotationStatus.Success, + lastRotationJobId: jobId, + encryptedLastRotationMessage: null + }, + tx + ); + }); + }); + + await auditLogService.createAuditLog({ + ...(auditLogInfo ?? { + actor: { + type: ActorType.PLATFORM, + metadata: {} + } + }), + projectId, + event: { + type: EventType.SECRET_ROTATION_ROTATE_SECRETS, + metadata: { + type, + rotationId, + connectionId: connection.id, + folderId, + parameters, + secretsMapping, + status: SecretRotationStatus.Success, + occurredAt: new Date(), + message: null, + jobId + } + } + }); + + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId); + await snapshotService.performSnapshot(folder.id); + await secretQueueService.syncSecrets({ + orgId: connection.orgId, + secretPath: folder.path, + projectId, + environmentSlug: environment.slug, + excludeReplication: true + }); + + return updatedRotation; + } catch (error) { + const errorMessage = parseRotationErrorMessage(error); + + if (isFinalAttempt) { + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + + const { cipherTextBlob: encryptedMessage } = encryptor({ + plainText: Buffer.from(errorMessage) + }); + + const updatedRotation = await secretRotationV2DAL.updateById(secretRotation.id, { + rotationStatus: SecretRotationStatus.Failed, + lastRotationJobId: jobId, + lastRotationAttemptedAt: new Date(), + encryptedLastRotationMessage: encryptedMessage, + nextRotationAt: getNextUtcRotationInterval(secretRotation.rotateAtUtc as TSecretRotationV2["rotateAtUtc"]) + }); + + if (shouldSendNotification) { + await $queueSendSecretRotationStatusNotification(updatedRotation); + } + } + + await auditLogService.createAuditLog({ + ...(auditLogInfo ?? { + actor: { + type: ActorType.PLATFORM, + metadata: {} + } + }), + projectId, + event: { + type: EventType.SECRET_ROTATION_ROTATE_SECRETS, + metadata: { + type, + rotationId, + connectionId: connection.id, + folderId, + parameters, + secretsMapping, + occurredAt: new Date(), + status: SecretRotationStatus.Failed, + message: isFinalAttempt ? "See Rotation status for details" : "Rotation will be re-attempted shortly...", + jobId + } + } + }); + + throw new BadRequestError({ message: errorMessage }); + } finally { + await lock?.release(); + } + }; + + const rotateSecretRotation = async ( + { rotationId, type, auditLogInfo }: TRotateSecretRotationV2, + actor: OrgServiceActor + ) => { + const plan = await licenseService.getPlan(actor.orgId); + + if (!plan.secretRotation) + throw new BadRequestError({ + message: + "Failed to rotate secret rotation secrets due to plan restriction. Upgrade plan to rotate secret rotation secrets." + }); + + const secretRotation = await secretRotationV2DAL.findById(rotationId); + + if (!secretRotation) + throw new NotFoundError({ + message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID "${rotationId}"` + }); + + const { projectId, environment, folder, connection } = secretRotation; + + 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( + ProjectPermissionSecretRotationActions.RotateSecrets, + subject(ProjectPermissionSub.SecretRotation, { + environment: environment.slug, + secretPath: folder.path + }) + ); + + if (connection.app !== SECRET_ROTATION_CONNECTION_MAP[type]) + throw new BadRequestError({ + message: `Secret Rotation with ID "${rotationId}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}` + }); + + const isRotationOccurring = Boolean(await keyStore.getItem(KeyStorePrefixes.SecretRotationLock(secretRotation.id))); + + if (isRotationOccurring) + throw new BadRequestError({ message: `A rotation is already in progress. Please try again shortly.` }); + + try { + const updatedRotation = await rotateGeneratedCredentials(secretRotation, { + auditLogInfo, + isManualRotation: true + }); + + return await expandSecretRotation(updatedRotation, kmsService); + } catch (err) { + throw new InternalServerError({ + message: (err as Error).message ?? "Failed to rotate secrets: check Rotation status for details." + }); + } + }; + + const getDashboardSecretRotationCount = async ( + { projectId, environments, secretPath, search }: TGetDashboardSecretRotationV2Count, + actor: OrgServiceActor + ) => { + // we don't check plan for dashboard like dynamic secret, actions will be prevented + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager, + projectId + }); + + const permissiveEnvironments = environments.filter((environment) => + permission.can( + ProjectPermissionSecretRotationActions.Read, + subject(ProjectPermissionSub.SecretRotation, { environment, secretPath }) + ) + ); + + if (!permissiveEnvironments.length) return 0; + + const folders = await folderDAL.findBySecretPathMultiEnv(projectId, permissiveEnvironments, secretPath); + + if (!folders.length) { + throw new NotFoundError({ + message: `Folders with path '${secretPath}' in environments with slugs '${permissiveEnvironments.join( + ", " + )}' not found` + }); + } + + const count = await secretRotationV2DAL.findWithMappedSecretsCount({ + $in: { folderId: folders.map((folder) => folder.id) }, + search, + projectId + }); + + return count; + }; + + const getDashboardSecretRotations = async ( + { + projectId, + environments, + secretPath, + search, + limit, + offset = 0, + orderBy = SecretsOrderBy.Name, + orderDirection = OrderByDirection.ASC + }: TGetDashboardSecretRotationsV2, + actor: OrgServiceActor + ) => { + // we don't check plan for dashboard like dynamic secret, actions will be prevented + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager, + projectId + }); + + const permissiveEnvironments = environments.filter((environment) => + permission.can( + ProjectPermissionSecretRotationActions.Read, + subject(ProjectPermissionSub.SecretRotation, { environment, secretPath }) + ) + ); + + if (!permissiveEnvironments.length) return []; + + const folders = await folderDAL.findBySecretPathMultiEnv(projectId, permissiveEnvironments, secretPath); + + if (!folders.length) { + throw new NotFoundError({ + message: `Folders with path '${secretPath}' in environments with slugs '${permissiveEnvironments.join( + ", " + )}' not found` + }); + } + + const folderIds = folders.map((folder) => folder.id); + + const secretRotations = await secretRotationV2DAL.findWithMappedSecrets( + { + $in: { folderId: folderIds }, + search, + projectId + }, + { + limit, + offset, + sort: orderBy ? [[orderBy, orderDirection]] : undefined + } + ); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + + const secretRotationsWithSecrets = await Promise.all( + secretRotations.map(async ({ secrets, ...rotation }) => { + const decryptedSecrets = secrets.map((secret) => { + const canDescribeSecret = hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.DescribeSecret, + { + environment: rotation.environment.slug, + secretPath: rotation.folder.path, + secretName: secret.key, + // TODO: scott/akhil our mapper seems to not propagate children's children types + // @ts-expect-error eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment + secretTags: (secret.tags as { slug: string; name: string; color: string }[]).map((i) => i.slug) + } + ); + + if (!canDescribeSecret) { + return null; // return null so we know to display empty row in dashboard + } + + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment: rotation.environment.slug, + secretPath: rotation.folder.path, + secretName: secret.key, + // TODO: scott/akhil our mapper seems to not propagate children's children types + // @ts-expect-error eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment + secretTags: (secret.tags as { slug: string; name: string; color: string }[]).map((i) => i.slug) + } + ); + + return reshapeBridgeSecret( + projectId, + rotation.environment.slug, + rotation.folder.path, + { + ...secret, + value: secret.encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() + : "", + comment: secret.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() + : "" + }, + secretValueHidden && secret.type === SecretType.Shared + ); + }); + + const expandedRotation = await expandSecretRotation(rotation, kmsService); + + return { + ...expandedRotation, + secrets: decryptedSecrets + }; + }) + ); + + return secretRotationsWithSecrets as (TSecretRotationV2 & { + secrets: Awaited>[]; + })[]; + }; + + const getQuickSearchSecretRotations = async ( + { folderMappings, filters: { search, ...options }, projectId }: TQuickSearchSecretRotationsV2, + actor: OrgServiceActor + ) => { + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager + }); + + const permissiveFolderMappings = folderMappings.filter(({ path, environment }) => + permission.can( + ProjectPermissionSecretRotationActions.Read, + subject(ProjectPermissionSub.SecretRotation, { environment, secretPath: path }) + ) + ); + + if (!permissiveFolderMappings.length) return []; + + const secretRotations = await secretRotationV2DAL.find( + { + projectId, + $search: { + name: `%${search}%` + }, + $in: { + folderId: permissiveFolderMappings.map(({ folderId }) => folderId) + } + }, + options + ); + + return secretRotations as TSecretRotationV2[]; + }; + + return { + listSecretRotationOptions, + listSecretRotationsByProjectId, + createSecretRotation, + updateSecretRotation, + findSecretRotationById, + findSecretRotationByName, + deleteSecretRotation, + findSecretRotationGeneratedCredentialsById, + rotateSecretRotation, + rotateGeneratedCredentials, + getDashboardSecretRotationCount, + getDashboardSecretRotations, + getQuickSearchSecretRotations + }; +}; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts new file mode 100644 index 000000000..c52fa5465 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts @@ -0,0 +1,180 @@ +import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types"; +import { TSqlCredentialsRotationGeneratedCredentials } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types"; +import { OrderByDirection } from "@app/lib/types"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { SecretsOrderBy } from "@app/services/secret/secret-types"; + +import { + TAuth0ClientSecretRotation, + TAuth0ClientSecretRotationGeneratedCredentials, + TAuth0ClientSecretRotationInput, + TAuth0ClientSecretRotationListItem, + TAuth0ClientSecretRotationWithConnection +} from "./auth0-client-secret"; +import { + TMsSqlCredentialsRotation, + TMsSqlCredentialsRotationInput, + TMsSqlCredentialsRotationListItem, + TMsSqlCredentialsRotationWithConnection +} from "./mssql-credentials"; +import { + TPostgresCredentialsRotation, + TPostgresCredentialsRotationInput, + TPostgresCredentialsRotationListItem, + TPostgresCredentialsRotationWithConnection +} from "./postgres-credentials"; +import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal"; +import { SecretRotation } from "./secret-rotation-v2-enums"; + +export type TSecretRotationV2 = TPostgresCredentialsRotation | TMsSqlCredentialsRotation | TAuth0ClientSecretRotation; + +export type TSecretRotationV2WithConnection = + | TPostgresCredentialsRotationWithConnection + | TMsSqlCredentialsRotationWithConnection + | TAuth0ClientSecretRotationWithConnection; + +export type TSecretRotationV2GeneratedCredentials = + | TSqlCredentialsRotationGeneratedCredentials + | TAuth0ClientSecretRotationGeneratedCredentials; + +export type TSecretRotationV2Input = + | TPostgresCredentialsRotationInput + | TMsSqlCredentialsRotationInput + | TAuth0ClientSecretRotationInput; + +export type TSecretRotationV2ListItem = + | TPostgresCredentialsRotationListItem + | TMsSqlCredentialsRotationListItem + | TAuth0ClientSecretRotationListItem; + +export type TSecretRotationV2Raw = NonNullable>>; + +export type TListSecretRotationsV2ByProjectId = { + projectId: string; + type?: SecretRotation; +}; + +export type TFindSecretRotationV2ByIdDTO = { + rotationId: string; + type: SecretRotation; +}; + +export type TRotateSecretRotationV2 = TFindSecretRotationV2ByIdDTO & { auditLogInfo: AuditLogInfo }; + +export type TRotateAtUtc = { hours: number; minutes: number }; + +export type TFindSecretRotationV2ByNameDTO = { + rotationName: string; + secretPath: string; + environment: string; + projectId: string; + type: SecretRotation; +}; + +export type TCreateSecretRotationV2DTO = Pick< + TSecretRotationV2, + "parameters" | "secretsMapping" | "description" | "rotationInterval" | "name" | "connectionId" | "projectId" +> & { + type: SecretRotation; + secretPath: string; + environment: string; + isAutoRotationEnabled?: boolean; + rotateAtUtc?: TRotateAtUtc; +}; + +export type TUpdateSecretRotationV2DTO = Partial< + Omit +> & { + rotationId: string; + type: SecretRotation; +}; + +export type TDeleteSecretRotationV2DTO = { + type: SecretRotation; + rotationId: string; + deleteSecrets: boolean; + revokeGeneratedCredentials: boolean; +}; + +export type TGetDashboardSecretRotationV2Count = { + search?: string; + projectId: string; + secretPath: string; + environments: string[]; +}; + +export type TGetDashboardSecretRotationsV2 = { + search?: string; + projectId: string; + secretPath: string; + environments: string[]; + orderBy?: SecretsOrderBy; + orderDirection?: OrderByDirection; + limit: number; + offset: number; +}; + +export type TQuickSearchSecretRotationsV2Filters = { + offset?: number; + limit?: number; + orderBy?: SecretsOrderBy; + orderDirection?: OrderByDirection; + search?: string; +}; + +export type TQuickSearchSecretRotationsV2 = { + projectId: string; + folderMappings: { folderId: string; path: string; environment: string }[]; + filters: TQuickSearchSecretRotationsV2Filters; +}; + +export type TSecretRotationRotateGeneratedCredentials = { + auditLogInfo?: AuditLogInfo; + jobId?: string; + shouldSendNotification?: boolean; + isFinalAttempt?: boolean; + isManualRotation?: boolean; +}; + +export type TSecretRotationRotateSecretsJobPayload = { rotationId: string; queuedAt: Date; isManualRotation: boolean }; + +export type TSecretRotationSendNotificationJobPayload = { + secretRotation: TSecretRotationV2Raw; +}; + +// scott: the reason for the callback structure of the rotation factory is to facilitate, when possible, +// transactional behavior. By passing in the rotation mutation, if this mutation fails we can roll back the +// third party credential changes (when supported), preventing credentials getting out of sync + +export type TRotationFactoryIssueCredentials = ( + callback: (newCredentials: T[number]) => Promise +) => Promise; + +export type TRotationFactoryRevokeCredentials = ( + generatedCredentials: T, + callback: () => Promise +) => Promise; + +export type TRotationFactoryRotateCredentials = ( + credentialsToRevoke: T[number] | undefined, + callback: (newCredentials: T[number]) => Promise +) => Promise; + +export type TRotationFactoryGetSecretsPayload = ( + generatedCredentials: T[number] +) => { key: string; value: string }[]; + +export type TRotationFactory< + T extends TSecretRotationV2WithConnection, + C extends TSecretRotationV2GeneratedCredentials +> = ( + secretRotation: T, + appConnectionDAL: Pick, + kmsService: Pick +) => { + issueCredentials: TRotationFactoryIssueCredentials; + revokeCredentials: TRotationFactoryRevokeCredentials; + rotateCredentials: TRotationFactoryRotateCredentials; + getSecretsPayload: TRotationFactoryGetSecretsPayload; +}; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts new file mode 100644 index 000000000..2db9c0251 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +import { Auth0ClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret"; +import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials"; +import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials"; + +export const SecretRotationV2Schema = z.discriminatedUnion("type", [ + PostgresCredentialsRotationSchema, + MsSqlCredentialsRotationSchema, + Auth0ClientSecretRotationSchema +]); diff --git a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/index.ts b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/index.ts new file mode 100644 index 000000000..1ab210d66 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/index.ts @@ -0,0 +1,2 @@ +export * from "./sql-credentials-rotation-fns"; +export * from "./sql-credentials-rotation-schemas"; diff --git a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts new file mode 100644 index 000000000..12e9b5964 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts @@ -0,0 +1,161 @@ +import { + TRotationFactory, + TRotationFactoryGetSecretsPayload, + TRotationFactoryIssueCredentials, + TRotationFactoryRevokeCredentials, + TRotationFactoryRotateCredentials +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { getSqlConnectionClient, SQL_CONNECTION_ALTER_LOGIN_STATEMENT } from "@app/services/app-connection/shared/sql"; + +import { generatePassword } from "../utils"; +import { + TSqlCredentialsRotationGeneratedCredentials, + TSqlCredentialsRotationWithConnection +} from "./sql-credentials-rotation-types"; + +const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGeneratedCredentials) => { + const error = e as Error; + + if (!error?.message) return "Unknown error"; + + let redactedMessage = error.message; + + credentials.forEach(({ password }) => { + redactedMessage = redactedMessage.replaceAll(password, "*******************"); + }); + + return redactedMessage; +}; + +export const sqlCredentialsRotationFactory: TRotationFactory< + TSqlCredentialsRotationWithConnection, + TSqlCredentialsRotationGeneratedCredentials +> = (secretRotation) => { + const { + connection, + parameters: { username1, username2 }, + activeIndex, + secretsMapping + } = secretRotation; + + const $validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => { + const client = await getSqlConnectionClient({ + ...connection, + credentials: { + ...connection.credentials, + ...credentials + } + }); + + try { + await client.raw("SELECT 1"); + } catch (error) { + throw new Error(redactPasswords(error, [credentials])); + } finally { + await client.destroy(); + } + }; + + const issueCredentials: TRotationFactoryIssueCredentials = async ( + callback + ) => { + const client = await getSqlConnectionClient(connection); + + // For SQL, since we get existing users, we change both their passwords + // on issue to invalidate their existing passwords + const credentialsSet = [ + { username: username1, password: generatePassword() }, + { username: username2, password: generatePassword() } + ]; + + try { + await client.transaction(async (tx) => { + for await (const credentials of credentialsSet) { + await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials)); + } + }); + } catch (error) { + throw new Error(redactPasswords(error, credentialsSet)); + } finally { + await client.destroy(); + } + + for await (const credentials of credentialsSet) { + await $validateCredentials(credentials); + } + + return callback(credentialsSet[0]); + }; + + const revokeCredentials: TRotationFactoryRevokeCredentials = async ( + credentialsToRevoke, + callback + ) => { + const client = await getSqlConnectionClient(connection); + + const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ username, password: generatePassword() })); + + try { + await client.transaction(async (tx) => { + for await (const credentials of revokedCredentials) { + // invalidate previous passwords + await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials)); + } + }); + } catch (error) { + throw new Error(redactPasswords(error, revokedCredentials)); + } finally { + await client.destroy(); + } + + return callback(); + }; + + const rotateCredentials: TRotationFactoryRotateCredentials = async ( + _, + callback + ) => { + const client = await getSqlConnectionClient(connection); + + // generate new password for the next active user + const credentials = { username: activeIndex === 0 ? username2 : username1, password: generatePassword() }; + + try { + await client.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials)); + } catch (error) { + throw new Error(redactPasswords(error, [credentials])); + } finally { + await client.destroy(); + } + + await $validateCredentials(credentials); + + return callback(credentials); + }; + + const getSecretsPayload: TRotationFactoryGetSecretsPayload = ( + generatedCredentials + ) => { + const { username, password } = secretsMapping; + + const secrets = [ + { + key: username, + value: generatedCredentials.username + }, + { + key: password, + value: generatedCredentials.password + } + ]; + + return secrets; + }; + + return { + issueCredentials, + revokeCredentials, + rotateCredentials, + getSecretsPayload + }; +}; diff --git a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-schemas.ts b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-schemas.ts new file mode 100644 index 000000000..7ec47741f --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-schemas.ts @@ -0,0 +1,39 @@ +import { z } from "zod"; + +import { SecretRotations } from "@app/lib/api-docs"; +import { SecretNameSchema } from "@app/server/lib/schemas"; + +export const SqlCredentialsRotationGeneratedCredentialsSchema = z + .object({ + username: z.string(), + password: z.string() + }) + .array() + .min(1) + .max(2); + +export const SqlCredentialsRotationParametersSchema = z.object({ + username1: z + .string() + .trim() + .min(1, "Username1 Required") + .describe(SecretRotations.PARAMETERS.SQL_CREDENTIALS.username1), + username2: z + .string() + .trim() + .min(1, "Username2 Required") + .describe(SecretRotations.PARAMETERS.SQL_CREDENTIALS.username2) +}); + +export const SqlCredentialsRotationSecretsMappingSchema = z.object({ + username: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.SQL_CREDENTIALS.username), + password: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.SQL_CREDENTIALS.password) +}); + +export const SqlCredentialsRotationTemplateSchema = z.object({ + createUserStatement: z.string(), + secretsMapping: z.object({ + username: z.string(), + password: z.string() + }) +}); diff --git a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types.ts b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types.ts new file mode 100644 index 000000000..6eada6019 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types.ts @@ -0,0 +1,14 @@ +import { z } from "zod"; + +import { TMsSqlCredentialsRotationWithConnection } from "@app/ee/services/secret-rotation-v2/mssql-credentials"; +import { TPostgresCredentialsRotationWithConnection } from "@app/ee/services/secret-rotation-v2/postgres-credentials"; + +import { SqlCredentialsRotationGeneratedCredentialsSchema } from "./sql-credentials-rotation-schemas"; + +export type TSqlCredentialsRotationWithConnection = + | TPostgresCredentialsRotationWithConnection + | TMsSqlCredentialsRotationWithConnection; + +export type TSqlCredentialsRotationGeneratedCredentials = z.infer< + typeof SqlCredentialsRotationGeneratedCredentialsSchema +>; diff --git a/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts b/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts new file mode 100644 index 000000000..dfe4c22ed --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts @@ -0,0 +1,84 @@ +import { randomInt } from "crypto"; + +const DEFAULT_PASSWORD_REQUIREMENTS = { + length: 48, + required: { + lowercase: 1, + uppercase: 1, + digits: 1, + symbols: 0 + }, + allowedSymbols: "-_.~!*" +}; + +export const generatePassword = () => { + try { + const { length, required, allowedSymbols } = DEFAULT_PASSWORD_REQUIREMENTS; + + const chars = { + lowercase: "abcdefghijklmnopqrstuvwxyz", + uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + digits: "0123456789", + symbols: allowedSymbols || "-_.~!*" + }; + + const parts: string[] = []; + + if (required.lowercase > 0) { + parts.push( + ...Array(required.lowercase) + .fill(0) + .map(() => chars.lowercase[randomInt(chars.lowercase.length)]) + ); + } + + if (required.uppercase > 0) { + parts.push( + ...Array(required.uppercase) + .fill(0) + .map(() => chars.uppercase[randomInt(chars.uppercase.length)]) + ); + } + + if (required.digits > 0) { + parts.push( + ...Array(required.digits) + .fill(0) + .map(() => chars.digits[randomInt(chars.digits.length)]) + ); + } + + if (required.symbols > 0) { + parts.push( + ...Array(required.symbols) + .fill(0) + .map(() => chars.symbols[randomInt(chars.symbols.length)]) + ); + } + + const requiredTotal = Object.values(required).reduce((a, b) => a + b, 0); + const remainingLength = Math.max(length - requiredTotal, 0); + + const allowedChars = Object.entries(chars) + .filter(([key]) => required[key as keyof typeof required] > 0) + .map(([, value]) => value) + .join(""); + + parts.push( + ...Array(remainingLength) + .fill(0) + .map(() => allowedChars[randomInt(allowedChars.length)]) + ); + + // shuffle the array to mix up the characters + for (let i = parts.length - 1; i > 0; i -= 1) { + const j = randomInt(i + 1); + [parts[i], parts[j]] = [parts[j], parts[i]]; + } + + return parts.join(""); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : "Unknown error"; + throw new Error(`Failed to generate password: ${message}`); + } +}; diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts index 46c519d58..e3c6b6b5c 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts @@ -8,23 +8,49 @@ import axios from "axios"; import jmespath from "jmespath"; import knex from "knex"; -import { getConfig } from "@app/lib/config/env"; -import { getDbConnectionHost } from "@app/lib/knex"; import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { verifyHostInputValidity } from "../../dynamic-secret/dynamic-secret-fns"; import { TAssignOp, TDbProviderClients, TDirectAssignOp, THttpProviderFunction } from "../templates/types"; import { TSecretRotationData, TSecretRotationDbFn } from "./secret-rotation-queue-types"; -const REGEX = /\${([^}]+)}/g; const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000; +const replaceTemplateVariables = (str: string, getValue: (key: string) => unknown) => { + // Use array to collect pieces and join at the end (more efficient for large strings) + const parts: string[] = []; + let pos = 0; + + while (pos < str.length) { + const start = str.indexOf("${", pos); + if (start === -1) { + parts.push(str.slice(pos)); + break; + } + + parts.push(str.slice(pos, start)); + const end = str.indexOf("}", start + 2); + + if (end === -1) { + parts.push(str.slice(start)); + break; + } + + const varName = str.slice(start + 2, end); + parts.push(String(getValue(varName))); + pos = end + 1; + } + + return parts.join(""); +}; + export const interpolate = (data: any, getValue: (key: string) => unknown) => { if (!data) return; if (typeof data === "number") return data; if (typeof data === "string") { - return data.replace(REGEX, (_a, b) => getValue(b) as string); + return replaceTemplateVariables(data, getValue); } if (typeof data === "object" && Array.isArray(data)) { @@ -88,32 +114,14 @@ export const secretRotationDbFn = async ({ variables, options }: TSecretRotationDbFn) => { - const appCfg = getConfig(); - const ssl = ca ? { rejectUnauthorized: false, ca } : undefined; - const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not - const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI); - - if ( - isCloud && - // internal ips - (host === "host.docker.internal" || host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/)) - ) - throw new Error("Invalid db host"); - if ( - host === "localhost" || - host === "127.0.0.1" || - // database infisical uses - dbHost === host - ) - throw new Error("Invalid db host"); - + const [hostIp] = await verifyHostInputValidity(host); const db = knex({ client, connection: { database, port, - host, + host: hostIp, user: username, password, connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT, diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts index 015cce0e5..2c6124348 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts @@ -5,18 +5,15 @@ import { IAMClient } from "@aws-sdk/client-iam"; -import { SecretKeyEncoding, SecretType } from "@app/db/schemas"; +import { SecretType } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; -import { - encryptSymmetric128BitHexKeyUTF8, - infisicalSymmetricDecrypt, - infisicalSymmetricEncypt -} from "@app/lib/crypto/encryption"; +import { encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto/encryption"; import { daysToMillisecond, secondsToMillis } from "@app/lib/dates"; import { NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; +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 { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; @@ -51,7 +48,7 @@ type TSecretRotationQueueFactoryDep = { secretRotationDAL: TSecretRotationDALFactory; projectBotService: Pick; secretDAL: Pick; - secretV2BridgeDAL: Pick; + secretV2BridgeDAL: Pick; secretVersionDAL: Pick; secretVersionV2BridgeDAL: Pick; telemetryService: Pick; @@ -135,20 +132,15 @@ export const secretRotationQueueFactory = ({ // deep copy const provider = JSON.parse(JSON.stringify(rotationProvider)) as TSecretRotationProviderTemplate; + const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = + await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: secretRotation.projectId + }); - // now get the encrypted variable values - // in includes the inputs, the previous outputs - // internal mapping variables etc - const { encryptedDataTag, encryptedDataIV, encryptedData, keyEncoding } = secretRotation; - if (!encryptedDataTag || !encryptedDataIV || !encryptedData || !keyEncoding) { - throw new DisableRotationErrors({ message: "No inputs found" }); - } - const decryptedData = infisicalSymmetricDecrypt({ - keyEncoding: keyEncoding as SecretKeyEncoding, - ciphertext: encryptedData, - iv: encryptedDataIV, - tag: encryptedDataTag - }); + const decryptedData = secretManagerDecryptor({ + cipherTextBlob: secretRotation.encryptedRotationData + }).toString(); const variables = JSON.parse(decryptedData) as TSecretRotationEncData; // rotation set cycle @@ -180,6 +172,8 @@ export const secretRotationQueueFactory = ({ provider.template.client === TDbProviderClients.MsSqlServer ? ({ encrypt: appCfg.ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT, + // when ca is provided use that + trustServerCertificate: !ca, cryptoCredentialsDetails: ca ? { ca } : {} } as Record) : undefined; @@ -301,11 +295,9 @@ export const secretRotationQueueFactory = ({ outputs: newCredential.outputs, internal: newCredential.internal }); - const encVarData = infisicalSymmetricEncypt(JSON.stringify(variables)); - const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId: secretRotation.projectId - }); + const encryptedRotationData = secretManagerEncryptor({ + plainText: Buffer.from(JSON.stringify(variables)) + }).cipherTextBlob; const numberOfSecretsRotated = rotationOutputs.length; if (shouldUseSecretV2Bridge) { @@ -321,11 +313,7 @@ export const secretRotationQueueFactory = ({ await secretRotationDAL.updateById( rotationId, { - encryptedData: encVarData.ciphertext, - encryptedDataIV: encVarData.iv, - encryptedDataTag: encVarData.tag, - keyEncoding: encVarData.encoding, - algorithm: encVarData.algorithm, + encryptedRotationData, lastRotatedAt: new Date(), statusMessage: "Rotated successfull", status: "success" @@ -345,11 +333,14 @@ export const secretRotationQueueFactory = ({ await secretVersionV2BridgeDAL.insertMany( updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({ ...el, + actorType: ActorType.PLATFORM, secretId: id })), tx ); }); + + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(secretRotation.projectId); } else { if (!botKey) throw new NotFoundError({ @@ -369,11 +360,7 @@ export const secretRotationQueueFactory = ({ await secretRotationDAL.updateById( rotationId, { - encryptedData: encVarData.ciphertext, - encryptedDataIV: encVarData.iv, - encryptedDataTag: encVarData.tag, - keyEncoding: encVarData.encoding, - algorithm: encVarData.algorithm, + encryptedRotationData, lastRotatedAt: new Date(), statusMessage: "Rotated successfull", status: "success" diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts index 6dde2657f..2364de79d 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts @@ -1,10 +1,12 @@ import { ForbiddenError, subject } from "@casl/ability"; import Ajv from "ajv"; -import { ProjectVersion, TableName } from "@app/db/schemas"; -import { decryptSymmetric128BitHexKeyUTF8, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; +import { ActionProjectType, ProjectVersion, TableName } from "@app/db/schemas"; +import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto/encryption"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TProjectPermission } from "@app/lib/types"; +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 { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TSecretDALFactory } from "@app/services/secret/secret-dal"; @@ -13,7 +15,11 @@ import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret import { TLicenseServiceFactory } from "../license/license-service"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { + ProjectPermissionSecretActions, + ProjectPermissionSecretRotationActions, + ProjectPermissionSub +} from "../permission/project-permission"; import { TSecretRotationDALFactory } from "./secret-rotation-dal"; import { TSecretRotationQueueFactory } from "./secret-rotation-queue"; import { TSecretRotationEncData } from "./secret-rotation-queue/secret-rotation-queue-types"; @@ -30,6 +36,7 @@ type TSecretRotationServiceFactoryDep = { permissionService: Pick; secretRotationQueue: TSecretRotationQueueFactory; projectBotService: Pick; + kmsService: Pick; }; export type TSecretRotationServiceFactory = ReturnType; @@ -44,7 +51,8 @@ export const secretRotationServiceFactory = ({ folderDAL, secretDAL, projectBotService, - secretV2BridgeDAL + secretV2BridgeDAL, + kmsService }: TSecretRotationServiceFactoryDep) => { const getProviderTemplates = async ({ actor, @@ -53,14 +61,18 @@ export const secretRotationServiceFactory = ({ actorAuthMethod, projectId }: TProjectPermission) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretRotationActions.Read, + ProjectPermissionSub.SecretRotation ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); return { custom: [], @@ -81,15 +93,16 @@ export const secretRotationServiceFactory = ({ secretPath, environment }: TCreateSecretRotationDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretRotationActions.Read, ProjectPermissionSub.SecretRotation ); @@ -100,7 +113,7 @@ export const secretRotationServiceFactory = ({ }); } ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); @@ -114,6 +127,13 @@ export const secretRotationServiceFactory = ({ }); if (selectedSecrets.length !== Object.values(outputs).length) throw new NotFoundError({ message: `Secrets not found in folder with ID '${folder.id}'` }); + const rotatedSecrets = selectedSecrets.filter(({ isRotatedSecret }) => isRotatedSecret); + if (rotatedSecrets.length) + throw new BadRequestError({ + message: `Selected secrets are already used for rotation: ${rotatedSecrets + .map((secret) => secret.key) + .join(", ")}` + }); } else { const selectedSecrets = await secretDAL.find({ folderId: folder.id, @@ -154,7 +174,11 @@ export const secretRotationServiceFactory = ({ inputs: formattedInputs, creds: [] }; - const encData = infisicalSymmetricEncypt(JSON.stringify(unencryptedData)); + const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + const secretRotation = await secretRotationDAL.transaction(async (tx) => { const doc = await secretRotationDAL.create( { @@ -162,11 +186,8 @@ export const secretRotationServiceFactory = ({ secretPath, interval, envId: folder.envId, - encryptedDataTag: encData.tag, - encryptedDataIV: encData.iv, - encryptedData: encData.ciphertext, - algorithm: encData.algorithm, - keyEncoding: encData.encoding + encryptedRotationData: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(unencryptedData)) }) + .cipherTextBlob }, tx ); @@ -189,14 +210,18 @@ export const secretRotationServiceFactory = ({ }; const getByProjectId = async ({ actorId, projectId, actor, actorOrgId, actorAuthMethod }: TListByProjectIdDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretRotationActions.Read, + ProjectPermissionSub.SecretRotation ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); if (shouldUseSecretV2Bridge) { const docs = await secretRotationDAL.findSecretV2({ projectId }); @@ -234,14 +259,18 @@ export const secretRotationServiceFactory = ({ message: "Failed to add secret rotation due to plan restriction. Upgrade plan to add secret rotation." }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - doc.projectId, + projectId: project.id, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretRotationActions.Edit, + ProjectPermissionSub.SecretRotation ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation); await secretRotationQueue.removeFromQueue(doc.id, doc.interval); await secretRotationQueue.addToQueue(doc.id, doc.interval); return doc; @@ -251,15 +280,16 @@ export const secretRotationServiceFactory = ({ const doc = await secretRotationDAL.findById(rotationId); if (!doc) throw new NotFoundError({ message: `Rotation with ID '${rotationId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - doc.projectId, + projectId: doc.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, + ProjectPermissionSecretRotationActions.Delete, ProjectPermissionSub.SecretRotation ); const deletedDoc = await secretRotationDAL.transaction(async (tx) => { diff --git a/backend/src/ee/services/secret-rotation/templates/index.ts b/backend/src/ee/services/secret-rotation/templates/index.ts index 39774ae28..ce3ecd687 100644 --- a/backend/src/ee/services/secret-rotation/templates/index.ts +++ b/backend/src/ee/services/secret-rotation/templates/index.ts @@ -18,7 +18,8 @@ export const rotationTemplates: TSecretRotationProviderTemplate[] = [ title: "PostgreSQL", image: "postgres.png", description: "Rotate PostgreSQL/CockroachDB user credentials", - template: POSTGRES_TEMPLATE + template: POSTGRES_TEMPLATE, + isDeprecated: true }, { name: "mysql", @@ -32,7 +33,8 @@ export const rotationTemplates: TSecretRotationProviderTemplate[] = [ title: "Microsoft SQL Server", image: "mssqlserver.png", description: "Rotate Microsoft SQL server user credentials", - template: MSSQL_TEMPLATE + template: MSSQL_TEMPLATE, + isDeprecated: true }, { name: "aws-iam", diff --git a/backend/src/ee/services/secret-rotation/templates/types.ts b/backend/src/ee/services/secret-rotation/templates/types.ts index 2adc40ba3..2ec998db7 100644 --- a/backend/src/ee/services/secret-rotation/templates/types.ts +++ b/backend/src/ee/services/secret-rotation/templates/types.ts @@ -50,6 +50,7 @@ export type TSecretRotationProviderTemplate = { image?: string; description?: string; template: THttpProviderTemplate | TDbProviderTemplate | TAwsProviderTemplate; + isDeprecated?: boolean; }; export type THttpProviderTemplate = { diff --git a/backend/src/ee/services/secret-scanning/secret-scanning-dal.ts b/backend/src/ee/services/secret-scanning/secret-scanning-dal.ts index 828322ad2..f8bf484e2 100644 --- a/backend/src/ee/services/secret-scanning/secret-scanning-dal.ts +++ b/backend/src/ee/services/secret-scanning/secret-scanning-dal.ts @@ -1,9 +1,12 @@ -import { Knex } from "knex"; +import knex, { Knex } from "knex"; import { TDbClient } from "@app/db"; import { TableName, TSecretScanningGitRisksInsert } from "@app/db/schemas"; -import { DatabaseError } from "@app/lib/errors"; -import { ormify } from "@app/lib/knex"; +import { DatabaseError, GatewayTimeoutError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { OrderByDirection } from "@app/lib/types"; + +import { SecretScanningResolvedStatus, TGetOrgRisksDTO } from "./secret-scanning-types"; export type TSecretScanningDALFactory = ReturnType; @@ -19,5 +22,70 @@ export const secretScanningDALFactory = (db: TDbClient) => { } }; - return { ...gitRiskOrm, upsert }; + const findByOrgId = async (orgId: string, filter: TGetOrgRisksDTO["filter"], tx?: Knex) => { + try { + // Find statements + const sqlQuery = (tx || db.replicaNode())(TableName.SecretScanningGitRisk) + // eslint-disable-next-line func-names + .where(`${TableName.SecretScanningGitRisk}.orgId`, orgId); + + if (filter.repositoryNames) { + void sqlQuery.whereIn(`${TableName.SecretScanningGitRisk}.repositoryFullName`, filter.repositoryNames); + } + + if (filter.resolvedStatus) { + if (filter.resolvedStatus !== SecretScanningResolvedStatus.All) { + const isResolved = filter.resolvedStatus === SecretScanningResolvedStatus.Resolved; + + void sqlQuery.where(`${TableName.SecretScanningGitRisk}.isResolved`, isResolved); + } + } + + // Select statements + void sqlQuery + .select(selectAllTableCols(TableName.SecretScanningGitRisk)) + .limit(filter.limit) + .offset(filter.offset); + + if (filter.orderBy) { + const orderDirection = filter.orderDirection || OrderByDirection.ASC; + + void sqlQuery.orderBy(filter.orderBy, orderDirection); + } + + const countQuery = (tx || db.replicaNode())(TableName.SecretScanningGitRisk) + .where(`${TableName.SecretScanningGitRisk}.orgId`, orgId) + .count(); + + const uniqueReposQuery = (tx || db.replicaNode())(TableName.SecretScanningGitRisk) + .where(`${TableName.SecretScanningGitRisk}.orgId`, orgId) + .distinct("repositoryFullName") + .select("repositoryFullName"); + + // we timeout long running queries to prevent DB resource issues (2 minutes) + const docs = await sqlQuery.timeout(1000 * 120); + const uniqueRepos = await uniqueReposQuery.timeout(1000 * 120); + const totalCount = await countQuery; + + return { + risks: docs, + totalCount: Number(totalCount?.[0].count), + repos: uniqueRepos + .filter(Boolean) + .map((r) => r.repositoryFullName!) + .sort((a, b) => a.localeCompare(b)) + }; + } catch (error) { + if (error instanceof knex.KnexTimeoutError) { + throw new GatewayTimeoutError({ + error, + message: "Failed to fetch secret leaks due to timeout. Add more search filters." + }); + } + + throw new DatabaseError({ error }); + } + }; + + return { ...gitRiskOrm, upsert, findByOrgId }; }; diff --git a/backend/src/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue.ts b/backend/src/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue.ts index 1907ddd9a..42ff90055 100644 --- a/backend/src/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue.ts +++ b/backend/src/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue.ts @@ -238,11 +238,11 @@ export const secretScanningQueueFactory = ({ }); queueService.listen(QueueName.SecretPushEventScan, "failed", (job, err) => { - logger.error("Failed to secret scan on push", job?.data, err); + logger.error(err, "Failed to secret scan on push", job?.data); }); queueService.listen(QueueName.SecretFullRepoScan, "failed", (job, err) => { - logger.error("Failed to do full repo secret scan", job?.data, err); + logger.error(err, "Failed to do full repo secret scan", job?.data); }); return { startFullRepoScan, startPushEventScan }; diff --git a/backend/src/ee/services/secret-scanning/secret-scanning-service.ts b/backend/src/ee/services/secret-scanning/secret-scanning-service.ts index 945164094..c5e7be9d8 100644 --- a/backend/src/ee/services/secret-scanning/secret-scanning-service.ts +++ b/backend/src/ee/services/secret-scanning/secret-scanning-service.ts @@ -15,6 +15,7 @@ import { TSecretScanningDALFactory } from "./secret-scanning-dal"; import { TSecretScanningQueueFactory } from "./secret-scanning-queue"; import { SecretScanningRiskStatus, + TGetAllOrgRisksDTO, TGetOrgInstallStatusDTO, TGetOrgRisksDTO, TInstallAppSessionDTO, @@ -118,11 +119,21 @@ export const secretScanningServiceFactory = ({ return Boolean(appInstallation); }; - const getRisksByOrg = async ({ actor, orgId, actorId, actorAuthMethod, actorOrgId }: TGetOrgRisksDTO) => { + const getRisksByOrg = async ({ actor, orgId, actorId, actorAuthMethod, actorOrgId, filter }: TGetOrgRisksDTO) => { const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning); + + const results = await secretScanningDAL.findByOrgId(orgId, filter); + + return results; + }; + + const getAllRisksByOrg = async ({ actor, orgId, actorId, actorAuthMethod, actorOrgId }: TGetAllOrgRisksDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning); + const risks = await secretScanningDAL.find({ orgId }, { sort: [["createdAt", "desc"]] }); - return { risks }; + return risks; }; const updateRiskStatus = async ({ @@ -189,6 +200,7 @@ export const secretScanningServiceFactory = ({ linkInstallationToOrg, getOrgInstallationStatus, getRisksByOrg, + getAllRisksByOrg, updateRiskStatus, handleRepoPushEvent, handleRepoDeleteEvent diff --git a/backend/src/ee/services/secret-scanning/secret-scanning-types.ts b/backend/src/ee/services/secret-scanning/secret-scanning-types.ts index dc83599af..d3212a73f 100644 --- a/backend/src/ee/services/secret-scanning/secret-scanning-types.ts +++ b/backend/src/ee/services/secret-scanning/secret-scanning-types.ts @@ -1,4 +1,4 @@ -import { TOrgPermission } from "@app/lib/types"; +import { OrderByDirection, TOrgPermission } from "@app/lib/types"; export enum SecretScanningRiskStatus { FalsePositive = "RESOLVED_FALSE_POSITIVE", @@ -7,6 +7,12 @@ export enum SecretScanningRiskStatus { Unresolved = "UNRESOLVED" } +export enum SecretScanningResolvedStatus { + All = "all", + Resolved = "resolved", + Unresolved = "unresolved" +} + export type TInstallAppSessionDTO = TOrgPermission; export type TLinkInstallSessionDTO = { @@ -16,7 +22,22 @@ export type TLinkInstallSessionDTO = { export type TGetOrgInstallStatusDTO = TOrgPermission; -export type TGetOrgRisksDTO = TOrgPermission; +type RiskFilter = { + offset: number; + limit: number; + orderBy?: "createdAt" | "name"; + orderDirection?: OrderByDirection; + repositoryNames?: string[]; + resolvedStatus?: SecretScanningResolvedStatus; +}; + +export type TGetOrgRisksDTO = { + filter: RiskFilter; +} & TOrgPermission; + +export type TGetAllOrgRisksDTO = { + filter: Omit; +} & TOrgPermission; export type TUpdateRiskStatusDTO = { riskId: string; diff --git a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts index 481123896..015a8d420 100644 --- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts +++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts @@ -1,14 +1,18 @@ -import { ForbiddenError, subject } from "@casl/ability"; +/* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-argument */ +// akhilmhdh: I did this, quite strange bug with eslint. Everything do have a type stil has this error +import { ForbiddenError } from "@casl/ability"; -import { TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas"; +import { ActionProjectType, TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { InternalServerError, NotFoundError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; +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 { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TSecretDALFactory } from "@app/services/secret/secret-dal"; +import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "@app/services/secret/secret-fns"; import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal"; import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; @@ -19,8 +23,16 @@ import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secre import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; import { TLicenseServiceFactory } from "../license/license-service"; +import { + hasSecretReadValueOrDescribePermission, + throwIfMissingSecretReadValueOrDescribePermission +} from "../permission/permission-fns"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "../permission/project-permission"; import { TGetSnapshotDataDTO, TProjectSnapshotCountDTO, @@ -83,20 +95,21 @@ export const secretSnapshotServiceFactory = ({ actorAuthMethod, path }: TProjectSnapshotCountDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath: path + }); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) { @@ -119,20 +132,21 @@ export const secretSnapshotServiceFactory = ({ limit = 20, offset = 0 }: TProjectSnapshotListDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath: path + }); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) @@ -147,15 +161,17 @@ export const secretSnapshotServiceFactory = ({ const getSnapshotData = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TGetSnapshotDataDTO) => { const snapshot = await snapshotDAL.findById(id); if (!snapshot) throw new NotFoundError({ message: `Snapshot with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - snapshot.projectId, + projectId: snapshot.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); + const shouldUseBridge = snapshot.projectVersion === 3; let snapshotDetails; if (shouldUseBridge) { @@ -164,68 +180,112 @@ export const secretSnapshotServiceFactory = ({ projectId: snapshot.projectId }); const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotV2DataById(id); + + const fullFolderPath = await getFullFolderPath({ + folderDAL, + folderId: encryptedSnapshotDetails.folderId, + envId: encryptedSnapshotDetails.environment.id + }); + snapshotDetails = { ...encryptedSnapshotDetails, - secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({ - ...el, - secretKey: el.key, - secretValue: el.encryptedValue - ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() - : "", - secretComment: el.encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() - : "" - })) + secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => { + const canReadValue = hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment: encryptedSnapshotDetails.environment.slug, + secretPath: fullFolderPath, + secretName: el.key, + secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined + } + ); + + let secretValue = ""; + if (canReadValue) { + secretValue = el.encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() + : ""; + } else { + secretValue = INFISICAL_SECRET_VALUE_HIDDEN_MASK; + } + + return { + ...el, + secretKey: el.key, + secretValueHidden: !canReadValue, + secretValue, + secretComment: el.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() + : "" + }; + }) }; } else { const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotDataById(id); + + const fullFolderPath = await getFullFolderPath({ + folderDAL, + folderId: encryptedSnapshotDetails.folderId, + envId: encryptedSnapshotDetails.environment.id + }); + const { botKey } = await projectBotService.getBotKey(snapshot.projectId); if (!botKey) throw new NotFoundError({ message: `Project bot key not found for project with ID '${snapshot.projectId}'` }); snapshotDetails = { ...encryptedSnapshotDetails, - secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({ - ...el, - secretKey: decryptSymmetric128BitHexKeyUTF8({ + secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => { + const secretKey = decryptSymmetric128BitHexKeyUTF8({ ciphertext: el.secretKeyCiphertext, iv: el.secretKeyIV, tag: el.secretKeyTag, key: botKey - }), - secretValue: decryptSymmetric128BitHexKeyUTF8({ - ciphertext: el.secretValueCiphertext, - iv: el.secretValueIV, - tag: el.secretValueTag, - key: botKey - }), - secretComment: - el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext - ? decryptSymmetric128BitHexKeyUTF8({ - ciphertext: el.secretCommentCiphertext, - iv: el.secretCommentIV, - tag: el.secretCommentTag, - key: botKey - }) - : "" - })) + }); + + const canReadValue = hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment: encryptedSnapshotDetails.environment.slug, + secretPath: fullFolderPath, + secretName: secretKey, + secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined + } + ); + + let secretValue = ""; + + if (canReadValue) { + secretValue = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: el.secretValueCiphertext, + iv: el.secretValueIV, + tag: el.secretValueTag, + key: botKey + }); + } else { + secretValue = INFISICAL_SECRET_VALUE_HIDDEN_MASK; + } + + return { + ...el, + secretKey, + secretValueHidden: !canReadValue, + secretValue, + secretComment: + el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext + ? decryptSymmetric128BitHexKeyUTF8({ + ciphertext: el.secretCommentCiphertext, + iv: el.secretCommentIV, + tag: el.secretCommentTag, + key: botKey + }) + : "" + }; + }) }; } - const fullFolderPath = await getFullFolderPath({ - folderDAL, - folderId: snapshotDetails.folderId, - envId: snapshotDetails.environment.id - }); - - // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: snapshotDetails.environment.slug, - secretPath: fullFolderPath - }) - ); - return snapshotDetails; }; @@ -322,13 +382,14 @@ export const secretSnapshotServiceFactory = ({ if (!snapshot) throw new NotFoundError({ message: `Snapshot with ID '${snapshotId}' not found` }); const shouldUseBridge = snapshot.projectVersion === 3; - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - snapshot.projectId, + projectId: snapshot.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback @@ -337,8 +398,32 @@ export const secretSnapshotServiceFactory = ({ if (shouldUseBridge) { const rollback = await snapshotDAL.transaction(async (tx) => { const rollbackSnaps = await snapshotDAL.findRecursivelySnapshotsV2Bridge(snapshot.id, tx); - // this will remove all secrets in current folder - const deletedTopLevelSecs = await secretV2BridgeDAL.delete({ folderId: snapshot.folderId }, tx); + const secretRotationIds = rollbackSnaps + .flatMap((snap) => snap.secretVersions) + .filter((el) => el.isRotatedSecret) + .map((el) => el.secretId); + + // this will remove all secrets in current folder except rotated secrets which we ignore + const deletedTopLevelSecs = await secretV2BridgeDAL.delete( + { + $complex: { + operator: "and", + value: [ + { + operator: "eq", + field: "folderId", + value: snapshot.folderId + }, + { + operator: "notIn", + field: "id", + value: secretRotationIds + } + ] + } + }, + tx + ); const deletedTopLevelSecsGroupById = groupBy(deletedTopLevelSecs, (item) => item.id); // this will remove all secrets and folders on child // due to sql foreign key and link list connection removing the folders removes everything below too @@ -363,14 +448,31 @@ export const secretSnapshotServiceFactory = ({ ); const secrets = await secretV2BridgeDAL.insertMany( rollbackSnaps.flatMap(({ secretVersions, folderId }) => - secretVersions.map( - ({ latestSecretVersion, version, updatedAt, createdAt, secretId, envId, id, tags, ...el }) => ({ - ...el, - id: secretId, - version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion, - folderId - }) - ) + secretVersions + .filter((v) => !v.isRotatedSecret) + .map( + ({ + latestSecretVersion, + version, + updatedAt, + createdAt, + secretId, + envId, + id, + tags, + // exclude the bottom fields from the secret - they are for versioning only. + userActorId, + identityActorId, + actorType, + isRotatedSecret, + ...el + }) => ({ + ...el, + id: secretId, + version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion, + folderId + }) + ) ), tx ); @@ -395,8 +497,18 @@ export const secretSnapshotServiceFactory = ({ })), tx ); + const userActorId = actor === ActorType.USER ? actorId : undefined; + const identityActorId = actor !== ActorType.USER ? actorId : undefined; + const actorType = actor || ActorType.PLATFORM; + const secretVersions = await secretVersionV2BridgeDAL.insertMany( - secrets.map(({ id, updatedAt, createdAt, ...el }) => ({ ...el, secretId: id })), + secrets.map(({ id, updatedAt, createdAt, ...el }) => ({ + ...el, + secretId: id, + userActorId, + identityActorId, + actorType + })), tx ); await secretVersionV2TagBridgeDAL.insertMany( diff --git a/backend/src/ee/services/secret-snapshot/snapshot-dal.ts b/backend/src/ee/services/secret-snapshot/snapshot-dal.ts index 8a9eeab8c..c547d85c2 100644 --- a/backend/src/ee/services/secret-snapshot/snapshot-dal.ts +++ b/backend/src/ee/services/secret-snapshot/snapshot-dal.ts @@ -1,4 +1,4 @@ -/* eslint-disable no-await-in-loop */ +/* eslint-disable no-await-in-loop,@typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-argument */ import { Knex } from "knex"; import { z } from "zod"; @@ -181,6 +181,11 @@ export const snapshotDALFactory = (db: TDbClient) => { `${TableName.SnapshotFolder}.folderVersionId`, `${TableName.SecretFolderVersion}.id` ) + .leftJoin( + TableName.SecretRotationV2SecretMapping, + `${TableName.SecretRotationV2SecretMapping}.secretId`, + `${TableName.SecretVersionV2}.secretId` + ) .select(selectAllTableCols(TableName.SecretVersionV2)) .select( db.ref("id").withSchema(TableName.Snapshot).as("snapshotId"), @@ -195,7 +200,8 @@ export const snapshotDALFactory = (db: TDbClient) => { db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), - db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug") + db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), + db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping) ); return sqlNestRelationships({ data, @@ -221,7 +227,11 @@ export const snapshotDALFactory = (db: TDbClient) => { { key: "id", label: "secretVersions" as const, - mapper: (el) => SecretVersionsV2Schema.parse(el), + mapper: (el) => ({ + ...SecretVersionsV2Schema.parse(el), + isRotatedSecret: Boolean(el.rotationId), + rotationId: el.rotationId + }), childrenMapper: [ { key: "tagVersionId", @@ -476,6 +486,11 @@ export const snapshotDALFactory = (db: TDbClient) => { `${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id` ) + .leftJoin( + TableName.SecretRotationV2SecretMapping, + `${TableName.SecretVersionV2}.secretId`, + `${TableName.SecretRotationV2SecretMapping}.secretId` + ) .leftJoin<{ latestSecretVersion: number }>( (tx || db)(TableName.SecretVersionV2) .groupBy("secretId") @@ -506,7 +521,8 @@ export const snapshotDALFactory = (db: TDbClient) => { db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), - db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug") + db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), + db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping) ); const formated = sqlNestRelationships({ @@ -523,7 +539,8 @@ export const snapshotDALFactory = (db: TDbClient) => { label: "secretVersions" as const, mapper: (el) => ({ ...SecretVersionsV2Schema.parse(el), - latestSecretVersion: el.latestSecretVersion as number + latestSecretVersion: el.latestSecretVersion as number, + isRotatedSecret: Boolean(el.rotationId) }), childrenMapper: [ { diff --git a/backend/src/ee/services/secret-snapshot/snapshot-service-fns.ts b/backend/src/ee/services/secret-snapshot/snapshot-service-fns.ts index 51cb9c056..cd5372c8d 100644 --- a/backend/src/ee/services/secret-snapshot/snapshot-service-fns.ts +++ b/backend/src/ee/services/secret-snapshot/snapshot-service-fns.ts @@ -8,7 +8,18 @@ type GetFullFolderPath = { export const getFullFolderPath = async ({ folderDAL, folderId, envId }: GetFullFolderPath): Promise => { // Helper function to remove duplicate slashes - const removeDuplicateSlashes = (path: string) => path.replace(/\/{2,}/g, "/"); + const removeDuplicateSlashes = (path: string) => { + const chars = []; + let lastWasSlash = false; + + for (let i = 0; i < path.length; i += 1) { + const char = path[i]; + if (char !== "/" || !lastWasSlash) chars.push(char); + lastWasSlash = char === "/"; + } + + return chars.join(""); + }; // Fetch all folders at once based on environment ID to avoid multiple queries const folders = await folderDAL.find({ envId }); diff --git a/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-dal.ts b/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-dal.ts new file mode 100644 index 000000000..b8afa0df2 --- /dev/null +++ b/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-dal.ts @@ -0,0 +1,66 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; + +export type TSshCertificateTemplateDALFactory = ReturnType; + +export const sshCertificateTemplateDALFactory = (db: TDbClient) => { + const sshCertificateTemplateOrm = ormify(db, TableName.SshCertificateTemplate); + + const getById = async (id: string, tx?: Knex) => { + try { + const certTemplate = await (tx || db.replicaNode())(TableName.SshCertificateTemplate) + .join( + TableName.SshCertificateAuthority, + `${TableName.SshCertificateAuthority}.id`, + `${TableName.SshCertificateTemplate}.sshCaId` + ) + .join(TableName.Project, `${TableName.Project}.id`, `${TableName.SshCertificateAuthority}.projectId`) + .where(`${TableName.SshCertificateTemplate}.id`, "=", id) + .select(selectAllTableCols(TableName.SshCertificateTemplate)) + .select( + db.ref("projectId").withSchema(TableName.SshCertificateAuthority), + db.ref("friendlyName").as("caName").withSchema(TableName.SshCertificateAuthority), + db.ref("status").as("caStatus").withSchema(TableName.SshCertificateAuthority) + ) + .first(); + + return certTemplate; + } catch (error) { + throw new DatabaseError({ error, name: "Get SSH certificate template by ID" }); + } + }; + + /** + * Returns the SSH certificate template named [name] within project with id [projectId] + */ + const getByName = async (name: string, projectId: string, tx?: Knex) => { + try { + const certTemplate = await (tx || db.replicaNode())(TableName.SshCertificateTemplate) + .join( + TableName.SshCertificateAuthority, + `${TableName.SshCertificateAuthority}.id`, + `${TableName.SshCertificateTemplate}.sshCaId` + ) + .join(TableName.Project, `${TableName.Project}.id`, `${TableName.SshCertificateAuthority}.projectId`) + .where(`${TableName.SshCertificateTemplate}.name`, "=", name) + .where(`${TableName.Project}.id`, "=", projectId) + .select(selectAllTableCols(TableName.SshCertificateTemplate)) + .select( + db.ref("projectId").withSchema(TableName.SshCertificateAuthority), + db.ref("friendlyName").as("caName").withSchema(TableName.SshCertificateAuthority), + db.ref("status").as("caStatus").withSchema(TableName.SshCertificateAuthority) + ) + .first(); + + return certTemplate; + } catch (error) { + throw new DatabaseError({ error, name: "Get SSH certificate template by name" }); + } + }; + + return { ...sshCertificateTemplateOrm, getById, getByName }; +}; diff --git a/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-schema.ts b/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-schema.ts new file mode 100644 index 000000000..fb7a95203 --- /dev/null +++ b/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-schema.ts @@ -0,0 +1,15 @@ +import { SshCertificateTemplatesSchema } from "@app/db/schemas"; + +export const sanitizedSshCertificateTemplate = SshCertificateTemplatesSchema.pick({ + id: true, + sshCaId: true, + status: true, + name: true, + ttl: true, + maxTTL: true, + allowedUsers: true, + allowedHosts: true, + allowCustomKeyIds: true, + allowUserCertificates: true, + allowHostCertificates: true +}); diff --git a/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-service.ts b/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-service.ts new file mode 100644 index 000000000..6687efaf9 --- /dev/null +++ b/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-service.ts @@ -0,0 +1,249 @@ +import { ForbiddenError } from "@casl/ability"; + +import { ActionProjectType } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { ms } from "@app/lib/ms"; + +import { TSshCertificateAuthorityDALFactory } from "../ssh/ssh-certificate-authority-dal"; +import { TSshCertificateTemplateDALFactory } from "./ssh-certificate-template-dal"; +import { + SshCertTemplateStatus, + TCreateSshCertTemplateDTO, + TDeleteSshCertTemplateDTO, + TGetSshCertTemplateDTO, + TUpdateSshCertTemplateDTO +} from "./ssh-certificate-template-types"; + +type TSshCertificateTemplateServiceFactoryDep = { + sshCertificateTemplateDAL: Pick< + TSshCertificateTemplateDALFactory, + "transaction" | "getByName" | "create" | "updateById" | "deleteById" | "getById" + >; + sshCertificateAuthorityDAL: Pick; + permissionService: Pick; +}; + +export type TSshCertificateTemplateServiceFactory = ReturnType; + +export const sshCertificateTemplateServiceFactory = ({ + sshCertificateTemplateDAL, + sshCertificateAuthorityDAL, + permissionService +}: TSshCertificateTemplateServiceFactoryDep) => { + const createSshCertTemplate = async ({ + sshCaId, + name, + ttl, + maxTTL, + allowUserCertificates, + allowHostCertificates, + allowedUsers, + allowedHosts, + allowCustomKeyIds, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TCreateSshCertTemplateDTO) => { + const ca = await sshCertificateAuthorityDAL.findById(sshCaId); + if (!ca) { + throw new NotFoundError({ + message: `SSH CA with ID ${sshCaId} not found` + }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: ca.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.SshCertificateTemplates + ); + + if (ms(ttl) > ms(maxTTL)) { + throw new BadRequestError({ + message: "TTL cannot be greater than max TTL" + }); + } + + const newCertificateTemplate = await sshCertificateTemplateDAL.transaction(async (tx) => { + const existingTemplate = await sshCertificateTemplateDAL.getByName(name, ca.projectId, tx); + if (existingTemplate) { + throw new BadRequestError({ + message: `SSH certificate template with name ${name} already exists` + }); + } + + const certificateTemplate = await sshCertificateTemplateDAL.create( + { + sshCaId, + name, + ttl, + maxTTL, + allowUserCertificates, + allowHostCertificates, + allowedUsers, + allowedHosts, + allowCustomKeyIds, + status: SshCertTemplateStatus.ACTIVE + }, + tx + ); + + return certificateTemplate; + }); + + return { certificateTemplate: newCertificateTemplate, ca }; + }; + + const updateSshCertTemplate = async ({ + id, + status, + name, + ttl, + maxTTL, + allowUserCertificates, + allowHostCertificates, + allowedUsers, + allowedHosts, + allowCustomKeyIds, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TUpdateSshCertTemplateDTO) => { + const certTemplate = await sshCertificateTemplateDAL.getById(id); + if (!certTemplate) { + throw new NotFoundError({ + message: `SSH certificate template with ID ${id} not found` + }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: certTemplate.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.SshCertificateTemplates + ); + + const updatedCertificateTemplate = await sshCertificateTemplateDAL.transaction(async (tx) => { + if (name) { + const existingTemplate = await sshCertificateTemplateDAL.getByName(name, certTemplate.projectId, tx); + if (existingTemplate && existingTemplate.id !== id) { + throw new BadRequestError({ + message: `SSH certificate template with name ${name} already exists` + }); + } + } + + if (ms(ttl || certTemplate.ttl) > ms(maxTTL || certTemplate.maxTTL)) { + throw new BadRequestError({ + message: "TTL cannot be greater than max TTL" + }); + } + + const certificateTemplate = await sshCertificateTemplateDAL.updateById( + id, + { + status, + name, + ttl, + maxTTL, + allowUserCertificates, + allowHostCertificates, + allowedUsers, + allowedHosts, + allowCustomKeyIds + }, + tx + ); + + return certificateTemplate; + }); + + return { + certificateTemplate: updatedCertificateTemplate, + projectId: certTemplate.projectId + }; + }; + + const deleteSshCertTemplate = async ({ + id, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TDeleteSshCertTemplateDTO) => { + const certificateTemplate = await sshCertificateTemplateDAL.getById(id); + if (!certificateTemplate) { + throw new NotFoundError({ + message: `SSH certificate template with ID ${id} not found` + }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: certificateTemplate.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.SshCertificateTemplates + ); + + await sshCertificateTemplateDAL.deleteById(certificateTemplate.id); + + return certificateTemplate; + }; + + const getSshCertTemplate = async ({ id, actorId, actorAuthMethod, actor, actorOrgId }: TGetSshCertTemplateDTO) => { + const certTemplate = await sshCertificateTemplateDAL.getById(id); + if (!certTemplate) { + throw new NotFoundError({ + message: `SSH certificate template with ID ${id} not found` + }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: certTemplate.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.SshCertificateTemplates + ); + + return certTemplate; + }; + + return { + createSshCertTemplate, + updateSshCertTemplate, + deleteSshCertTemplate, + getSshCertTemplate + }; +}; diff --git a/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-types.ts b/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-types.ts new file mode 100644 index 000000000..64de1bf0c --- /dev/null +++ b/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-types.ts @@ -0,0 +1,39 @@ +import { TProjectPermission } from "@app/lib/types"; + +export enum SshCertTemplateStatus { + ACTIVE = "active", + DISABLED = "disabled" +} + +export type TCreateSshCertTemplateDTO = { + sshCaId: string; + name: string; + ttl: string; + maxTTL: string; + allowUserCertificates: boolean; + allowHostCertificates: boolean; + allowedUsers: string[]; + allowedHosts: string[]; + allowCustomKeyIds: boolean; +} & Omit; + +export type TUpdateSshCertTemplateDTO = { + id: string; + status?: SshCertTemplateStatus; + name?: string; + ttl?: string; + maxTTL?: string; + allowUserCertificates?: boolean; + allowHostCertificates?: boolean; + allowedUsers?: string[]; + allowedHosts?: string[]; + allowCustomKeyIds?: boolean; +} & Omit; + +export type TGetSshCertTemplateDTO = { + id: string; +} & Omit; + +export type TDeleteSshCertTemplateDTO = { + id: string; +} & Omit; diff --git a/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-validators.ts b/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-validators.ts new file mode 100644 index 000000000..a09cc55ff --- /dev/null +++ b/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-validators.ts @@ -0,0 +1,35 @@ +import { isIP } from "net"; +import RE2 from "re2"; + +import { isFQDN } from "@app/lib/validator/validate-url"; + +// Validates usernames or wildcard (*) +export const isValidUserPattern = (value: string): boolean => { + // Length check before regex to prevent ReDoS + if (typeof value !== "string") return false; + if (value.length > 32) return false; // Maximum Linux username length + if (value === "*") return true; // Handle wildcard separately + + // Simpler, more specific pattern for usernames + const userRegex = new RE2(/^[a-z_][a-z0-9_-]*$/i); + return userRegex.test(value); +}; + +// Validates hostnames, wildcard domains, or IP addresses +export const isValidHostPattern = (value: string): boolean => { + // Input validation + if (typeof value !== "string") return false; + + // Length check + if (value.length > 255) return false; + + // Handle the wildcard case separately + if (value === "*") return true; + + // Check for IP addresses using Node.js built-in functions + if (isIP(value)) return true; + + return isFQDN(value, { + allow_wildcard: true + }); +}; diff --git a/backend/src/ee/services/ssh-certificate/ssh-certificate-body-dal.ts b/backend/src/ee/services/ssh-certificate/ssh-certificate-body-dal.ts new file mode 100644 index 000000000..c3d16a39e --- /dev/null +++ b/backend/src/ee/services/ssh-certificate/ssh-certificate-body-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TSshCertificateBodyDALFactory = ReturnType; + +export const sshCertificateBodyDALFactory = (db: TDbClient) => { + const sshCertificateBodyOrm = ormify(db, TableName.SshCertificateBody); + return sshCertificateBodyOrm; +}; diff --git a/backend/src/ee/services/ssh-certificate/ssh-certificate-dal.ts b/backend/src/ee/services/ssh-certificate/ssh-certificate-dal.ts new file mode 100644 index 000000000..9c5bd1d3e --- /dev/null +++ b/backend/src/ee/services/ssh-certificate/ssh-certificate-dal.ts @@ -0,0 +1,38 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify } from "@app/lib/knex"; + +export type TSshCertificateDALFactory = ReturnType; + +export const sshCertificateDALFactory = (db: TDbClient) => { + const sshCertificateOrm = ormify(db, TableName.SshCertificate); + + const countSshCertificatesInProject = async (projectId: string) => { + try { + interface CountResult { + count: string; + } + + const query = db + .replicaNode()(TableName.SshCertificate) + .join( + TableName.SshCertificateAuthority, + `${TableName.SshCertificate}.sshCaId`, + `${TableName.SshCertificateAuthority}.id` + ) + .join(TableName.Project, `${TableName.SshCertificateAuthority}.projectId`, `${TableName.Project}.id`) + .where(`${TableName.Project}.id`, projectId); + + const count = await query.count("*").first(); + + return parseInt((count as unknown as CountResult).count || "0", 10); + } catch (error) { + throw new DatabaseError({ error, name: "Count all SSH certificates in project" }); + } + }; + return { + ...sshCertificateOrm, + countSshCertificatesInProject + }; +}; diff --git a/backend/src/ee/services/ssh-certificate/ssh-certificate-schema.ts b/backend/src/ee/services/ssh-certificate/ssh-certificate-schema.ts new file mode 100644 index 000000000..cf8d59d8c --- /dev/null +++ b/backend/src/ee/services/ssh-certificate/ssh-certificate-schema.ts @@ -0,0 +1,13 @@ +import { SshCertificatesSchema } from "@app/db/schemas"; + +export const sanitizedSshCertificate = SshCertificatesSchema.pick({ + id: true, + sshCaId: true, + sshCertificateTemplateId: true, + serialNumber: true, + certType: true, + principals: true, + keyId: true, + notBefore: true, + notAfter: true +}); diff --git a/backend/src/ee/services/ssh-certificate/ssh-certificate-types.ts b/backend/src/ee/services/ssh-certificate/ssh-certificate-types.ts new file mode 100644 index 000000000..14e2755ee --- /dev/null +++ b/backend/src/ee/services/ssh-certificate/ssh-certificate-types.ts @@ -0,0 +1,7 @@ +export enum SshCertKeyAlgorithm { + RSA_2048 = "RSA_2048", + RSA_4096 = "RSA_4096", + ECDSA_P256 = "EC_prime256v1", + ECDSA_P384 = "EC_secp384r1", + ED25519 = "ED25519" +} diff --git a/backend/src/ee/services/ssh-host/ssh-host-dal.ts b/backend/src/ee/services/ssh-host/ssh-host-dal.ts new file mode 100644 index 000000000..4baeca503 --- /dev/null +++ b/backend/src/ee/services/ssh-host/ssh-host-dal.ts @@ -0,0 +1,193 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { groupBy, unique } from "@app/lib/fn"; +import { ormify } from "@app/lib/knex"; + +export type TSshHostDALFactory = ReturnType; + +export const sshHostDALFactory = (db: TDbClient) => { + const sshHostOrm = ormify(db, TableName.SshHost); + + const findUserAccessibleSshHosts = async (projectIds: string[], userId: string, tx?: Knex) => { + try { + const user = await (tx || db.replicaNode())(TableName.Users).where({ id: userId }).select("username").first(); + + if (!user) { + throw new DatabaseError({ name: `${TableName.Users}: UserNotFound`, error: new Error("User not found") }); + } + + const rows = await (tx || db.replicaNode())(TableName.SshHost) + .leftJoin(TableName.SshHostLoginUser, `${TableName.SshHost}.id`, `${TableName.SshHostLoginUser}.sshHostId`) + .leftJoin( + TableName.SshHostLoginUserMapping, + `${TableName.SshHostLoginUser}.id`, + `${TableName.SshHostLoginUserMapping}.sshHostLoginUserId` + ) + .leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SshHostLoginUserMapping}.userId`) + .whereIn(`${TableName.SshHost}.projectId`, projectIds) + .andWhere(`${TableName.SshHostLoginUserMapping}.userId`, userId) + .select( + db.ref("id").withSchema(TableName.SshHost).as("sshHostId"), + db.ref("projectId").withSchema(TableName.SshHost), + db.ref("hostname").withSchema(TableName.SshHost), + db.ref("userCertTtl").withSchema(TableName.SshHost), + db.ref("hostCertTtl").withSchema(TableName.SshHost), + db.ref("loginUser").withSchema(TableName.SshHostLoginUser), + db.ref("username").withSchema(TableName.Users), + db.ref("userId").withSchema(TableName.SshHostLoginUserMapping), + db.ref("userSshCaId").withSchema(TableName.SshHost), + db.ref("hostSshCaId").withSchema(TableName.SshHost) + ) + .orderBy(`${TableName.SshHost}.updatedAt`, "desc"); + + const grouped = groupBy(rows, (r) => r.sshHostId); + return Object.values(grouped).map((hostRows) => { + const { sshHostId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId, projectId } = hostRows[0]; + + const loginMappingGrouped = groupBy(hostRows, (r) => r.loginUser); + + const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser]) => ({ + loginUser, + allowedPrincipals: { + usernames: [user.username] + } + })); + + return { + id: sshHostId, + hostname, + projectId, + userCertTtl, + hostCertTtl, + loginMappings, + userSshCaId, + hostSshCaId + }; + }); + } catch (error) { + throw new DatabaseError({ error, name: `${TableName.SshHost}: FindSshHostsWithPrincipalsAcrossProjects` }); + } + }; + + const findSshHostsWithLoginMappings = async (projectId: string, tx?: Knex) => { + try { + const rows = await (tx || db.replicaNode())(TableName.SshHost) + .leftJoin(TableName.SshHostLoginUser, `${TableName.SshHost}.id`, `${TableName.SshHostLoginUser}.sshHostId`) + .leftJoin( + TableName.SshHostLoginUserMapping, + `${TableName.SshHostLoginUser}.id`, + `${TableName.SshHostLoginUserMapping}.sshHostLoginUserId` + ) + .leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`) + .where(`${TableName.SshHost}.projectId`, projectId) + .select( + db.ref("id").withSchema(TableName.SshHost).as("sshHostId"), + db.ref("projectId").withSchema(TableName.SshHost), + db.ref("hostname").withSchema(TableName.SshHost), + db.ref("userCertTtl").withSchema(TableName.SshHost), + db.ref("hostCertTtl").withSchema(TableName.SshHost), + db.ref("loginUser").withSchema(TableName.SshHostLoginUser), + db.ref("username").withSchema(TableName.Users), + db.ref("userId").withSchema(TableName.SshHostLoginUserMapping), + db.ref("userSshCaId").withSchema(TableName.SshHost), + db.ref("hostSshCaId").withSchema(TableName.SshHost) + ) + .orderBy(`${TableName.SshHost}.updatedAt`, "desc"); + + const hostsGrouped = groupBy(rows, (r) => r.sshHostId); + return Object.values(hostsGrouped).map((hostRows) => { + const { sshHostId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = hostRows[0]; + + const loginMappingGrouped = groupBy( + hostRows.filter((r) => r.loginUser), + (r) => r.loginUser + ); + + const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({ + loginUser, + allowedPrincipals: { + usernames: unique(entries.map((e) => e.username)).filter(Boolean) + } + })); + + return { + id: sshHostId, + hostname, + projectId, + userCertTtl, + hostCertTtl, + loginMappings, + userSshCaId, + hostSshCaId + }; + }); + } catch (error) { + throw new DatabaseError({ error, name: `${TableName.SshHost}: FindSshHostsWithLoginMappings` }); + } + }; + + const findSshHostByIdWithLoginMappings = async (sshHostId: string, tx?: Knex) => { + try { + const rows = await (tx || db.replicaNode())(TableName.SshHost) + .leftJoin(TableName.SshHostLoginUser, `${TableName.SshHost}.id`, `${TableName.SshHostLoginUser}.sshHostId`) + .leftJoin( + TableName.SshHostLoginUserMapping, + `${TableName.SshHostLoginUser}.id`, + `${TableName.SshHostLoginUserMapping}.sshHostLoginUserId` + ) + .leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`) + .where(`${TableName.SshHost}.id`, sshHostId) + .select( + db.ref("id").withSchema(TableName.SshHost).as("sshHostId"), + db.ref("projectId").withSchema(TableName.SshHost), + db.ref("hostname").withSchema(TableName.SshHost), + db.ref("userCertTtl").withSchema(TableName.SshHost), + db.ref("hostCertTtl").withSchema(TableName.SshHost), + db.ref("loginUser").withSchema(TableName.SshHostLoginUser), + db.ref("username").withSchema(TableName.Users), + db.ref("userId").withSchema(TableName.SshHostLoginUserMapping), + db.ref("userSshCaId").withSchema(TableName.SshHost), + db.ref("hostSshCaId").withSchema(TableName.SshHost) + ); + + if (rows.length === 0) return null; + + const { sshHostId: id, projectId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = rows[0]; + + const loginMappingGrouped = groupBy( + rows.filter((r) => r.loginUser), + (r) => r.loginUser + ); + + const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({ + loginUser, + allowedPrincipals: { + usernames: unique(entries.map((e) => e.username)).filter(Boolean) + } + })); + + return { + id, + projectId, + hostname, + userCertTtl, + hostCertTtl, + loginMappings, + userSshCaId, + hostSshCaId + }; + } catch (error) { + throw new DatabaseError({ error, name: `${TableName.SshHost}: FindSshHostByIdWithLoginMappings` }); + } + }; + + return { + ...sshHostOrm, + findSshHostsWithLoginMappings, + findUserAccessibleSshHosts, + findSshHostByIdWithLoginMappings + }; +}; diff --git a/backend/src/ee/services/ssh-host/ssh-host-login-user-mapping-dal.ts b/backend/src/ee/services/ssh-host/ssh-host-login-user-mapping-dal.ts new file mode 100644 index 000000000..0d9e8013b --- /dev/null +++ b/backend/src/ee/services/ssh-host/ssh-host-login-user-mapping-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TSshHostLoginUserMappingDALFactory = ReturnType; + +export const sshHostLoginUserMappingDALFactory = (db: TDbClient) => { + const sshHostLoginUserMappingOrm = ormify(db, TableName.SshHostLoginUserMapping); + return sshHostLoginUserMappingOrm; +}; diff --git a/backend/src/ee/services/ssh-host/ssh-host-schema.ts b/backend/src/ee/services/ssh-host/ssh-host-schema.ts new file mode 100644 index 000000000..4eeb90881 --- /dev/null +++ b/backend/src/ee/services/ssh-host/ssh-host-schema.ts @@ -0,0 +1,20 @@ +import { z } from "zod"; + +import { SshHostsSchema } from "@app/db/schemas"; + +export const sanitizedSshHost = SshHostsSchema.pick({ + id: true, + projectId: true, + hostname: true, + userCertTtl: true, + hostCertTtl: true, + userSshCaId: true, + hostSshCaId: true +}); + +export const loginMappingSchema = z.object({ + loginUser: z.string().trim(), + allowedPrincipals: z.object({ + usernames: z.array(z.string().trim()).transform((usernames) => Array.from(new Set(usernames))) + }) +}); diff --git a/backend/src/ee/services/ssh-host/ssh-host-service.ts b/backend/src/ee/services/ssh-host/ssh-host-service.ts new file mode 100644 index 000000000..69807431a --- /dev/null +++ b/backend/src/ee/services/ssh-host/ssh-host-service.ts @@ -0,0 +1,694 @@ +import { ForbiddenError, subject } from "@casl/ability"; + +import { ActionProjectType, ProjectType } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { ProjectPermissionSshHostActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; +import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal"; +import { TSshCertificateBodyDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-body-dal"; +import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal"; +import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types"; +import { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal"; +import { TSshHostLoginUserMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-user-mapping-dal"; +import { TSshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login-user-dal"; +import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +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 { TProjectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal"; +import { TUserDALFactory } from "@app/services/user/user-dal"; + +import { + convertActorToPrincipals, + createSshCert, + createSshKeyPair, + getSshPublicKey +} from "../ssh/ssh-certificate-authority-fns"; +import { SshCertType } from "../ssh/ssh-certificate-authority-types"; +import { + TCreateSshHostDTO, + TDeleteSshHostDTO, + TGetSshHostDTO, + TIssueSshHostHostCertDTO, + TIssueSshHostUserCertDTO, + TListSshHostsDTO, + TUpdateSshHostDTO +} from "./ssh-host-types"; + +type TSshHostServiceFactoryDep = { + userDAL: Pick; + projectDAL: Pick; + projectSshConfigDAL: Pick; + sshCertificateAuthorityDAL: Pick; + sshCertificateAuthoritySecretDAL: Pick; + sshCertificateDAL: Pick; + sshCertificateBodyDAL: Pick; + sshHostDAL: Pick< + TSshHostDALFactory, + | "transaction" + | "create" + | "findById" + | "updateById" + | "deleteById" + | "findOne" + | "findSshHostByIdWithLoginMappings" + | "findUserAccessibleSshHosts" + >; + sshHostLoginUserDAL: TSshHostLoginUserDALFactory; + sshHostLoginUserMappingDAL: TSshHostLoginUserMappingDALFactory; + permissionService: Pick; + kmsService: Pick; +}; + +export type TSshHostServiceFactory = ReturnType; + +export const sshHostServiceFactory = ({ + userDAL, + projectDAL, + projectSshConfigDAL, + sshCertificateAuthorityDAL, + sshCertificateAuthoritySecretDAL, + sshCertificateDAL, + sshCertificateBodyDAL, + sshHostDAL, + sshHostLoginUserMappingDAL, + sshHostLoginUserDAL, + permissionService, + kmsService +}: TSshHostServiceFactoryDep) => { + /** + * Return list of all SSH hosts that a user can issue user SSH certificates for + * (i.e. is able to access / connect to) across all SSH projects in the organization + */ + const listSshHosts = async ({ actorId, actorAuthMethod, actor, actorOrgId }: TListSshHostsDTO) => { + if (actor !== ActorType.USER) { + // (dangtony98): only support user for now + throw new BadRequestError({ message: `Actor type ${actor} not supported` }); + } + + const sshProjects = await projectDAL.find({ + orgId: actorOrgId, + type: ProjectType.SSH + }); + + const allowedHosts = []; + + for await (const project of sshProjects) { + try { + await permissionService.getProjectPermission({ + actor, + actorId, + projectId: project.id, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + const projectHosts = await sshHostDAL.findUserAccessibleSshHosts([project.id], actorId); + + allowedHosts.push(...projectHosts); + } catch { + // intentionally ignore projects where user lacks access + } + } + + return allowedHosts; + }; + + const createSshHost = async ({ + projectId, + hostname, + userCertTtl, + hostCertTtl, + loginMappings, + userSshCaId: requestedUserSshCaId, + hostSshCaId: requestedHostSshCaId, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TCreateSshHostDTO) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSshHostActions.Create, + subject(ProjectPermissionSub.SshHosts, { + hostname + }) + ); + + const resolveSshCaId = async ({ + requestedId, + fallbackId, + label + }: { + requestedId?: string; + fallbackId?: string | null; + label: "User" | "Host"; + }) => { + const finalId = requestedId ?? fallbackId; + if (!finalId) { + throw new BadRequestError({ message: `Missing ${label.toLowerCase()} SSH CA` }); + } + + const ca = await sshCertificateAuthorityDAL.findOne({ + id: finalId, + projectId + }); + + if (!ca) { + throw new BadRequestError({ + message: `${label} SSH CA with ID '${finalId}' not found in project '${projectId}'` + }); + } + + return ca.id; + }; + + const projectSshConfig = await projectSshConfigDAL.findOne({ projectId }); + + const userSshCaId = await resolveSshCaId({ + requestedId: requestedUserSshCaId, + fallbackId: projectSshConfig?.defaultUserSshCaId, + label: "User" + }); + + const hostSshCaId = await resolveSshCaId({ + requestedId: requestedHostSshCaId, + fallbackId: projectSshConfig?.defaultHostSshCaId, + label: "Host" + }); + + const newSshHost = await sshHostDAL.transaction(async (tx) => { + const host = await sshHostDAL.create( + { + projectId, + hostname, + userCertTtl, + hostCertTtl, + userSshCaId, + hostSshCaId + }, + tx + ); + + // (dangtony98): room to optimize + for await (const { loginUser, allowedPrincipals } of loginMappings) { + const sshHostLoginUser = await sshHostLoginUserDAL.create( + { + sshHostId: host.id, + loginUser + }, + tx + ); + + if (allowedPrincipals.usernames.length > 0) { + const users = await userDAL.find( + { + $in: { + username: allowedPrincipals.usernames + } + }, + { tx } + ); + + const foundUsernames = new Set(users.map((u) => u.username)); + + for (const uname of allowedPrincipals.usernames) { + if (!foundUsernames.has(uname)) { + throw new BadRequestError({ + message: `Invalid username: ${uname}` + }); + } + } + + for await (const user of users) { + // check that each user has access to the SSH project + await permissionService.getUserProjectPermission({ + userId: user.id, + projectId, + authMethod: actorAuthMethod, + userOrgId: actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + } + + await sshHostLoginUserMappingDAL.insertMany( + users.map((user) => ({ + sshHostLoginUserId: sshHostLoginUser.id, + userId: user.id + })), + tx + ); + } + } + + const newSshHostWithLoginMappings = await sshHostDAL.findSshHostByIdWithLoginMappings(host.id, tx); + if (!newSshHostWithLoginMappings) { + throw new NotFoundError({ message: `SSH host with ID '${host.id}' not found` }); + } + + return newSshHostWithLoginMappings; + }); + + return newSshHost; + }; + + const updateSshHost = async ({ + sshHostId, + hostname, + userCertTtl, + hostCertTtl, + loginMappings, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TUpdateSshHostDTO) => { + const host = await sshHostDAL.findById(sshHostId); + if (!host) throw new NotFoundError({ message: `SSH host with ID '${sshHostId}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: host.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSshHostActions.Edit, + subject(ProjectPermissionSub.SshHosts, { + hostname: host.hostname + }) + ); + + const updatedHost = await sshHostDAL.transaction(async (tx) => { + await sshHostDAL.updateById( + sshHostId, + { + hostname, + userCertTtl, + hostCertTtl + }, + tx + ); + + if (loginMappings) { + await sshHostLoginUserDAL.delete({ sshHostId: host.id }, tx); + if (loginMappings.length) { + for await (const { loginUser, allowedPrincipals } of loginMappings) { + const sshHostLoginUser = await sshHostLoginUserDAL.create( + { + sshHostId: host.id, + loginUser + }, + tx + ); + + if (allowedPrincipals.usernames.length > 0) { + const users = await userDAL.find( + { + $in: { + username: allowedPrincipals.usernames + } + }, + { tx } + ); + + const foundUsernames = new Set(users.map((u) => u.username)); + + for (const uname of allowedPrincipals.usernames) { + if (!foundUsernames.has(uname)) { + throw new BadRequestError({ + message: `Invalid username: ${uname}` + }); + } + } + + for await (const user of users) { + await permissionService.getUserProjectPermission({ + userId: user.id, + projectId: host.projectId, + authMethod: actorAuthMethod, + userOrgId: actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + } + + await sshHostLoginUserMappingDAL.insertMany( + users.map((user) => ({ + sshHostLoginUserId: sshHostLoginUser.id, + userId: user.id + })), + tx + ); + } + } + } + } + + const updatedHostWithLoginMappings = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId, tx); + if (!updatedHostWithLoginMappings) { + throw new NotFoundError({ message: `SSH host with ID '${sshHostId}' not found` }); + } + + return updatedHostWithLoginMappings; + }); + + return updatedHost; + }; + + const deleteSshHost = async ({ sshHostId, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteSshHostDTO) => { + const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId); + if (!host) throw new NotFoundError({ message: `SSH host with ID '${sshHostId}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: host.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSshHostActions.Delete, + subject(ProjectPermissionSub.SshHosts, { + hostname: host.hostname + }) + ); + + await sshHostDAL.deleteById(sshHostId); + + return host; + }; + + const getSshHost = async ({ sshHostId, actorId, actorAuthMethod, actor, actorOrgId }: TGetSshHostDTO) => { + const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId); + if (!host) { + throw new NotFoundError({ + message: `SSH host with ID ${sshHostId} not found` + }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: host.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSshHostActions.Read, + subject(ProjectPermissionSub.SshHosts, { + hostname: host.hostname + }) + ); + + return host; + }; + + /** + * Return SSH certificate and corresponding new SSH public-private key pair where + * SSH public key is signed using CA behind SSH certificate with name [templateName]. + * + * Note: Used for issuing SSH credentials as part of request against a specific SSH Host. + */ + const issueSshHostUserCert = async ({ + sshHostId, + loginUser, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TIssueSshHostUserCertDTO) => { + const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId); + if (!host) { + throw new NotFoundError({ + message: `SSH host with ID ${sshHostId} not found` + }); + } + + await permissionService.getProjectPermission({ + actor, + actorId, + projectId: host.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + const internalPrincipals = await convertActorToPrincipals({ + actor, + actorId, + userDAL + }); + + const mapping = host.loginMappings.find( + (m) => + m.loginUser === loginUser && + m.allowedPrincipals.usernames.some((allowed) => internalPrincipals.includes(allowed)) + ); + + if (!mapping) { + throw new UnauthorizedError({ + message: `You are not allowed to login as ${loginUser} on this host` + }); + } + + const keyId = `${actor}-${actorId}`; + + const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: host.userSshCaId }); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: host.projectId + }); + + const decryptedCaPrivateKey = secretManagerDecryptor({ + cipherTextBlob: sshCaSecret.encryptedPrivateKey + }); + + // (dangtony98): will support more algorithms in the future + const keyAlgorithm = SshCertKeyAlgorithm.ED25519; + const { publicKey, privateKey } = await createSshKeyPair(keyAlgorithm); + + // (dangtony98): include the loginUser as a principal on the issued certificate + const principals = [...internalPrincipals, loginUser]; + + const { serialNumber, signedPublicKey, ttl } = await createSshCert({ + caPrivateKey: decryptedCaPrivateKey.toString("utf8"), + clientPublicKey: publicKey, + keyId, + principals, + requestedTtl: host.userCertTtl, + certType: SshCertType.USER + }); + + const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: host.projectId + }); + + const encryptedCertificate = secretManagerEncryptor({ + plainText: Buffer.from(signedPublicKey, "utf8") + }).cipherTextBlob; + + await sshCertificateDAL.transaction(async (tx) => { + const cert = await sshCertificateDAL.create( + { + sshCaId: host.userSshCaId, + sshHostId: host.id, + serialNumber, + certType: SshCertType.USER, + principals, + keyId, + notBefore: new Date(), + notAfter: new Date(Date.now() + ttl * 1000) + }, + tx + ); + + await sshCertificateBodyDAL.create( + { + sshCertId: cert.id, + encryptedCertificate + }, + tx + ); + }); + + return { + host, + principals, + serialNumber, + signedPublicKey, + privateKey, + publicKey, + ttl, + keyAlgorithm + }; + }; + + const issueSshHostHostCert = async ({ + sshHostId, + publicKey, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TIssueSshHostHostCertDTO) => { + const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId); + if (!host) { + throw new NotFoundError({ + message: `SSH host with ID ${sshHostId} not found` + }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: host.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSshHostActions.IssueHostCert, + subject(ProjectPermissionSub.SshHosts, { + hostname: host.hostname + }) + ); + + const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: host.hostSshCaId }); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: host.projectId + }); + + const decryptedCaPrivateKey = secretManagerDecryptor({ + cipherTextBlob: sshCaSecret.encryptedPrivateKey + }); + + const principals = [host.hostname]; + const keyId = `host-${host.id}`; + + const { serialNumber, signedPublicKey, ttl } = await createSshCert({ + caPrivateKey: decryptedCaPrivateKey.toString("utf8"), + clientPublicKey: publicKey, + keyId, + principals, + requestedTtl: host.hostCertTtl, + certType: SshCertType.HOST + }); + + const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: host.projectId + }); + + const encryptedCertificate = secretManagerEncryptor({ + plainText: Buffer.from(signedPublicKey, "utf8") + }).cipherTextBlob; + + await sshCertificateDAL.transaction(async (tx) => { + const cert = await sshCertificateDAL.create( + { + sshCaId: host.hostSshCaId, + sshHostId: host.id, + serialNumber, + certType: SshCertType.HOST, + principals, + keyId, + notBefore: new Date(), + notAfter: new Date(Date.now() + ttl * 1000) + }, + tx + ); + + await sshCertificateBodyDAL.create( + { + sshCertId: cert.id, + encryptedCertificate + }, + tx + ); + }); + + return { host, principals, serialNumber, signedPublicKey }; + }; + + const getSshHostUserCaPk = async (sshHostId: string) => { + const host = await sshHostDAL.findById(sshHostId); + if (!host) { + throw new NotFoundError({ + message: `SSH host with ID ${sshHostId} not found` + }); + } + + const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: host.userSshCaId }); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: host.projectId + }); + + const decryptedCaPrivateKey = secretManagerDecryptor({ + cipherTextBlob: sshCaSecret.encryptedPrivateKey + }); + + const publicKey = await getSshPublicKey(decryptedCaPrivateKey.toString("utf-8")); + + return publicKey; + }; + + const getSshHostHostCaPk = async (sshHostId: string) => { + const host = await sshHostDAL.findById(sshHostId); + if (!host) { + throw new NotFoundError({ + message: `SSH host with ID ${sshHostId} not found` + }); + } + + const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: host.hostSshCaId }); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: host.projectId + }); + + const decryptedCaPrivateKey = secretManagerDecryptor({ + cipherTextBlob: sshCaSecret.encryptedPrivateKey + }); + + const publicKey = await getSshPublicKey(decryptedCaPrivateKey.toString("utf-8")); + + return publicKey; + }; + + return { + listSshHosts, + createSshHost, + updateSshHost, + deleteSshHost, + getSshHost, + issueSshHostUserCert, + issueSshHostHostCert, + getSshHostUserCaPk, + getSshHostHostCaPk + }; +}; diff --git a/backend/src/ee/services/ssh-host/ssh-host-types.ts b/backend/src/ee/services/ssh-host/ssh-host-types.ts new file mode 100644 index 000000000..0c7cb25e1 --- /dev/null +++ b/backend/src/ee/services/ssh-host/ssh-host-types.ts @@ -0,0 +1,48 @@ +import { TProjectPermission } from "@app/lib/types"; + +export type TListSshHostsDTO = Omit; + +export type TCreateSshHostDTO = { + hostname: string; + userCertTtl: string; + hostCertTtl: string; + loginMappings: { + loginUser: string; + allowedPrincipals: { + usernames: string[]; + }; + }[]; + userSshCaId?: string; + hostSshCaId?: string; +} & TProjectPermission; + +export type TUpdateSshHostDTO = { + sshHostId: string; + hostname?: string; + userCertTtl?: string; + hostCertTtl?: string; + loginMappings?: { + loginUser: string; + allowedPrincipals: { + usernames: string[]; + }; + }[]; +} & Omit; + +export type TGetSshHostDTO = { + sshHostId: string; +} & Omit; + +export type TDeleteSshHostDTO = { + sshHostId: string; +} & Omit; + +export type TIssueSshHostUserCertDTO = { + sshHostId: string; + loginUser: string; +} & Omit; + +export type TIssueSshHostHostCertDTO = { + sshHostId: string; + publicKey: string; +} & Omit; diff --git a/backend/src/ee/services/ssh-host/ssh-host-validators.ts b/backend/src/ee/services/ssh-host/ssh-host-validators.ts new file mode 100644 index 000000000..7b739b9cb --- /dev/null +++ b/backend/src/ee/services/ssh-host/ssh-host-validators.ts @@ -0,0 +1,15 @@ +import { isFQDN } from "@app/lib/validator/validate-url"; + +export const isValidHostname = (value: string): boolean => { + if (typeof value !== "string") return false; + if (value.length > 255) return false; + + // Only allow strict FQDNs, no wildcards or IPs + return isFQDN(value, { + require_tld: true, + allow_underscores: false, + allow_trailing_dot: false, + allow_numeric_tld: true, + allow_wildcard: false + }); +}; diff --git a/backend/src/ee/services/ssh-host/ssh-login-user-dal.ts b/backend/src/ee/services/ssh-host/ssh-login-user-dal.ts new file mode 100644 index 000000000..88a9bf59a --- /dev/null +++ b/backend/src/ee/services/ssh-host/ssh-login-user-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TSshHostLoginUserDALFactory = ReturnType; + +export const sshHostLoginUserDALFactory = (db: TDbClient) => { + const sshHostLoginUserOrm = ormify(db, TableName.SshHostLoginUser); + return sshHostLoginUserOrm; +}; diff --git a/backend/src/ee/services/ssh/ssh-certificate-authority-dal.ts b/backend/src/ee/services/ssh/ssh-certificate-authority-dal.ts new file mode 100644 index 000000000..c906efa91 --- /dev/null +++ b/backend/src/ee/services/ssh/ssh-certificate-authority-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TSshCertificateAuthorityDALFactory = ReturnType; + +export const sshCertificateAuthorityDALFactory = (db: TDbClient) => { + const sshCaOrm = ormify(db, TableName.SshCertificateAuthority); + return sshCaOrm; +}; diff --git a/backend/src/ee/services/ssh/ssh-certificate-authority-fns.ts b/backend/src/ee/services/ssh/ssh-certificate-authority-fns.ts new file mode 100644 index 000000000..60c966fcd --- /dev/null +++ b/backend/src/ee/services/ssh/ssh-certificate-authority-fns.ts @@ -0,0 +1,606 @@ +import { execFile } from "child_process"; +import crypto from "crypto"; +import { promises as fs } from "fs"; +import { Knex } from "knex"; +import os from "os"; +import path from "path"; +import RE2 from "re2"; +import { promisify } from "util"; + +import { TSshCertificateTemplates } from "@app/db/schemas"; +import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types"; +import { BadRequestError } from "@app/lib/errors"; +import { ms } from "@app/lib/ms"; +import { CharacterType, characterValidator } from "@app/lib/validator/validate-string"; +import { ActorType } from "@app/services/auth/auth-type"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { + isValidHostPattern, + isValidUserPattern +} from "../ssh-certificate-template/ssh-certificate-template-validators"; +import { + SshCaKeySource, + SshCaStatus, + SshCertType, + TConvertActorToPrincipalsDTO, + TCreateSshCaHelperDTO, + TCreateSshCertDTO +} from "./ssh-certificate-authority-types"; + +const execFileAsync = promisify(execFile); + +const EXEC_TIMEOUT_MS = 10000; // 10 seconds +/* eslint-disable no-bitwise */ +export const createSshCertSerialNumber = () => { + const randomBytes = crypto.randomBytes(8); // 8 bytes = 64 bits + randomBytes[0] &= 0x7f; // Ensure the most significant bit is 0 (to stay within unsigned range) + return BigInt(`0x${randomBytes.toString("hex")}`).toString(10); // Convert to decimal +}; + +/** + * Return a pair of SSH CA keys based on the specified key algorithm [keyAlgorithm]. + * We use this function because the key format generated by `ssh-keygen` is unique. + */ +export const createSshKeyPair = async (keyAlgorithm: SshCertKeyAlgorithm) => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-key-")); + const privateKeyFile = path.join(tempDir, "id_key"); + const publicKeyFile = `${privateKeyFile}.pub`; + + let keyType: string; + let keyBits: string | null; + + switch (keyAlgorithm) { + case SshCertKeyAlgorithm.RSA_2048: + keyType = "rsa"; + keyBits = "2048"; + break; + case SshCertKeyAlgorithm.RSA_4096: + keyType = "rsa"; + keyBits = "4096"; + break; + case SshCertKeyAlgorithm.ECDSA_P256: + keyType = "ecdsa"; + keyBits = "256"; + break; + case SshCertKeyAlgorithm.ECDSA_P384: + keyType = "ecdsa"; + keyBits = "384"; + break; + case SshCertKeyAlgorithm.ED25519: + keyType = "ed25519"; + keyBits = null; + break; + default: + throw new BadRequestError({ + message: "Failed to produce SSH CA key pair generation command due to unrecognized key algorithm" + }); + } + + try { + const args = ["-t", keyType]; + if (keyBits !== null) { + args.push("-b", keyBits); + } + args.push("-f", privateKeyFile, "-N", ""); + + // Generate the SSH key pair + // The "-N ''" sets an empty passphrase + // The keys are created in the temporary directory + await execFileAsync("ssh-keygen", args, { + timeout: EXEC_TIMEOUT_MS + }); + + // Read the generated keys + const publicKey = await fs.readFile(publicKeyFile, "utf8"); + const privateKey = await fs.readFile(privateKeyFile, "utf8"); + + return { publicKey, privateKey }; + } finally { + // Cleanup the temporary directory and all its contents + await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {}); + } +}; + +/** + * Return the SSH public key for the given SSH private key. + */ +export const getSshPublicKey = async (privateKey: string) => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-key-")); + const privateKeyFile = path.join(tempDir, "id_key"); + try { + await fs.writeFile(privateKeyFile, privateKey, { mode: 0o600 }); + + // Run ssh-keygen to extract the public key + const { stdout } = await execFileAsync("ssh-keygen", ["-y", "-f", privateKeyFile], { + encoding: "utf8", + timeout: EXEC_TIMEOUT_MS + }); + return stdout.trim(); + } finally { + // Ensure that files and the temporary directory are cleaned up + await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {}); + } +}; + +/** + * Validate the requested SSH certificate type based on the SSH certificate template configuration. + */ +export const validateSshCertificateType = (template: TSshCertificateTemplates, certType: SshCertType) => { + if (!template.allowUserCertificates && certType === SshCertType.USER) { + throw new BadRequestError({ message: "Failed to validate user certificate type due to template restriction" }); + } + + if (!template.allowHostCertificates && certType === SshCertType.HOST) { + throw new BadRequestError({ message: "Failed to validate host certificate type due to template restriction" }); + } +}; + +/** + * Validate the requested SSH certificate principals based on the SSH certificate template configuration. + */ +export const validateSshCertificatePrincipals = ( + certType: SshCertType, + template: TSshCertificateTemplates, + principals: string[] +) => { + /** + * Validate and sanitize a principal string + */ + const validatePrincipal = (principal: string) => { + const sanitized = principal.trim(); + + // basic checks for empty or control characters + if (sanitized.length === 0) { + throw new BadRequestError({ + message: "Principal cannot be an empty string." + }); + } + + if (new RE2(/\r|\n|\t|\0/).test(sanitized)) { + throw new BadRequestError({ + message: `Principal '${sanitized}' contains invalid whitespace or control characters.` + }); + } + + // disallow whitespace anywhere + if (new RE2(/\s/).test(sanitized)) { + throw new BadRequestError({ + message: `Principal '${sanitized}' cannot contain whitespace.` + }); + } + + // restrict allowed characters to letters, digits, dot, underscore, and hyphen + if ( + !characterValidator([ + CharacterType.AlphaNumeric, + CharacterType.Period, + CharacterType.Underscore, + CharacterType.Hyphen + ])(sanitized) + ) { + throw new BadRequestError({ + message: `Principal '${sanitized}' contains invalid characters. Allowed: alphanumeric, '.', '_', '-'.` + }); + } + + // disallow leading hyphen to avoid potential argument-like inputs + if (sanitized.startsWith("-")) { + throw new BadRequestError({ + message: `Principal '${sanitized}' cannot start with a hyphen.` + }); + } + + // length restriction (adjust as needed) + if (sanitized.length > 64) { + throw new BadRequestError({ + message: `Principal '${sanitized}' is too long.` + }); + } + + return sanitized; + }; + + // Sanitize and validate all principals using the helper + const sanitizedPrincipals = principals.map(validatePrincipal); + + switch (certType) { + case SshCertType.USER: { + if (template.allowedUsers.length === 0) { + throw new BadRequestError({ + message: "No allowed users are configured in the SSH certificate template." + }); + } + + const allowsAllUsers = template.allowedUsers.includes("*") ?? false; + + sanitizedPrincipals.forEach((principal) => { + if (principal === "*") { + throw new BadRequestError({ + message: `Principal '*' is not allowed for user certificates.` + }); + } + if (allowsAllUsers && !isValidUserPattern(principal)) { + throw new BadRequestError({ + message: `Principal '${principal}' does not match a valid user pattern.` + }); + } + if (!allowsAllUsers && !template.allowedUsers.includes(principal)) { + throw new BadRequestError({ + message: `Principal '${principal}' is not in the list of allowed users.` + }); + } + }); + break; + } + case SshCertType.HOST: { + if (template.allowedHosts.length === 0) { + throw new BadRequestError({ + message: "No allowed hosts are configured in the SSH certificate template." + }); + } + + const allowsAllHosts = template.allowedHosts.includes("*") ?? false; + + sanitizedPrincipals.forEach((principal) => { + if (principal.includes("*")) { + throw new BadRequestError({ + message: `Principal '${principal}' with wildcards is not allowed for host certificates.` + }); + } + if (allowsAllHosts && !isValidHostPattern(principal)) { + throw new BadRequestError({ + message: `Principal '${principal}' does not match a valid host pattern.` + }); + } + + if ( + !allowsAllHosts && + !template.allowedHosts.some((allowedHost) => { + if (allowedHost.startsWith("*.")) { + const baseDomain = allowedHost.slice(2); // Remove the leading "*." + return principal.endsWith(`.${baseDomain}`); + } + return principal === allowedHost; + }) + ) { + throw new BadRequestError({ + message: `Principal '${principal}' is not in the list of allowed hosts or domains.` + }); + } + }); + break; + } + default: + throw new BadRequestError({ + message: "Failed to validate SSH certificate principals due to unrecognized requested certificate type" + }); + } +}; + +/** + * Validate the requested SSH certificate TTL based on the SSH certificate template configuration. + */ +export const validateSshCertificateTtl = (template: TSshCertificateTemplates, ttl?: string) => { + if (!ttl) { + // use default template ttl + return Math.ceil(ms(template.ttl) / 1000); + } + + if (ms(ttl) > ms(template.maxTTL)) { + throw new BadRequestError({ + message: "Failed TTL validation due to TTL being greater than configured max TTL on template" + }); + } + + return Math.ceil(ms(ttl) / 1000); +}; + +/** + * Validate the requested SSH certificate key ID to ensure + * that it only contains alphanumeric characters with no spaces. + */ +export const validateSshCertificateKeyId = (keyId: string) => { + const regex = characterValidator([ + CharacterType.AlphaNumeric, + CharacterType.Hyphen, + CharacterType.Colon, + CharacterType.Period + ]); + if (!regex(keyId)) { + throw new BadRequestError({ + message: + "Failed to validate Key ID because it can only contain alphanumeric characters and hyphens, with no spaces." + }); + } + + if (keyId.length > 50) { + throw new BadRequestError({ + message: "keyId can only be up to 50 characters long." + }); + } +}; + +/** + * Validate the format of the SSH public key + */ +const validateSshPublicKey = async (publicKey: string) => { + const validPrefixes = ["ssh-rsa", "ssh-ed25519", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]; + const startsWithValidPrefix = validPrefixes.some((prefix) => publicKey.startsWith(`${prefix} `)); + if (!startsWithValidPrefix) { + throw new BadRequestError({ message: "Failed to validate SSH public key format: unsupported key type." }); + } + + // write the key to a temp file and run `ssh-keygen -l -f` + // check to see if OpenSSH can read/interpret the public key + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-pubkey-")); + const pubKeyFile = path.join(tempDir, "key.pub"); + + try { + await fs.writeFile(pubKeyFile, publicKey, { mode: 0o600 }); + await execFileAsync("ssh-keygen", ["-l", "-f", pubKeyFile], { timeout: EXEC_TIMEOUT_MS }); + } catch (error) { + throw new BadRequestError({ + message: "Failed to validate SSH public key format: could not be parsed." + }); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {}); + } +}; + +export const getKeyAlgorithmFromFingerprintOutput = (output: string): SshCertKeyAlgorithm | undefined => { + const parts = output.trim().split(" "); + const bitsInt = parseInt(parts[0], 10); + const keyTypeRaw = parts.at(-1)?.replace(/[()]/g, ""); // remove surrounding parentheses + + if (keyTypeRaw === "RSA") { + return bitsInt === 2048 ? SshCertKeyAlgorithm.RSA_2048 : SshCertKeyAlgorithm.RSA_4096; + } + + if (keyTypeRaw === "ECDSA") { + return bitsInt === 256 ? SshCertKeyAlgorithm.ECDSA_P256 : SshCertKeyAlgorithm.ECDSA_P384; + } + + if (keyTypeRaw === "ED25519") { + return SshCertKeyAlgorithm.ED25519; + } + + return undefined; +}; + +export const normalizeSshPrivateKey = (raw: string): string => { + return `${raw + .replace(/\r\n/g, "\n") // Windows CRLF → LF + .replace(/\r/g, "\n") // Old Mac CR → LF + .replace(/\\n/g, "\n") // Double-escaped \n + .trim()}\n`; +}; + +/** + * Validate the format of the SSH private key + * + * Returns the SSH public key corresponding to the private key + * and the key algorithm categorization. + */ +export const validateSshPrivateKey = async (privateKey: string) => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-privkey-")); + const privateKeyFile = path.join(tempDir, "id_key"); + + try { + await fs.writeFile(privateKeyFile, privateKey, { + encoding: "utf8", + mode: 0o600 + }); + + // This will fail if the private key is malformed or unreadable + const { stdout: publicKey } = await execFileAsync("ssh-keygen", ["-y", "-f", privateKeyFile], { + timeout: EXEC_TIMEOUT_MS + }); + + const { stdout: fingerprint } = await execFileAsync("ssh-keygen", ["-lf", privateKeyFile]); + const keyAlgorithm = getKeyAlgorithmFromFingerprintOutput(fingerprint); + + if (!keyAlgorithm) { + throw new BadRequestError({ + message: "Failed to validate SSH private key format: The key algorithm is not supported." + }); + } + + return { + publicKey, + keyAlgorithm + }; + } catch (err) { + throw new BadRequestError({ + message: "Failed to validate SSH private key format: could not be parsed." + }); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {}); + } +}; + +/** + * Validate that the provided public and private keys are valid and constitute + * a matching SSH key pair. + */ +export const validateExternalSshCaKeyPair = async (publicKey: string, privateKey: string) => { + await validateSshPublicKey(publicKey); + + const { publicKey: derivedPublicKey, keyAlgorithm } = await validateSshPrivateKey(privateKey); + + if (publicKey.trim() !== derivedPublicKey.trim()) { + throw new BadRequestError({ + message: + "Failed to validate matching SSH key pair: The provided public key does not match the public key derived from the private key." + }); + } + + return keyAlgorithm; +}; + +/** + * Create an SSH certificate for a user or host. + */ +export const createSshCert = async ({ + template, + caPrivateKey, + clientPublicKey, + keyId, + principals, + requestedTtl, // in ms lib format + certType +}: TCreateSshCertDTO) => { + let ttl: number | undefined; + + if (!template && requestedTtl) { + const parsedTtl = Math.ceil(ms(requestedTtl) / 1000); + if (parsedTtl > 0) ttl = parsedTtl; + } + + if (template) { + // validate if the requested [certType] is allowed under the template configuration + validateSshCertificateType(template, certType); + + // validate if the requested [principals] are valid for the given [certType] under the template configuration + validateSshCertificatePrincipals(certType, template, principals); + + // validate if the requested TTL is valid under the template configuration + ttl = validateSshCertificateTtl(template, requestedTtl); + } + + if (!ttl) { + throw new BadRequestError({ + message: "Failed to create SSH certificate due to missing TTL" + }); + } + + validateSshCertificateKeyId(keyId); + await validateSshPublicKey(clientPublicKey); + + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-cert-")); + + const publicKeyFile = path.join(tempDir, "user_key.pub"); + const privateKeyFile = path.join(tempDir, "ca_key"); + const signedPublicKeyFile = path.join(tempDir, "user_key-cert.pub"); + + const serialNumber = createSshCertSerialNumber(); + + // Build `ssh-keygen` arguments for signing + // Using an array avoids shell injection issues + const sshKeygenArgs = [ + certType === "host" ? "-h" : null, // host certificate if needed + "-s", + privateKeyFile, // path to SSH CA private key + "-I", + keyId, // identity (key ID) + "-n", + principals.join(","), // principals + "-V", + `+${ttl}s`, // validity (TTL in seconds) + "-z", + serialNumber, // serial number + publicKeyFile // public key file to sign + ].filter(Boolean) as string[]; + + try { + // Write public and private keys to the temp directory + await fs.writeFile(publicKeyFile, clientPublicKey, { mode: 0o600 }); + await fs.writeFile(privateKeyFile, caPrivateKey, { mode: 0o600 }); + + // Execute the signing process + await execFileAsync("ssh-keygen", sshKeygenArgs, { encoding: "utf8", timeout: EXEC_TIMEOUT_MS }); + + // Read the signed public key from the generated cert file + const signedPublicKey = await fs.readFile(signedPublicKeyFile, "utf8"); + + return { serialNumber, signedPublicKey, ttl }; + } finally { + // Cleanup the temporary directory and all its contents + await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {}); + } +}; + +export const createSshCaHelper = async ({ + projectId, + friendlyName, + keyAlgorithm: requestedKeyAlgorithm, + keySource, + externalPk, + externalSk, + sshCertificateAuthorityDAL, + sshCertificateAuthoritySecretDAL, + kmsService, + tx: outerTx +}: TCreateSshCaHelperDTO) => { + // Function to handle the actual creation logic + const processCreation = async (tx: Knex) => { + let publicKey: string; + let privateKey: string; + let keyAlgorithm: SshCertKeyAlgorithm = requestedKeyAlgorithm; + if (keySource === SshCaKeySource.INTERNAL) { + // generate SSH CA key pair internally + ({ publicKey, privateKey } = await createSshKeyPair(requestedKeyAlgorithm)); + } else { + // use external SSH CA key pair + if (!externalPk || !externalSk) { + throw new BadRequestError({ + message: "Public and private keys are required when key source is external" + }); + } + publicKey = externalPk; + privateKey = externalSk; + keyAlgorithm = await validateExternalSshCaKeyPair(publicKey, privateKey); + } + const ca = await sshCertificateAuthorityDAL.create( + { + projectId, + friendlyName, + status: SshCaStatus.ACTIVE, + keyAlgorithm, + keySource + }, + tx + ); + const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey( + { + type: KmsDataKey.SecretManager, + projectId + }, + tx + ); + await sshCertificateAuthoritySecretDAL.create( + { + sshCaId: ca.id, + encryptedPrivateKey: secretManagerEncryptor({ plainText: Buffer.from(privateKey, "utf8") }).cipherTextBlob + }, + tx + ); + return { ...ca, publicKey }; + }; + + if (outerTx) { + return processCreation(outerTx); + } + + return sshCertificateAuthorityDAL.transaction(processCreation); +}; + +/** + * Convert an actor to a list of principals to be included in an SSH certificate. + * + * (dangtony98): This function is only supported for user actors at the moment and returns + * only the email of the associated user. In the future, we will consider other + * actor types and attributes such as group membership slugs and/or metadata to be + * included in the list of principals. + */ +export const convertActorToPrincipals = async ({ userDAL, actor, actorId }: TConvertActorToPrincipalsDTO) => { + if (actor !== ActorType.USER) { + throw new BadRequestError({ + message: "Failed to convert actor to principals due to unsupported actor type" + }); + } + + const user = await userDAL.findById(actorId); + + return [user.username]; +}; diff --git a/backend/src/ee/services/ssh/ssh-certificate-authority-schema.ts b/backend/src/ee/services/ssh/ssh-certificate-authority-schema.ts new file mode 100644 index 000000000..af66e83ca --- /dev/null +++ b/backend/src/ee/services/ssh/ssh-certificate-authority-schema.ts @@ -0,0 +1,10 @@ +import { SshCertificateAuthoritiesSchema } from "@app/db/schemas"; + +export const sanitizedSshCa = SshCertificateAuthoritiesSchema.pick({ + id: true, + projectId: true, + friendlyName: true, + status: true, + keyAlgorithm: true, + keySource: true +}); diff --git a/backend/src/ee/services/ssh/ssh-certificate-authority-secret-dal.ts b/backend/src/ee/services/ssh/ssh-certificate-authority-secret-dal.ts new file mode 100644 index 000000000..9423a0ff2 --- /dev/null +++ b/backend/src/ee/services/ssh/ssh-certificate-authority-secret-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TSshCertificateAuthoritySecretDALFactory = ReturnType; + +export const sshCertificateAuthoritySecretDALFactory = (db: TDbClient) => { + const sshCaSecretOrm = ormify(db, TableName.SshCertificateAuthoritySecret); + return sshCaSecretOrm; +}; diff --git a/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts b/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts new file mode 100644 index 000000000..312b7966b --- /dev/null +++ b/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts @@ -0,0 +1,509 @@ +import { ForbiddenError } from "@casl/ability"; + +import { ActionProjectType } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; +import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal"; +import { TSshCertificateBodyDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-body-dal"; +import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal"; +import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { SshCertTemplateStatus } from "../ssh-certificate-template/ssh-certificate-template-types"; +import { createSshCaHelper, createSshCert, createSshKeyPair, getSshPublicKey } from "./ssh-certificate-authority-fns"; +import { + SshCaStatus, + TCreateSshCaDTO, + TDeleteSshCaDTO, + TGetSshCaCertificateTemplatesDTO, + TGetSshCaDTO, + TGetSshCaPublicKeyDTO, + TIssueSshCredsDTO, + TSignSshKeyDTO, + TUpdateSshCaDTO +} from "./ssh-certificate-authority-types"; + +type TSshCertificateAuthorityServiceFactoryDep = { + sshCertificateAuthorityDAL: Pick< + TSshCertificateAuthorityDALFactory, + "transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne" + >; + sshCertificateAuthoritySecretDAL: Pick; + sshCertificateTemplateDAL: Pick; + sshCertificateDAL: Pick; + sshCertificateBodyDAL: Pick; + kmsService: Pick< + TKmsServiceFactory, + "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey" | "getOrgKmsKeyId" | "createCipherPairWithDataKey" + >; + permissionService: Pick; +}; + +export type TSshCertificateAuthorityServiceFactory = ReturnType; + +export const sshCertificateAuthorityServiceFactory = ({ + sshCertificateAuthorityDAL, + sshCertificateAuthoritySecretDAL, + sshCertificateTemplateDAL, + sshCertificateDAL, + sshCertificateBodyDAL, + kmsService, + permissionService +}: TSshCertificateAuthorityServiceFactoryDep) => { + /** + * Generates a new SSH CA + */ + const createSshCa = async ({ + projectId, + friendlyName, + keyAlgorithm: requestedKeyAlgorithm, + publicKey: externalPk, + privateKey: externalSk, + keySource, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TCreateSshCaDTO) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.SshCertificateAuthorities + ); + + const newCa = await createSshCaHelper({ + projectId, + friendlyName, + keyAlgorithm: requestedKeyAlgorithm, + keySource, + externalPk, + externalSk, + sshCertificateAuthorityDAL, + sshCertificateAuthoritySecretDAL, + kmsService + }); + + return newCa; + }; + + /** + * Return SSH CA with id [caId] + */ + const getSshCaById = async ({ caId, actor, actorId, actorAuthMethod, actorOrgId }: TGetSshCaDTO) => { + const ca = await sshCertificateAuthorityDAL.findById(caId); + if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: ca.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.SshCertificateAuthorities + ); + + const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: ca.id }); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: ca.projectId + }); + + const decryptedCaPrivateKey = secretManagerDecryptor({ + cipherTextBlob: sshCaSecret.encryptedPrivateKey + }); + + const publicKey = await getSshPublicKey(decryptedCaPrivateKey.toString("utf-8")); + + return { ...ca, publicKey }; + }; + + /** + * Return public key of SSH CA with id [caId] + */ + const getSshCaPublicKey = async ({ caId }: TGetSshCaPublicKeyDTO) => { + const ca = await sshCertificateAuthorityDAL.findById(caId); + if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` }); + + const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: ca.id }); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: ca.projectId + }); + + const decryptedCaPrivateKey = secretManagerDecryptor({ + cipherTextBlob: sshCaSecret.encryptedPrivateKey + }); + + const publicKey = await getSshPublicKey(decryptedCaPrivateKey.toString("utf-8")); + + return publicKey; + }; + + /** + * Update SSH CA with id [caId] + * Note: Used to enable/disable CA + */ + const updateSshCaById = async ({ + caId, + friendlyName, + status, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TUpdateSshCaDTO) => { + const ca = await sshCertificateAuthorityDAL.findById(caId); + if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: ca.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.SshCertificateAuthorities + ); + + const updatedCa = await sshCertificateAuthorityDAL.updateById(caId, { friendlyName, status }); + + const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: ca.id }); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: ca.projectId + }); + + const decryptedCaPrivateKey = secretManagerDecryptor({ + cipherTextBlob: sshCaSecret.encryptedPrivateKey + }); + + const publicKey = await getSshPublicKey(decryptedCaPrivateKey.toString("utf-8")); + + return { ...updatedCa, publicKey }; + }; + + /** + * Delete SSH CA with id [caId] + */ + const deleteSshCaById = async ({ caId, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteSshCaDTO) => { + const ca = await sshCertificateAuthorityDAL.findById(caId); + if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: ca.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.SshCertificateAuthorities + ); + + const deletedCa = await sshCertificateAuthorityDAL.deleteById(caId); + + return deletedCa; + }; + + /** + * Return SSH certificate and corresponding new SSH public-private key pair where + * SSH public key is signed using CA behind SSH certificate with name [templateName]. + */ + const issueSshCreds = async ({ + certificateTemplateId, + keyAlgorithm, + certType, + principals, + ttl: requestedTtl, + keyId: requestedKeyId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TIssueSshCredsDTO) => { + const sshCertificateTemplate = await sshCertificateTemplateDAL.getById(certificateTemplateId); + if (!sshCertificateTemplate) { + throw new NotFoundError({ + message: "No SSH certificate template found with specified name" + }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: sshCertificateTemplate.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.SshCertificates + ); + + if (sshCertificateTemplate.caStatus === SshCaStatus.DISABLED) { + throw new BadRequestError({ + message: "SSH CA is disabled" + }); + } + + if (sshCertificateTemplate.status === SshCertTemplateStatus.DISABLED) { + throw new BadRequestError({ + message: "SSH certificate template is disabled" + }); + } + + // set [keyId] depending on if [allowCustomKeyIds] is true or false + const keyId = sshCertificateTemplate.allowCustomKeyIds + ? requestedKeyId ?? `${actor}-${actorId}` + : `${actor}-${actorId}`; + + const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: sshCertificateTemplate.sshCaId }); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: sshCertificateTemplate.projectId + }); + + const decryptedCaPrivateKey = secretManagerDecryptor({ + cipherTextBlob: sshCaSecret.encryptedPrivateKey + }); + + // create user key pair + const { publicKey, privateKey } = await createSshKeyPair(keyAlgorithm); + + const { serialNumber, signedPublicKey, ttl } = await createSshCert({ + template: sshCertificateTemplate, + caPrivateKey: decryptedCaPrivateKey.toString("utf8"), + clientPublicKey: publicKey, + keyId, + principals, + requestedTtl, + certType + }); + + const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: sshCertificateTemplate.projectId + }); + + const encryptedCertificate = secretManagerEncryptor({ + plainText: Buffer.from(signedPublicKey, "utf8") + }).cipherTextBlob; + + await sshCertificateDAL.transaction(async (tx) => { + const cert = await sshCertificateDAL.create( + { + sshCaId: sshCertificateTemplate.sshCaId, + sshCertificateTemplateId: sshCertificateTemplate.id, + serialNumber, + certType, + principals, + keyId, + notBefore: new Date(), + notAfter: new Date(Date.now() + ttl * 1000) + }, + tx + ); + + await sshCertificateBodyDAL.create( + { + sshCertId: cert.id, + encryptedCertificate + }, + tx + ); + }); + + return { + serialNumber, + signedPublicKey, + privateKey, + publicKey, + certificateTemplate: sshCertificateTemplate, + ttl, + keyId + }; + }; + + /** + * Return SSH certificate by signing SSH public key [publicKey] + * using CA behind SSH certificate template with name [templateName] + */ + const signSshKey = async ({ + certificateTemplateId, + publicKey, + certType, + principals, + ttl: requestedTtl, + keyId: requestedKeyId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TSignSshKeyDTO) => { + const sshCertificateTemplate = await sshCertificateTemplateDAL.getById(certificateTemplateId); + if (!sshCertificateTemplate) { + throw new NotFoundError({ + message: "No SSH certificate template found with specified name" + }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: sshCertificateTemplate.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.SshCertificates + ); + + if (sshCertificateTemplate.caStatus === SshCaStatus.DISABLED) { + throw new BadRequestError({ + message: "SSH CA is disabled" + }); + } + + if (sshCertificateTemplate.status === SshCertTemplateStatus.DISABLED) { + throw new BadRequestError({ + message: "SSH certificate template is disabled" + }); + } + + // set [keyId] depending on if [allowCustomKeyIds] is true or false + const keyId = sshCertificateTemplate.allowCustomKeyIds + ? requestedKeyId ?? `${actor}-${actorId}` + : `${actor}-${actorId}`; + + const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: sshCertificateTemplate.sshCaId }); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: sshCertificateTemplate.projectId + }); + + const decryptedCaPrivateKey = secretManagerDecryptor({ + cipherTextBlob: sshCaSecret.encryptedPrivateKey + }); + + const { serialNumber, signedPublicKey, ttl } = await createSshCert({ + template: sshCertificateTemplate, + caPrivateKey: decryptedCaPrivateKey.toString("utf8"), + clientPublicKey: publicKey, + keyId, + principals, + requestedTtl, + certType + }); + + const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: sshCertificateTemplate.projectId + }); + + const encryptedCertificate = secretManagerEncryptor({ + plainText: Buffer.from(signedPublicKey, "utf8") + }).cipherTextBlob; + + await sshCertificateDAL.transaction(async (tx) => { + const cert = await sshCertificateDAL.create( + { + sshCaId: sshCertificateTemplate.sshCaId, + sshCertificateTemplateId: sshCertificateTemplate.id, + serialNumber, + certType, + principals, + keyId, + notBefore: new Date(), + notAfter: new Date(Date.now() + ttl * 1000) + }, + tx + ); + + await sshCertificateBodyDAL.create( + { + sshCertId: cert.id, + encryptedCertificate + }, + tx + ); + }); + + return { serialNumber, signedPublicKey, certificateTemplate: sshCertificateTemplate, ttl, keyId }; + }; + + const getSshCaCertificateTemplates = async ({ + caId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TGetSshCaCertificateTemplatesDTO) => { + const ca = await sshCertificateAuthorityDAL.findById(caId); + if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: ca.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.SshCertificateTemplates + ); + + const certificateTemplates = await sshCertificateTemplateDAL.find({ sshCaId: caId }); + + return { + certificateTemplates, + ca + }; + }; + + return { + issueSshCreds, + signSshKey, + createSshCa, + getSshCaById, + getSshCaPublicKey, + updateSshCaById, + deleteSshCaById, + getSshCaCertificateTemplates + }; +}; diff --git a/backend/src/ee/services/ssh/ssh-certificate-authority-types.ts b/backend/src/ee/services/ssh/ssh-certificate-authority-types.ts new file mode 100644 index 000000000..d433bd5ad --- /dev/null +++ b/backend/src/ee/services/ssh/ssh-certificate-authority-types.ts @@ -0,0 +1,102 @@ +import { Knex } from "knex"; + +import { TSshCertificateTemplates } from "@app/db/schemas"; +import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; +import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal"; +import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types"; +import { TProjectPermission } from "@app/lib/types"; +import { ActorType } from "@app/services/auth/auth-type"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TUserDALFactory } from "@app/services/user/user-dal"; + +export enum SshCaStatus { + ACTIVE = "active", + DISABLED = "disabled" +} + +export enum SshCaKeySource { + INTERNAL = "internal", + EXTERNAL = "external" +} + +export enum SshCertType { + USER = "user", + HOST = "host" +} + +export type TCreateSshCaDTO = { + friendlyName: string; + keyAlgorithm: SshCertKeyAlgorithm; + publicKey?: string; + privateKey?: string; + keySource: SshCaKeySource; +} & TProjectPermission; + +export type TCreateSshCaHelperDTO = { + projectId: string; + friendlyName: string; + keyAlgorithm: SshCertKeyAlgorithm; + keySource: SshCaKeySource; + externalPk?: string; + externalSk?: string; + sshCertificateAuthorityDAL: Pick; + sshCertificateAuthoritySecretDAL: Pick; + kmsService: Pick; + tx?: Knex; +}; + +export type TGetSshCaDTO = { + caId: string; +} & Omit; + +export type TGetSshCaPublicKeyDTO = { + caId: string; +}; + +export type TUpdateSshCaDTO = { + caId: string; + friendlyName?: string; + status?: SshCaStatus; +} & Omit; + +export type TDeleteSshCaDTO = { + caId: string; +} & Omit; + +export type TIssueSshCredsDTO = { + certificateTemplateId: string; + keyAlgorithm: SshCertKeyAlgorithm; + certType: SshCertType; + principals: string[]; + ttl?: string; + keyId?: string; +} & Omit; + +export type TSignSshKeyDTO = { + certificateTemplateId: string; + publicKey: string; + certType: SshCertType; + principals: string[]; + ttl?: string; + keyId?: string; +} & Omit; + +export type TGetSshCaCertificateTemplatesDTO = { + caId: string; +} & Omit; + +export type TCreateSshCertDTO = { + template?: TSshCertificateTemplates; + caPrivateKey: string; + clientPublicKey: string; + keyId: string; + principals: string[]; + requestedTtl?: string; + certType: SshCertType; +}; + +export type TConvertActorToPrincipalsDTO = { + actor: ActorType; + actorId: string; + userDAL: Pick; +}; diff --git a/backend/src/ee/services/trusted-ip/trusted-ip-service.ts b/backend/src/ee/services/trusted-ip/trusted-ip-service.ts index ecd2b3070..c407bdc82 100644 --- a/backend/src/ee/services/trusted-ip/trusted-ip-service.ts +++ b/backend/src/ee/services/trusted-ip/trusted-ip-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { ActionProjectType } from "@app/db/schemas"; import { BadRequestError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { TProjectPermission } from "@app/lib/types"; @@ -27,13 +28,14 @@ export const trustedIpServiceFactory = ({ projectDAL }: TTrustedIpServiceFactoryDep) => { const listIpsByProjectId = async ({ projectId, actor, actorId, actorAuthMethod, actorOrgId }: TProjectPermission) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); const trustedIps = await trustedIpDAL.find({ projectId @@ -51,13 +53,14 @@ export const trustedIpServiceFactory = ({ comment, isActive }: TCreateIpDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList); const project = await projectDAL.findById(projectId); @@ -96,13 +99,14 @@ export const trustedIpServiceFactory = ({ comment, trustedIpId }: TUpdateIpDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList); const project = await projectDAL.findById(projectId); @@ -141,13 +145,14 @@ export const trustedIpServiceFactory = ({ actorAuthMethod, trustedIpId }: TDeleteIpDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList); const project = await projectDAL.findById(projectId); diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index 723a22817..ac28e9ade 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -1,7 +1,17 @@ import { Redis } from "ioredis"; +import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext"; import { Redlock, Settings } from "@app/lib/red-lock"; +export const PgSqlLock = { + BootUpMigration: 2023, + SuperAdminInit: 2024, + KmsRootKeyInit: 2025, + OrgGatewayRootCaInit: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-root-ca:${orgId}`), + OrgGatewayCertExchange: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-cert-exchange:${orgId}`), + SecretRotationV2Creation: (folderId: string) => pgAdvisoryLockHashText(`secret-rotation-v2-creation:${folderId}`) +} as const; + export type TKeyStoreFactory = ReturnType; // all the key prefixes used must be set here to avoid conflict @@ -23,13 +33,18 @@ export const KeyStorePrefixes = { `sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const, SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) => `sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const, + SecretSyncLock: (syncId: string) => `secret-sync-mutex-${syncId}` as const, + SecretRotationLock: (rotationId: string) => `secret-rotation-v2-mutex-${rotationId}` as const, + SecretSyncLastRunTimestamp: (syncId: string) => `secret-sync-last-run-${syncId}` as const, IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) => `identity-access-token-status:${identityAccessTokenId}`, - ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}` + ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}`, + GatewayIdentityCredential: (identityId: string) => `gateway-credentials:${identityId}` }; export const KeyStoreTtls = { SetSyncSecretIntegrationLastRunTimestampInSeconds: 60, + SetSecretSyncLastRunTimestampInSeconds: 60, AccessTokenStatusUpdateInSeconds: 120 }; @@ -62,6 +77,8 @@ export const keyStoreFactory = (redisUrl: string) => { const incrementBy = async (key: string, value: number) => redis.incrby(key, value); + const setExpiry = async (key: string, expiryInSeconds: number) => redis.expire(key, expiryInSeconds); + const waitTillReady = async ({ key, waitingCb, @@ -88,6 +105,7 @@ export const keyStoreFactory = (redisUrl: string) => { return { setItem, getItem, + setExpiry, setItemWithExpiry, deleteItem, incrementBy, diff --git a/backend/src/keystore/memory.ts b/backend/src/keystore/memory.ts new file mode 100644 index 000000000..10b28ffec --- /dev/null +++ b/backend/src/keystore/memory.ts @@ -0,0 +1,39 @@ +import { Lock } from "@app/lib/red-lock"; + +import { TKeyStoreFactory } from "./keystore"; + +export const inMemoryKeyStore = (): TKeyStoreFactory => { + const store: Record = {}; + + return { + setItem: async (key, value) => { + store[key] = value; + return "OK"; + }, + setExpiry: async () => 0, + setItemWithExpiry: async (key, value) => { + store[key] = value; + return "OK"; + }, + deleteItem: async (key) => { + delete store[key]; + return 1; + }, + getItem: async (key) => { + const value = store[key]; + if (typeof value === "string") { + return value; + } + return null; + }, + incrementBy: async () => { + return 1; + }, + acquireLock: () => { + return Promise.resolve({ + release: () => {} + }) as Promise; + }, + waitTillReady: async () => {} + }; +}; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 136a4db29..0f88e269c 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1,3 +1,58 @@ +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { + SECRET_ROTATION_CONNECTION_MAP, + SECRET_ROTATION_NAME_MAP +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps"; + +export enum ApiDocsTags { + Identities = "Identities", + TokenAuth = "Token Auth", + UniversalAuth = "Universal Auth", + GcpAuth = "GCP Auth", + AwsAuth = "AWS Auth", + AzureAuth = "Azure Auth", + KubernetesAuth = "Kubernetes Auth", + JwtAuth = "JWT Auth", + OidcAuth = "OIDC Auth", + Groups = "Groups", + Organizations = "Organizations", + Projects = "Projects", + ProjectUsers = "Project Users", + ProjectGroups = "Project Groups", + ProjectIdentities = "Project Identities", + ProjectRoles = "Project Roles", + ProjectTemplates = "Project Templates", + Environments = "Environments", + Folders = "Folders", + SecretTags = "Secret Tags", + Secrets = "Secrets", + DynamicSecrets = "Dynamic Secrets", + SecretImports = "Secret Imports", + SecretRotations = "Secret Rotations", + IdentitySpecificPrivilegesV1 = "Identity Specific Privileges", + IdentitySpecificPrivilegesV2 = "Identity Specific Privileges V2", + AppConnections = "App Connections", + SecretSyncs = "Secret Syncs", + Integrations = "Integrations", + ServiceTokens = "Service Tokens", + AuditLogs = "Audit Logs", + PkiCertificateAuthorities = "PKI Certificate Authorities", + PkiCertificates = "PKI Certificates", + PkiCertificateTemplates = "PKI Certificate Templates", + PkiCertificateCollections = "PKI Certificate Collections", + PkiAlerting = "PKI Alerting", + SshCertificates = "SSH Certificates", + SshCertificateAuthorities = "SSH Certificate Authorities", + SshCertificateTemplates = "SSH Certificate Templates", + KmsKeys = "KMS Keys", + KmsEncryption = "KMS Encryption", + KmsSigning = "KMS Signing" +} + export const GROUPS = { CREATE: { name: "The name of the group to create.", @@ -19,7 +74,9 @@ export const GROUPS = { offset: "The offset to start from. If you enter 10, it will start from the 10th user.", limit: "The number of users to return.", username: "The username to search for.", - search: "The text string that user email or name will be filtered by." + search: "The text string that user email or name will be filtered by.", + filterUsers: + "Whether to filter the list of returned users. 'existingMembers' will only return existing users in the group, 'nonMembers' will only return users not in the group, undefined will return all users in the organization." }, ADD_USER: { id: "The ID of the group to add the user to.", @@ -54,6 +111,17 @@ export const IDENTITIES = { }, LIST: { orgId: "The ID of the organization to list identities." + }, + SEARCH: { + search: { + desc: "The filters to apply to the search.", + name: "The name of the identity to filter by.", + role: "The organizational role of the identity to filter by." + }, + offset: "The offset to start from. If you enter 10, it will start from the 10th identity.", + limit: "The number of identities to return.", + orderBy: "The column to order identities by.", + orderDirection: "The direction to order identities in." } } as const; @@ -237,7 +305,7 @@ export const KUBERNETES_AUTH = { kubernetesHost: "The host string, host:port pair, or URL to the base of the Kubernetes API server.", caCert: "The PEM-encoded CA cert for the Kubernetes API server.", tokenReviewerJwt: - "The long-lived service account JWT token for Infisical to access the TokenReview API to validate other service account JWT tokens submitted by applications/pods.", + "Optional JWT token for accessing Kubernetes TokenReview API. If provided, this long-lived token will be used to validate service account tokens during authentication. If omitted, the client's own JWT will be used instead, which requires the client to have the system:auth-delegator ClusterRole binding.", allowedNamespaces: "The comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical.", allowedNames: "The comma-separated list of trusted service account names that can authenticate with Infisical.", @@ -253,7 +321,7 @@ export const KUBERNETES_AUTH = { kubernetesHost: "The new host string, host:port pair, or URL to the base of the Kubernetes API server.", caCert: "The new PEM-encoded CA cert for the Kubernetes API server.", tokenReviewerJwt: - "The new long-lived service account JWT token for Infisical to access the TokenReview API to validate other service account JWT tokens submitted by applications/pods.", + "Optional JWT token for accessing Kubernetes TokenReview API. If provided, this long-lived token will be used to validate service account tokens during authentication. If omitted, the client's own JWT will be used instead, which requires the client to have the system:auth-delegator ClusterRole binding.", allowedNamespaces: "The new comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical.", allowedNames: "The new comma-separated list of trusted service account names that can authenticate with Infisical.", @@ -322,6 +390,7 @@ export const OIDC_AUTH = { boundIssuer: "The unique identifier of the identity provider issuing the JWT.", boundAudiences: "The list of intended recipients.", boundClaims: "The attributes that should be present in the JWT for it to be valid.", + claimMetadataMapping: "The attributes that should be present in the permission metadata from the JWT.", boundSubject: "The expected principal that is the subject of the JWT.", accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.", accessTokenTTL: "The lifetime for an access token in seconds.", @@ -335,6 +404,53 @@ export const OIDC_AUTH = { boundIssuer: "The new unique identifier of the identity provider issuing the JWT.", boundAudiences: "The new list of intended recipients.", boundClaims: "The new attributes that should be present in the JWT for it to be valid.", + claimMetadataMapping: "The new attributes that should be present in the permission metadata from the JWT.", + boundSubject: "The new expected principal that is the subject of the JWT.", + accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.", + accessTokenTTL: "The new lifetime for an access token in seconds.", + accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.", + accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used." + }, + RETRIEVE: { + identityId: "The ID of the identity to retrieve the auth method for." + }, + REVOKE: { + identityId: "The ID of the identity to revoke the auth method for." + } +} as const; + +export const JWT_AUTH = { + LOGIN: { + identityId: "The ID of the identity to login." + }, + ATTACH: { + identityId: "The ID of the identity to attach the configuration onto.", + configurationType: "The configuration for validating JWTs. Must be one of: 'jwks', 'static'", + jwksUrl: + "The URL of the JWKS endpoint. Required if configurationType is 'jwks'. This endpoint must serve JSON Web Key Sets (JWKS) containing the public keys used to verify JWT signatures.", + jwksCaCert: "The PEM-encoded CA certificate for validating the TLS connection to the JWKS endpoint.", + publicKeys: + "A list of PEM-encoded public keys used to verify JWT signatures. Required if configurationType is 'static'. Each key must be in RSA or ECDSA format and properly PEM-encoded with BEGIN/END markers.", + boundIssuer: "The unique identifier of the JWT provider.", + boundAudiences: "The list of intended recipients.", + boundClaims: "The attributes that should be present in the JWT for it to be valid.", + boundSubject: "The expected principal that is the subject of the JWT.", + accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.", + accessTokenTTL: "The lifetime for an access token in seconds.", + accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.", + accessTokenNumUsesLimit: "The maximum number of times that an access token can be used." + }, + UPDATE: { + identityId: "The ID of the identity to update the auth method for.", + configurationType: "The new configuration for validating JWTs. Must be one of: 'jwks', 'static'", + jwksUrl: + "The new URL of the JWKS endpoint. This endpoint must serve JSON Web Key Sets (JWKS) containing the public keys used to verify JWT signatures.", + jwksCaCert: "The new PEM-encoded CA certificate for validating the TLS connection to the JWKS endpoint.", + publicKeys: + "A new list of PEM-encoded public keys used to verify JWT signatures. Each key must be in RSA or ECDSA format and properly PEM-encoded with BEGIN/END markers.", + boundIssuer: "The new unique identifier of the JWT provider.", + boundAudiences: "The new list of intended recipients.", + boundClaims: "The new attributes that should be present in the JWT for it to be valid.", boundSubject: "The new expected principal that is the subject of the JWT.", accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.", accessTokenTTL: "The new lifetime for an access token in seconds.", @@ -380,7 +496,8 @@ export const ORGANIZATIONS = { search: "The text string that identity membership names will be filtered by." }, GET_PROJECTS: { - organizationId: "The ID of the organization to get projects from." + organizationId: "The ID of the organization to get projects from.", + type: "The type of project to filter by." }, LIST_GROUPS: { organizationId: "The ID of the organization to list groups for." @@ -391,6 +508,7 @@ export const PROJECTS = { CREATE: { organizationSlug: "The slug of the organization to create the project in.", projectName: "The name of the project to create.", + projectDescription: "An optional description label for the project.", slug: "An optional slug for the project.", template: "The name of the project template, if specified, to apply to this project." }, @@ -403,7 +521,10 @@ export const PROJECTS = { UPDATE: { workspaceId: "The ID of the project to update.", name: "The new name of the project.", - autoCapitalization: "Disable or enable auto-capitalization for the project." + projectDescription: "An optional description label for the project.", + autoCapitalization: "Disable or enable auto-capitalization for the project.", + slug: "An optional slug for the project. (must be unique within the organization)", + hasDeleteProtection: "Enable or disable delete protection for the project." }, GET_KEY: { workspaceId: "The ID of the project to get the key from." @@ -420,7 +541,7 @@ export const PROJECTS = { }, ADD_GROUP_TO_PROJECT: { projectId: "The ID of the project to add the group to.", - groupId: "The ID of the group to add to the project.", + groupIdOrName: "The ID or name of the group to add to the project.", role: "The role for the group to assume in the project." }, UPDATE_GROUP_IN_PROJECT: { @@ -441,6 +562,20 @@ export const PROJECTS = { LIST_INTEGRATION_AUTHORIZATION: { workspaceId: "The ID of the project to list integration auths for." }, + LIST_SSH_CAS: { + projectId: "The ID of the project to list SSH CAs for." + }, + LIST_SSH_HOSTS: { + projectId: "The ID of the project to list SSH hosts for." + }, + LIST_SSH_CERTIFICATES: { + projectId: "The ID of the project to list SSH certificates for.", + offset: "The offset to start from. If you enter 10, it will start from the 10th SSH certificate.", + limit: "The number of SSH certificates to return." + }, + LIST_SSH_CERTIFICATE_TEMPLATES: { + projectId: "The ID of the project to list SSH certificate templates for." + }, LIST_CAS: { slug: "The slug of the project to list CAs for.", status: "The status of the CA to filter by.", @@ -561,7 +696,10 @@ export const FOLDERS = { workspaceId: "The ID of the project to list folders from.", environment: "The slug of the environment to list folders from.", path: "The path to list folders from.", - directory: "The directory to list folders from. (Deprecated in favor of path)" + directory: "The directory to list folders from. (Deprecated in favor of path)", + recursive: "Whether or not to fetch all folders from the specified base path, and all of its subdirectories.", + lastSecretModified: + "The timestamp used to filter folders with secrets modified after the specified date. The format for this timestamp is ISO 8601 (e.g. 2025-04-01T09:41:45-04:00)" }, GET_BY_ID: { folderId: "The ID of the folder to get details." @@ -571,7 +709,8 @@ export const FOLDERS = { environment: "The slug of the environment to create the folder in.", name: "The name of the folder to create.", path: "The path of the folder to create.", - directory: "The directory of the folder to create. (Deprecated in favor of path)" + directory: "The directory of the folder to create. (Deprecated in favor of path)", + description: "An optional description label for the folder." }, UPDATE: { folderId: "The ID of the folder to update.", @@ -580,7 +719,8 @@ export const FOLDERS = { path: "The path of the folder to update.", directory: "The new directory of the folder to update. (Deprecated in favor of path)", projectSlug: "The slug of the project where the folder is located.", - workspaceId: "The ID of the project where the folder is located." + workspaceId: "The ID of the project where the folder is located.", + description: "An optional description label for the folder." }, DELETE: { folderIdOrName: "The ID or name of the folder to delete.", @@ -597,6 +737,7 @@ export const SECRETS = { secretPath: "The path of the secret to attach tags to.", type: "The type of the secret to attach tags to. (shared/personal)", environment: "The slug of the environment where the secret is located", + viewSecretValue: "Whether or not to retrieve the secret value.", projectSlug: "The slug of the project where the secret is located.", tagSlugs: "An array of existing tag slugs to attach to the secret." }, @@ -620,8 +761,11 @@ export const RAW_SECRETS = { "The slug of the project to list secrets from. This parameter is only applicable by machine identities.", environment: "The slug of the environment to list secrets from.", secretPath: "The secret path to list secrets from.", + viewSecretValue: "Whether or not to retrieve the secret value.", includeImports: "Weather to include imported secrets or not.", - tagSlugs: "The comma separated tag slugs to filter secrets." + tagSlugs: "The comma separated tag slugs to filter secrets.", + metadataFilter: + "The secret metadata key-value pairs to filter secrets by. When querying for multiple metadata pairs, the query is treated as an AND operation. Secret metadata format is key=value1,value=value2|key=value3,value=value4." }, CREATE: { secretName: "The name of the secret to create.", @@ -646,13 +790,15 @@ export const RAW_SECRETS = { secretPath: "The path of the secret to get.", version: "The version of the secret to get.", type: "The type of the secret to get.", + viewSecretValue: "Whether or not to retrieve the secret value.", includeImports: "Weather to include imported secrets or not." }, UPDATE: { secretName: "The name of the secret to update.", secretComment: "Update comment to the secret.", environment: "The slug of the environment where the secret is located.", - secretPath: "The path of the secret to update.", + mode: "Defines how the system should handle missing secrets during an update.", + secretPath: "The default path for secrets to update or upsert, if not provided in the secret details.", secretValue: "The new value of the secret.", skipMultilineEncoding: "Skip multiline encoding for the secret value.", type: "The type of the secret to update.", @@ -676,6 +822,12 @@ export const RAW_SECRETS = { workspaceId: "The ID of the project where the secret is located.", environment: "The slug of the environment where the the secret is located.", secretPath: "The folder path where the secret is located." + }, + GET_ACCESS_LIST: { + secretName: "The name of the secret to get the access list for.", + workspaceId: "The ID of the project where the secret is located.", + environment: "The slug of the environment where the the secret is located.", + secretPath: "The folder path where the secret is located." } } as const; @@ -731,7 +883,9 @@ export const DASHBOARD = { search: "The text string to filter secret keys and folder names by.", includeSecrets: "Whether to include project secrets in the response.", includeFolders: "Whether to include project folders in the response.", - includeDynamicSecrets: "Whether to include dynamic project secrets in the response." + includeDynamicSecrets: "Whether to include dynamic project secrets in the response.", + includeImports: "Whether to include project secret imports in the response.", + includeSecretRotations: "Whether to include project secret rotations in the response." }, SECRET_DETAILS_LIST: { projectId: "The ID of the project to list secrets/folders from.", @@ -746,7 +900,8 @@ export const DASHBOARD = { includeSecrets: "Whether to include project secrets in the response.", includeFolders: "Whether to include project folders in the response.", includeImports: "Whether to include project secret imports in the response.", - includeDynamicSecrets: "Whether to include dynamic project secrets in the response." + includeDynamicSecrets: "Whether to include dynamic project secrets in the response.", + includeSecretRotations: "Whether to include secret rotations in the response." } } as const; @@ -754,7 +909,13 @@ export const AUDIT_LOGS = { EXPORT: { projectId: "Optionally filter logs by project ID. If not provided, logs from the entire organization will be returned.", + environment: + "The environment to filter logs by. If not provided, logs from all environments will be returned. Note that the projectId parameter must also be provided.", eventType: "The type of the event to export.", + secretPath: + "The path of the secret to query audit logs for. Note that the projectId parameter must also be provided.", + secretKey: + "The key of the secret to query audit logs for. Note that the projectId parameter must also be provided.", userAgentType: "Choose which consuming application to export audit logs for.", eventMetadata: "Filter by event metadata key-value pairs. Formatted as `key1=value1,key2=value2`, with comma-separation.", @@ -772,7 +933,7 @@ export const DYNAMIC_SECRETS = { environmentSlug: "The slug of the environment to list folders from.", path: "The path to list folders from." }, - LIST_LEAES_BY_NAME: { + LIST_LEASES_BY_NAME: { projectSlug: "The slug of the project to create dynamic secret in.", environmentSlug: "The slug of the environment to list folders from.", path: "The path to list folders from.", @@ -1030,6 +1191,9 @@ export const INTEGRATION_AUTH = { DELETE_BY_ID: { integrationAuthId: "The ID of integration authentication object to delete." }, + UPDATE_BY_ID: { + integrationAuthId: "The ID of integration authentication object to update." + }, CREATE_ACCESS_TOKEN: { workspaceId: "The ID of the project to create the integration auth for.", integration: "The slug of integration for the auth object.", @@ -1072,6 +1236,7 @@ export const INTEGRATION = { shouldAutoRedeploy: "Used by Render to trigger auto deploy.", secretGCPLabel: "The label for GCP secrets.", secretAWSTag: "The tags for AWS secrets.", + azureLabel: "Define which label to assign to secrets created in Azure App Configuration.", githubVisibility: "Define where the secrets from the Github Integration should be visible. Option 'selected' lets you directly define which repositories to sync secrets to.", githubVisibilityRepoIds: @@ -1080,16 +1245,20 @@ export const INTEGRATION = { shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store.", shouldMaskSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Masked'.", shouldProtectSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Protected'.", - shouldEnableDelete: "The flag to enable deletion of secrets." + shouldEnableDelete: "The flag to enable deletion of secrets.", + octopusDeployScopeValues: "Specifies the scope values to set on synced secrets to Octopus Deploy.", + metadataSyncMode: "The mode for syncing metadata to external system" } }, UPDATE: { integrationId: "The ID of the integration object.", + region: "AWS region to sync secrets to.", app: "The name of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.", appId: "The ID of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.", isActive: "Whether the integration should be active or disabled.", secretPath: "The path of the secrets to sync secrets from.", + path: "Path to save the synced secrets. Used by Gitlab, AWS Parameter Store, Vault.", owner: "External integration providers service entity owner. Used in Github.", targetEnvironment: "The target environment of the integration provider. Used in cloudflare pages, TeamCity, Gitlab integrations.", @@ -1129,6 +1298,144 @@ export const AUDIT_LOG_STREAMS = { } }; +export const SSH_CERTIFICATE_AUTHORITIES = { + CREATE: { + projectId: "The ID of the project to create the SSH CA in.", + friendlyName: "A friendly name for the SSH CA.", + keyAlgorithm: + "The type of public key algorithm and size, in bits, of the key pair for the SSH CA; required if keySource is internal.", + publicKey: "The public key for the SSH CA key pair; required if keySource is external.", + privateKey: "The private key for the SSH CA key pair; required if keySource is external.", + keySource: "The source of the SSH CA key pair. This can be one of internal or external." + }, + GET: { + sshCaId: "The ID of the SSH CA to get." + }, + GET_PUBLIC_KEY: { + sshCaId: "The ID of the SSH CA to get the public key for." + }, + UPDATE: { + sshCaId: "The ID of the SSH CA to update.", + friendlyName: "A friendly name for the SSH CA to update to.", + status: "The status of the SSH CA to update to. This can be one of active or disabled." + }, + DELETE: { + sshCaId: "The ID of the SSH CA to delete." + }, + GET_CERTIFICATE_TEMPLATES: { + sshCaId: "The ID of the SSH CA to get the certificate templates for." + }, + SIGN_SSH_KEY: { + certificateTemplateId: "The ID of the SSH certificate template to sign the SSH public key with.", + publicKey: "The SSH public key to sign.", + certType: "The type of certificate to issue. This can be one of user or host.", + principals: "The list of principals (usernames, hostnames) to include in the certificate.", + ttl: "The time to live for the certificate such as 1m, 1h, 1d, ... If not specified, the default TTL for the template will be used.", + keyId: "The key ID to include in the certificate. If not specified, a default key ID will be generated.", + serialNumber: "The serial number of the issued SSH certificate.", + signedKey: "The SSH certificate or signed SSH public key." + }, + ISSUE_SSH_CREDENTIALS: { + certificateTemplateId: "The ID of the SSH certificate template to issue the SSH credentials with.", + keyAlgorithm: "The type of public key algorithm and size, in bits, of the key pair for the SSH CA.", + certType: "The type of certificate to issue. This can be one of user or host.", + principals: "The list of principals (usernames, hostnames) to include in the certificate.", + ttl: "The time to live for the certificate such as 1m, 1h, 1d, ... If not specified, the default TTL for the template will be used.", + keyId: "The key ID to include in the certificate. If not specified, a default key ID will be generated.", + serialNumber: "The serial number of the issued SSH certificate.", + signedKey: "The SSH certificate or signed SSH public key.", + privateKey: "The private key corresponding to the issued SSH certificate.", + publicKey: "The public key of the issued SSH certificate." + } +}; + +export const SSH_CERTIFICATE_TEMPLATES = { + GET: { + certificateTemplateId: "The ID of the SSH certificate template to get." + }, + CREATE: { + sshCaId: "The ID of the SSH CA to associate the certificate template with.", + name: "The name of the certificate template.", + ttl: "The default time to live for issued certificates such as 1m, 1h, 1d, 1y, ...", + maxTTL: "The maximum time to live for issued certificates such as 1m, 1h, 1d, 1y, ...", + allowedUsers: "The list of allowed users for certificates issued under this template.", + allowedHosts: "The list of allowed hosts for certificates issued under this template.", + allowUserCertificates: "Whether or not to allow user certificates to be issued under this template.", + allowHostCertificates: "Whether or not to allow host certificates to be issued under this template.", + allowCustomKeyIds: "Whether or not to allow custom key IDs for certificates issued under this template." + }, + UPDATE: { + certificateTemplateId: "The ID of the SSH certificate template to update.", + name: "The name of the certificate template.", + ttl: "The default time to live for issued certificates such as 1m, 1h, 1d, 1y, ...", + maxTTL: "The maximum time to live for issued certificates such as 1m, 1h, 1d, 1y, ...", + allowedUsers: "The list of allowed users for certificates issued under this template.", + allowedHosts: "The list of allowed hosts for certificates issued under this template.", + allowUserCertificates: "Whether or not to allow user certificates to be issued under this template.", + allowHostCertificates: "Whether or not to allow host certificates to be issued under this template.", + allowCustomKeyIds: "Whether or not to allow custom key IDs for certificates issued under this template." + }, + DELETE: { + certificateTemplateId: "The ID of the SSH certificate template to delete." + } +}; + +export const SSH_HOSTS = { + GET: { + sshHostId: "The ID of the SSH host to get." + }, + CREATE: { + projectId: "The ID of the project to create the SSH host in.", + hostname: "The hostname of the SSH host.", + userCertTtl: "The time to live for user certificates issued under this host.", + hostCertTtl: "The time to live for host certificates issued under this host.", + loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')", + allowedPrincipals: "A list of allowed principals that can log in as the login user.", + loginMappings: + "A list of login mappings for the SSH host. Each login mapping contains a login user and a list of corresponding allowed principals being usernames of users in the Infisical SSH project.", + userSshCaId: + "The ID of the SSH CA to use for user certificates. If not specified, the default user SSH CA will be used if it exists.", + hostSshCaId: + "The ID of the SSH CA to use for host certificates. If not specified, the default host SSH CA will be used if it exists." + }, + UPDATE: { + sshHostId: "The ID of the SSH host to update.", + hostname: "The hostname of the SSH host to update to.", + userCertTtl: "The time to live for user certificates issued under this host to update to.", + hostCertTtl: "The time to live for host certificates issued under this host to update to.", + loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')", + allowedPrincipals: "A list of allowed principals that can log in as the login user.", + loginMappings: + "A list of login mappings for the SSH host. Each login mapping contains a login user and a list of corresponding allowed principals being usernames of users in the Infisical SSH project." + }, + DELETE: { + sshHostId: "The ID of the SSH host to delete." + }, + ISSUE_SSH_CREDENTIALS: { + sshHostId: "The ID of the SSH host to issue the SSH credentials for.", + loginUser: "The login user to issue the SSH credentials for.", + keyAlgorithm: "The type of public key algorithm and size, in bits, of the key pair for the SSH host.", + serialNumber: "The serial number of the issued SSH certificate.", + signedKey: "The SSH certificate or signed SSH public key.", + privateKey: "The private key corresponding to the issued SSH certificate.", + publicKey: "The public key of the issued SSH certificate." + }, + ISSUE_HOST_CERT: { + sshHostId: "The ID of the SSH host to issue the SSH certificate for.", + publicKey: "The SSH public key to issue the SSH certificate for.", + serialNumber: "The serial number of the issued SSH certificate.", + signedKey: "The SSH certificate or signed SSH public key." + }, + GET_USER_CA_PUBLIC_KEY: { + sshHostId: "The ID of the SSH host to get the user SSH CA public key for.", + publicKey: "The public key of the user SSH CA linked to the SSH host." + }, + GET_HOST_CA_PUBLIC_KEY: { + sshHostId: "The ID of the SSH host to get the host SSH CA public key for.", + publicKey: "The public key of the host SSH CA linked to the SSH host." + } +}; + export const CERTIFICATE_AUTHORITIES = { CREATE: { projectSlug: "Slug of the project to create the CA in.", @@ -1411,7 +1718,8 @@ export const KMS = { projectId: "The ID of the project to create the key in.", name: "The name of the key to be created. Must be slug-friendly.", description: "An optional description of the key.", - encryptionAlgorithm: "The algorithm to use when performing cryptographic operations with the key." + encryptionAlgorithm: "The algorithm to use when performing cryptographic operations with the key.", + type: "The type of key to be created, either encrypt-decrypt or sign-verify, based on your intended use for the key." }, UPDATE_KEY: { keyId: "The ID of the key to be updated.", @@ -1430,6 +1738,13 @@ export const KMS = { orderDirection: "The direction to order keys in.", search: "The text string to filter key names by." }, + GET_KEY_BY_ID: { + keyId: "The ID of the KMS key to retrieve." + }, + GET_KEY_BY_NAME: { + keyName: "The name of the KMS key to retrieve.", + projectId: "The ID of the project the key belongs to." + }, ENCRYPT: { keyId: "The ID of the key to encrypt the data with.", plaintext: "The plaintext to be encrypted (base64 encoded)." @@ -1437,6 +1752,28 @@ export const KMS = { DECRYPT: { keyId: "The ID of the key to decrypt the data with.", ciphertext: "The ciphertext to be decrypted (base64 encoded)." + }, + + LIST_SIGNING_ALGORITHMS: { + keyId: "The ID of the key to list the signing algorithms for. The key must be for signing and verifying." + }, + + GET_PUBLIC_KEY: { + keyId: "The ID of the key to get the public key for. The key must be for signing and verifying." + }, + + SIGN: { + keyId: "The ID of the key to sign the data with.", + data: "The data in string format to be signed (base64 encoded).", + isDigest: + "Whether the data is already digested or not. Please be aware that if you are passing a digest the algorithm used to create the digest must match the signing algorithm used to sign the digest.", + signingAlgorithm: "The algorithm to use when performing cryptographic operations with the key." + }, + VERIFY: { + keyId: "The ID of the key to verify the data with.", + data: "The data in string format to be verified (base64 encoded). For data larger than 4096 bytes you must first create a digest of the data and then pass the digest in the data parameter.", + signature: "The signature to be verified (base64 encoded).", + isDigest: "Whether the data is already digested or not." } }; @@ -1458,3 +1795,281 @@ export const ProjectTemplates = { templateId: "The ID of the project template to be deleted." } }; + +export const AppConnections = { + GET_BY_ID: (app: AppConnection) => ({ + connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection to retrieve.` + }), + GET_BY_NAME: (app: AppConnection) => ({ + connectionName: `The name of the ${APP_CONNECTION_NAME_MAP[app]} Connection to retrieve.` + }), + CREATE: (app: AppConnection) => { + const appName = APP_CONNECTION_NAME_MAP[app]; + return { + name: `The name of the ${appName} Connection to create. Must be slug-friendly.`, + description: `An optional description for the ${appName} Connection.`, + credentials: `The credentials used to connect with ${appName}.`, + method: `The method used to authenticate with ${appName}.`, + isPlatformManagedCredentials: `Whether or not the ${appName} Connection credentials should be managed by Infisical. Once enabled this cannot be reversed.` + }; + }, + UPDATE: (app: AppConnection) => { + const appName = APP_CONNECTION_NAME_MAP[app]; + return { + connectionId: `The ID of the ${appName} Connection to be updated.`, + name: `The updated name of the ${appName} Connection. Must be slug-friendly.`, + description: `The updated description of the ${appName} Connection.`, + credentials: `The credentials used to connect with ${appName}.`, + method: `The method used to authenticate with ${appName}.`, + isPlatformManagedCredentials: `Whether or not the ${appName} Connection credentials should be managed by Infisical. Once enabled this cannot be reversed.` + }; + }, + DELETE: (app: AppConnection) => ({ + connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection to be deleted.` + }), + CREDENTIALS: { + AUTH0_CONNECTION: { + domain: "The domain of the Auth0 instance to connect to.", + clientId: "Your Auth0 application's Client ID.", + clientSecret: "Your Auth0 application's Client Secret.", + audience: "The unique identifier of the target API you want to access." + }, + SQL_CONNECTION: { + host: "The hostname of the database server.", + port: "The port number of the database.", + database: "The name of the database to connect to.", + username: "The username to connect to the database with.", + password: "The password to connect to the database with.", + sslEnabled: "Whether or not to use SSL when connecting to the database.", + sslRejectUnauthorized: "Whether or not to reject unauthorized SSL certificates.", + sslCertificate: "The SSL certificate to use for connection." + }, + TERRAFORM_CLOUD: { + apiToken: "The API token to use to connect with Terraform Cloud." + }, + VERCEL: { + apiToken: "The API token used to authenticate with Vercel." + }, + CAMUNDA: { + clientId: "The client ID used to authenticate with Camunda.", + clientSecret: "The client secret used to authenticate with Camunda." + }, + WINDMILL: { + instanceUrl: "The Windmill instance URL to connect with (defaults to https://app.windmill.dev).", + accessToken: "The access token to use to connect with Windmill." + } + } +}; + +export const SecretSyncs = { + LIST: (destination?: SecretSync) => ({ + projectId: `The ID of the project to list ${destination ? SECRET_SYNC_NAME_MAP[destination] : "Secret"} Syncs from.` + }), + GET_BY_ID: (destination: SecretSync) => ({ + syncId: `The ID of the ${SECRET_SYNC_NAME_MAP[destination]} Sync to retrieve.` + }), + GET_BY_NAME: (destination: SecretSync) => ({ + syncName: `The name of the ${SECRET_SYNC_NAME_MAP[destination]} Sync to retrieve.`, + projectId: `The ID of the project the ${SECRET_SYNC_NAME_MAP[destination]} Sync is associated with.` + }), + CREATE: (destination: SecretSync) => { + const destinationName = SECRET_SYNC_NAME_MAP[destination]; + return { + name: `The name of the ${destinationName} Sync to create. Must be slug-friendly.`, + description: `An optional description for the ${destinationName} Sync.`, + projectId: "The ID of the project to create the sync in.", + environment: `The slug of the project environment to sync secrets from.`, + secretPath: `The folder path to sync secrets from.`, + connectionId: `The ID of the ${ + APP_CONNECTION_NAME_MAP[SECRET_SYNC_CONNECTION_MAP[destination]] + } Connection to use for syncing.`, + isAutoSyncEnabled: `Whether secrets should be automatically synced when changes occur at the source location or not.`, + syncOptions: "Optional parameters to modify how secrets are synced." + }; + }, + UPDATE: (destination: SecretSync) => { + const destinationName = SECRET_SYNC_NAME_MAP[destination]; + return { + syncId: `The ID of the ${destinationName} Sync to be updated.`, + connectionId: `The updated ID of the ${ + APP_CONNECTION_NAME_MAP[SECRET_SYNC_CONNECTION_MAP[destination]] + } Connection to use for syncing.`, + name: `The updated name of the ${destinationName} Sync. Must be slug-friendly.`, + environment: `The updated slug of the project environment to sync secrets from.`, + secretPath: `The updated folder path to sync secrets from.`, + description: `The updated description of the ${destinationName} Sync.`, + isAutoSyncEnabled: `Whether secrets should be automatically synced when changes occur at the source location or not.`, + syncOptions: "Optional parameters to modify how secrets are synced." + }; + }, + DELETE: (destination: SecretSync) => ({ + syncId: `The ID of the ${SECRET_SYNC_NAME_MAP[destination]} Sync to be deleted.`, + removeSecrets: `Whether previously synced secrets should be removed prior to deletion.` + }), + SYNC_SECRETS: (destination: SecretSync) => ({ + syncId: `The ID of the ${SECRET_SYNC_NAME_MAP[destination]} Sync to trigger a sync for.` + }), + IMPORT_SECRETS: (destination: SecretSync) => ({ + syncId: `The ID of the ${SECRET_SYNC_NAME_MAP[destination]} Sync to trigger importing secrets for.`, + importBehavior: `Specify whether Infisical should prioritize secret values from Infisical or ${SECRET_SYNC_NAME_MAP[destination]}.` + }), + REMOVE_SECRETS: (destination: SecretSync) => ({ + syncId: `The ID of the ${SECRET_SYNC_NAME_MAP[destination]} Sync to trigger removing secrets for.` + }), + SYNC_OPTIONS: (destination: SecretSync) => { + const destinationName = SECRET_SYNC_NAME_MAP[destination]; + return { + initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`, + disableSecretDeletion: `Enable this flag to prevent removal of secrets from the ${destinationName} destination when syncing.` + }; + }, + ADDITIONAL_SYNC_OPTIONS: { + AWS_PARAMETER_STORE: { + keyId: "The AWS KMS key ID or alias to use when encrypting parameters synced by Infisical.", + tags: "Optional resource tags to add to parameters synced by Infisical.", + syncSecretMetadataAsTags: `Whether Infisical secret metadata should be added as resource tags to parameters synced by Infisical.` + }, + AWS_SECRETS_MANAGER: { + keyId: "The AWS KMS key ID or alias to use when encrypting parameters synced by Infisical.", + tags: "Optional tags to add to secrets synced by Infisical.", + syncSecretMetadataAsTags: `Whether Infisical secret metadata should be added as tags to secrets synced by Infisical.` + } + }, + DESTINATION_CONFIG: { + AWS_PARAMETER_STORE: { + region: "The AWS region to sync secrets to.", + path: "The Parameter Store path to sync secrets to." + }, + AWS_SECRETS_MANAGER: { + region: "The AWS region to sync secrets to.", + mappingBehavior: "How secrets from Infisical should be mapped to AWS Secrets Manager; one-to-one or many-to-one.", + secretName: "The secret name in AWS Secrets Manager to sync to when using mapping behavior many-to-one." + }, + GITHUB: { + scope: "The GitHub scope that secrets should be synced to", + org: "The name of the GitHub organization.", + owner: "The name of the GitHub account owner of the repository.", + repo: "The name of the GitHub repository.", + env: "The name of the GitHub environment." + }, + AZURE_KEY_VAULT: { + vaultBaseUrl: "The base URL of the Azure Key Vault to sync secrets to. Example: https://example.vault.azure.net/" + }, + AZURE_APP_CONFIGURATION: { + configurationUrl: + "The URL of the Azure App Configuration to sync secrets to. Example: https://example.azconfig.io/", + label: "An optional label to assign to secrets created in Azure App Configuration." + }, + GCP: { + scope: "The Google project scope that secrets should be synced to.", + projectId: "The ID of the Google project secrets should be synced to." + }, + DATABRICKS: { + scope: "The Databricks secret scope that secrets should be synced to." + }, + CAMUNDA: { + scope: "The Camunda scope that secrets should be synced to.", + clusterUUID: "The UUID of the Camunda cluster that secrets should be synced to." + }, + HUMANITEC: { + app: "The ID of the Humanitec app to sync secrets to.", + org: "The ID of the Humanitec org to sync secrets to.", + env: "The ID of the Humanitec environment to sync secrets to.", + scope: "The Humanitec scope that secrets should be synced to." + }, + TERRAFORM_CLOUD: { + org: "The ID of the Terraform Cloud org to sync secrets to.", + variableSetName: "The name of the Terraform Cloud Variable Set to sync secrets to.", + variableSetId: "The ID of the Terraform Cloud Variable Set to sync secrets to.", + workspaceName: "The name of the Terraform Cloud workspace to sync secrets to.", + workspaceId: "The ID of the Terraform Cloud workspace to sync secrets to.", + scope: "The Terraform Cloud scope that secrets should be synced to.", + category: "The Terraform Cloud category that secrets should be synced to." + }, + VERCEL: { + app: "The ID of the Vercel app to sync secrets to.", + appName: "The name of the Vercel app to sync secrets to.", + env: "The ID of the Vercel environment to sync secrets to.", + branch: "The branch to sync preview secrets to.", + teamId: "The ID of the Vercel team to sync secrets to." + }, + WINDMILL: { + workspace: "The Windmill workspace to sync secrets to.", + path: "The Windmill workspace path to sync secrets to." + } + } +}; + +export const SecretRotations = { + LIST: (type?: SecretRotation) => ({ + projectId: `The ID of the project to list ${type ? SECRET_ROTATION_NAME_MAP[type] : "Secret"} Rotations from.` + }), + GET_BY_ID: (type: SecretRotation) => ({ + rotationId: `The ID of the ${SECRET_ROTATION_NAME_MAP[type]} Rotation to retrieve.` + }), + GET_GENERATED_CREDENTIALS_BY_ID: (type: SecretRotation) => ({ + rotationId: `The ID of the ${SECRET_ROTATION_NAME_MAP[type]} Rotation to retrieve the generated credentials for.` + }), + GET_BY_NAME: (type: SecretRotation) => ({ + rotationName: `The name of the ${SECRET_ROTATION_NAME_MAP[type]} Rotation to retrieve.`, + projectId: `The ID of the project the ${SECRET_ROTATION_NAME_MAP[type]} Rotation is located in.`, + secretPath: `The secret path the ${SECRET_ROTATION_NAME_MAP[type]} Rotation is located at.`, + environment: `The environment the ${SECRET_ROTATION_NAME_MAP[type]} Rotation is located in.` + }), + CREATE: (type: SecretRotation) => { + const destinationName = SECRET_ROTATION_NAME_MAP[type]; + return { + name: `The name of the ${destinationName} Rotation to create. Must be slug-friendly.`, + description: `An optional description for the ${destinationName} Rotation.`, + projectId: "The ID of the project to create the rotation in.", + environment: `The slug of the project environment to create the rotation in.`, + secretPath: `The secret path of the project to create the rotation in.`, + connectionId: `The ID of the ${ + APP_CONNECTION_NAME_MAP[SECRET_ROTATION_CONNECTION_MAP[type]] + } Connection to use for rotation.`, + isAutoRotationEnabled: `Whether secrets should be automatically rotated when the specified rotation interval has elapsed.`, + rotationInterval: `The interval, in days, to automatically rotate secrets.`, + rotateAtUtc: `The hours and minutes rotation should occur at in UTC. Defaults to Midnight (00:00) UTC.` + }; + }, + UPDATE: (type: SecretRotation) => { + const typeName = SECRET_ROTATION_NAME_MAP[type]; + return { + rotationId: `The ID of the ${typeName} Rotation to be updated.`, + name: `The updated name of the ${typeName} Rotation. Must be slug-friendly.`, + description: `The updated description of the ${typeName} Rotation.`, + isAutoRotationEnabled: `Whether secrets should be automatically rotated when the specified rotation interval has elapsed.`, + rotationInterval: `The updated interval, in days, to automatically rotate secrets.`, + rotateAtUtc: `The updated hours and minutes rotation should occur at in UTC.` + }; + }, + DELETE: (type: SecretRotation) => ({ + rotationId: `The ID of the ${SECRET_ROTATION_NAME_MAP[type]} Rotation to be deleted.`, + deleteSecrets: `Whether the mapped secrets belonging to this rotation should be deleted.`, + revokeGeneratedCredentials: `Whether the generated credentials associated with this rotation should be revoked.` + }), + ROTATE: (type: SecretRotation) => ({ + rotationId: `The ID of the ${SECRET_ROTATION_NAME_MAP[type]} Rotation to rotate generated credentials for.` + }), + PARAMETERS: { + SQL_CREDENTIALS: { + username1: + "The username of the first login to rotate passwords for. This user must already exists in your database.", + username2: + "The username of the second login to rotate passwords for. This user must already exists in your database." + }, + AUTH0_CLIENT_SECRET: { + clientId: "The client ID of the Auth0 Application to rotate the client secret for." + } + }, + SECRETS_MAPPING: { + SQL_CREDENTIALS: { + username: "The name of the secret that the active username will be mapped to.", + password: "The name of the secret that the generated password will be mapped to." + }, + AUTH0_CLIENT_SECRET: { + clientId: "The name of the secret that the client ID will be mapped to.", + clientSecret: "The name of the secret that the rotated client secret will be mapped to." + } + } +}; diff --git a/backend/src/lib/axios/digest-auth.ts b/backend/src/lib/axios/digest-auth.ts index ee9dbd79b..449c471fd 100644 --- a/backend/src/lib/axios/digest-auth.ts +++ b/backend/src/lib/axios/digest-auth.ts @@ -28,8 +28,8 @@ export const createDigestAuthRequestInterceptor = ( nc += 1; const nonceCount = nc.toString(16).padStart(8, "0"); const cnonce = crypto.randomBytes(24).toString("hex"); - const realm = authDetails.find((el) => el[0].toLowerCase().indexOf("realm") > -1)?.[1].replace(/"/g, ""); - const nonce = authDetails.find((el) => el[0].toLowerCase().indexOf("nonce") > -1)?.[1].replace(/"/g, ""); + const realm = authDetails.find((el) => el[0].toLowerCase().indexOf("realm") > -1)?.[1]?.replaceAll('"', "") || ""; + const nonce = authDetails.find((el) => el[0].toLowerCase().indexOf("nonce") > -1)?.[1]?.replaceAll('"', "") || ""; const ha1 = crypto.createHash("md5").update(`${username}:${realm}:${password}`).digest("hex"); const path = opts.url; diff --git a/backend/src/lib/base64/index.ts b/backend/src/lib/base64/index.ts index cfc0fde3f..dfdd03d47 100644 --- a/backend/src/lib/base64/index.ts +++ b/backend/src/lib/base64/index.ts @@ -1,26 +1,39 @@ -// Credit: https://github.com/miguelmota/is-base64 -export const isBase64 = ( - v: string, - opts = { allowEmpty: false, mimeRequired: false, allowMime: true, paddingRequired: false } -) => { - if (opts.allowEmpty === false && v === "") { - return false; +import RE2 from "re2"; + +type Base64Options = { + urlSafe?: boolean; + padding?: boolean; +}; + +const base64WithPadding = new RE2(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/); +const base64WithoutPadding = new RE2(/^[A-Za-z0-9+/]+$/); +const base64UrlWithPadding = new RE2( + /^(?:[A-Za-z0-9_-]{4})*(?:[A-Za-z0-9_-]{2}==|[A-Za-z0-9_-]{3}=|[A-Za-z0-9_-]{4})$/ +); +const base64UrlWithoutPadding = new RE2(/^[A-Za-z0-9_-]+$/); + +export const isBase64 = (str: string, options: Base64Options = {}): boolean => { + if (typeof str !== "string") { + throw new TypeError("Expected a string"); } - let regex = "(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+/]{3}=)?"; - const mimeRegex = "(data:\\w+\\/[a-zA-Z\\+\\-\\.]+;base64,)"; + // Default padding to true unless urlSafe is true + const opts: Base64Options = { + urlSafe: false, + padding: options.urlSafe === undefined ? true : !options.urlSafe, + ...options + }; - if (opts.mimeRequired === true) { - regex = mimeRegex + regex; - } else if (opts.allowMime === true) { - regex = `${mimeRegex}?${regex}`; + if (str === "") return true; + + let regex; + if (opts.urlSafe) { + regex = opts.padding ? base64UrlWithPadding : base64UrlWithoutPadding; + } else { + regex = opts.padding ? base64WithPadding : base64WithoutPadding; } - if (opts.paddingRequired === false) { - regex = "(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}(==)?|[A-Za-z0-9+\\/]{3}=?)?"; - } - - return new RegExp(`^${regex}$`, "gi").test(v); + return (!opts.padding || str.length % 4 === 0) && regex.test(str); }; export const getBase64SizeInBytes = (base64String: string) => { diff --git a/backend/src/lib/casl/boundary.test.ts b/backend/src/lib/casl/boundary.test.ts new file mode 100644 index 000000000..05c2b9ecf --- /dev/null +++ b/backend/src/lib/casl/boundary.test.ts @@ -0,0 +1,669 @@ +import { createMongoAbility } from "@casl/ability"; + +import { PermissionConditionOperators } from "."; +import { validatePermissionBoundary } from "./boundary"; + +describe("Validate Permission Boundary Function", () => { + test.each([ + { + title: "child with equal privilege", + parentPermission: createMongoAbility([ + { + action: ["create", "edit", "delete", "read"], + subject: "secrets" + } + ]), + childPermission: createMongoAbility([ + { + action: ["create", "edit", "delete", "read"], + subject: "secrets" + } + ]), + expectValid: true, + missingPermissions: [] + }, + { + title: "child with less privilege", + parentPermission: createMongoAbility([ + { + action: ["create", "edit", "delete", "read"], + subject: "secrets" + } + ]), + childPermission: createMongoAbility([ + { + action: ["create", "edit"], + subject: "secrets" + } + ]), + expectValid: true, + missingPermissions: [] + }, + { + title: "child with more privilege", + parentPermission: createMongoAbility([ + { + action: ["create"], + subject: "secrets" + } + ]), + childPermission: createMongoAbility([ + { + action: ["create", "edit"], + subject: "secrets" + } + ]), + expectValid: false, + missingPermissions: [{ action: "edit", subject: "secrets" }] + }, + { + title: "parent with multiple and child with multiple", + parentPermission: createMongoAbility([ + { + action: ["create"], + subject: "secrets" + }, + { + action: ["create", "edit"], + subject: "members" + } + ]), + childPermission: createMongoAbility([ + { + action: ["create"], + subject: "members" + }, + { + action: ["create"], + subject: "secrets" + } + ]), + expectValid: true, + missingPermissions: [] + }, + { + title: "Child with no access", + parentPermission: createMongoAbility([ + { + action: ["create"], + subject: "secrets" + }, + { + action: ["create", "edit"], + subject: "members" + } + ]), + childPermission: createMongoAbility([]), + expectValid: true, + missingPermissions: [] + }, + { + title: "Parent and child disjoint set", + parentPermission: createMongoAbility([ + { + action: ["create", "edit", "delete", "read"], + subject: "secrets", + conditions: { + environment: { [PermissionConditionOperators.$EQ]: "dev" } + } + } + ]), + childPermission: createMongoAbility([ + { + action: ["create", "edit", "delete", "read"], + subject: "secrets", + conditions: { + secretPath: { [PermissionConditionOperators.$EQ]: "dev" } + } + } + ]), + expectValid: false, + missingPermissions: ["create", "edit", "delete", "read"].map((el) => ({ + action: el, + subject: "secrets", + conditions: { + secretPath: { [PermissionConditionOperators.$EQ]: "dev" } + } + })) + }, + { + title: "Parent with inverted rules", + parentPermission: createMongoAbility([ + { + action: ["create", "edit", "delete", "read"], + subject: "secrets", + conditions: { + environment: { [PermissionConditionOperators.$EQ]: "dev" } + } + }, + { + action: "read", + subject: "secrets", + inverted: true, + conditions: { + environment: { [PermissionConditionOperators.$EQ]: "dev" }, + secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**" } + } + } + ]), + childPermission: createMongoAbility([ + { + action: "read", + subject: "secrets", + conditions: { + environment: { [PermissionConditionOperators.$EQ]: "dev" }, + secretPath: { [PermissionConditionOperators.$EQ]: "/" } + } + } + ]), + expectValid: true, + missingPermissions: [] + }, + { + title: "Parent with inverted rules - child accessing invalid one", + parentPermission: createMongoAbility([ + { + action: ["create", "edit", "delete", "read"], + subject: "secrets", + conditions: { + environment: { [PermissionConditionOperators.$EQ]: "dev" } + } + }, + { + action: "read", + subject: "secrets", + inverted: true, + conditions: { + environment: { [PermissionConditionOperators.$EQ]: "dev" }, + secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**" } + } + } + ]), + childPermission: createMongoAbility([ + { + action: "read", + subject: "secrets", + conditions: { + environment: { [PermissionConditionOperators.$EQ]: "dev" }, + secretPath: { [PermissionConditionOperators.$EQ]: "/hello/world" } + } + } + ]), + expectValid: false, + missingPermissions: [ + { + action: "read", + subject: "secrets", + conditions: { + environment: { [PermissionConditionOperators.$EQ]: "dev" }, + secretPath: { [PermissionConditionOperators.$EQ]: "/hello/world" } + } + } + ] + } + ])("Check permission: $title", ({ parentPermission, childPermission, expectValid, missingPermissions }) => { + const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission); + if (expectValid) { + expect(permissionBoundary.isValid).toBeTruthy(); + } else { + expect(permissionBoundary.isValid).toBeFalsy(); + expect(permissionBoundary.missingPermissions).toEqual(expect.arrayContaining(missingPermissions)); + } + }); +}); + +describe("Validate Permission Boundary: Checking Parent $eq operator", () => { + const parentPermission = createMongoAbility([ + { + action: ["create", "read"], + subject: "secrets", + conditions: { + environment: { [PermissionConditionOperators.$EQ]: "dev" } + } + } + ]); + + test.each([ + { + operator: PermissionConditionOperators.$EQ, + childPermission: createMongoAbility([ + { + action: ["create"], + subject: "secrets", + conditions: { + environment: { [PermissionConditionOperators.$EQ]: "dev" } + } + } + ]) + }, + { + operator: PermissionConditionOperators.$IN, + childPermission: createMongoAbility([ + { + action: ["create"], + subject: "secrets", + conditions: { + environment: { [PermissionConditionOperators.$IN]: ["dev"] } + } + } + ]) + }, + { + operator: PermissionConditionOperators.$GLOB, + childPermission: createMongoAbility([ + { + action: ["create"], + subject: "secrets", + conditions: { + environment: { [PermissionConditionOperators.$GLOB]: "dev" } + } + } + ]) + } + ])("Child $operator truthy cases", ({ childPermission }) => { + const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission); + expect(permissionBoundary.isValid).toBeTruthy(); + }); + + test.each([ + { + operator: PermissionConditionOperators.$EQ, + childPermission: createMongoAbility([ + { + action: ["create"], + subject: "secrets", + conditions: { + environment: { [PermissionConditionOperators.$EQ]: "prod" } + } + } + ]) + }, + { + operator: PermissionConditionOperators.$IN, + childPermission: createMongoAbility([ + { + action: ["create"], + subject: "secrets", + conditions: { + environment: { [PermissionConditionOperators.$IN]: ["dev", "prod"] } + } + } + ]) + }, + { + operator: PermissionConditionOperators.$GLOB, + childPermission: createMongoAbility([ + { + action: ["create"], + subject: "secrets", + conditions: { + environment: { [PermissionConditionOperators.$GLOB]: "dev**" } + } + } + ]) + }, + { + operator: PermissionConditionOperators.$NEQ, + childPermission: createMongoAbility([ + { + action: ["create"], + subject: "secrets", + conditions: { + environment: { [PermissionConditionOperators.$GLOB]: "staging" } + } + } + ]) + } + ])("Child $operator falsy cases", ({ childPermission }) => { + const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission); + expect(permissionBoundary.isValid).toBeFalsy(); + }); +}); + +describe("Validate Permission Boundary: Checking Parent $neq operator", () => { + const parentPermission = createMongoAbility([ + { + action: ["create", "read"], + subject: "secrets", + conditions: { + secretPath: { [PermissionConditionOperators.$NEQ]: "/hello" } + } + } + ]); + + test.each([ + { + operator: PermissionConditionOperators.$EQ, + childPermission: createMongoAbility([ + { + action: ["create"], + subject: "secrets", + conditions: { + secretPath: { [PermissionConditionOperators.$EQ]: "/" } + } + } + ]) + }, + { + operator: PermissionConditionOperators.$NEQ, + childPermission: createMongoAbility([ + { + action: ["create"], + subject: "secrets", + conditions: { + secretPath: { [PermissionConditionOperators.$NEQ]: "/hello" } + } + } + ]) + }, + { + operator: PermissionConditionOperators.$IN, + childPermission: createMongoAbility([ + { + action: ["create"], + subject: "secrets", + conditions: { + secretPath: { [PermissionConditionOperators.$IN]: ["/", "/staging"] } + } + } + ]) + }, + { + operator: PermissionConditionOperators.$GLOB, + childPermission: createMongoAbility([ + { + action: ["create"], + subject: "secrets", + conditions: { + secretPath: { [PermissionConditionOperators.$GLOB]: "/dev**" } + } + } + ]) + } + ])("Child $operator truthy cases", ({ childPermission }) => { + const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission); + expect(permissionBoundary.isValid).toBeTruthy(); + }); + + test.each([ + { + operator: PermissionConditionOperators.$EQ, + childPermission: createMongoAbility([ + { + action: ["create"], + subject: "secrets", + conditions: { + secretPath: { [PermissionConditionOperators.$EQ]: "/hello" } + } + } + ]) + }, + { + operator: PermissionConditionOperators.$NEQ, + childPermission: createMongoAbility([ + { + action: ["create"], + subject: "secrets", + conditions: { + secretPath: { [PermissionConditionOperators.$NEQ]: "/" } + } + } + ]) + }, + { + operator: PermissionConditionOperators.$IN, + childPermission: createMongoAbility([ + { + action: ["create"], + subject: "secrets", + conditions: { + secretPath: { [PermissionConditionOperators.$IN]: ["/", "/hello"] } + } + } + ]) + }, + { + operator: PermissionConditionOperators.$GLOB, + childPermission: createMongoAbility([ + { + action: ["create"], + subject: "secrets", + conditions: { + secretPath: { [PermissionConditionOperators.$GLOB]: "/hello**" } + } + } + ]) + } + ])("Child $operator falsy cases", ({ childPermission }) => { + const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission); + expect(permissionBoundary.isValid).toBeFalsy(); + }); +}); + +describe("Validate Permission Boundary: Checking Parent $IN operator", () => { + const parentPermission = createMongoAbility([ + { + action: ["edit"], + subject: "secrets", + conditions: { + environment: { [PermissionConditionOperators.$IN]: ["dev", "staging"] } + } + } + ]); + + test.each([ + { + operator: PermissionConditionOperators.$EQ, + childPermission: createMongoAbility([ + { + action: ["edit"], + subject: "secrets", + conditions: { + environment: { [PermissionConditionOperators.$EQ]: "dev" } + } + } + ]) + }, + { + operator: PermissionConditionOperators.$IN, + childPermission: createMongoAbility([ + { + action: ["edit"], + subject: "secrets", + conditions: { + environment: { [PermissionConditionOperators.$IN]: ["dev"] } + } + } + ]) + }, + { + operator: `${PermissionConditionOperators.$IN} - 2`, + childPermission: createMongoAbility([ + { + action: ["edit"], + subject: "secrets", + conditions: { + environment: { [PermissionConditionOperators.$IN]: ["dev", "staging"] } + } + } + ]) + }, + { + operator: PermissionConditionOperators.$GLOB, + childPermission: createMongoAbility([ + { + action: ["edit"], + subject: "secrets", + conditions: { + environment: { [PermissionConditionOperators.$GLOB]: "dev" } + } + } + ]) + } + ])("Child $operator truthy cases", ({ childPermission }) => { + const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission); + expect(permissionBoundary.isValid).toBeTruthy(); + }); + + test.each([ + { + operator: PermissionConditionOperators.$EQ, + childPermission: createMongoAbility([ + { + action: ["edit"], + subject: "secrets", + conditions: { + environment: { [PermissionConditionOperators.$EQ]: "prod" } + } + } + ]) + }, + { + operator: PermissionConditionOperators.$NEQ, + childPermission: createMongoAbility([ + { + action: ["edit"], + subject: "secrets", + conditions: { + environment: { [PermissionConditionOperators.$NEQ]: "dev" } + } + } + ]) + }, + { + operator: PermissionConditionOperators.$IN, + childPermission: createMongoAbility([ + { + action: ["edit"], + subject: "secrets", + conditions: { + environment: { [PermissionConditionOperators.$IN]: ["dev", "prod"] } + } + } + ]) + }, + { + operator: PermissionConditionOperators.$GLOB, + childPermission: createMongoAbility([ + { + action: ["edit"], + subject: "secrets", + conditions: { + environment: { [PermissionConditionOperators.$GLOB]: "dev**" } + } + } + ]) + } + ])("Child $operator falsy cases", ({ childPermission }) => { + const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission); + expect(permissionBoundary.isValid).toBeFalsy(); + }); +}); + +describe("Validate Permission Boundary: Checking Parent $GLOB operator", () => { + const parentPermission = createMongoAbility([ + { + action: ["create", "read"], + subject: "secrets", + conditions: { + secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**" } + } + } + ]); + + test.each([ + { + operator: PermissionConditionOperators.$EQ, + childPermission: createMongoAbility([ + { + action: ["create"], + subject: "secrets", + conditions: { + secretPath: { [PermissionConditionOperators.$EQ]: "/hello/world" } + } + } + ]) + }, + { + operator: PermissionConditionOperators.$IN, + childPermission: createMongoAbility([ + { + action: ["create"], + subject: "secrets", + conditions: { + secretPath: { [PermissionConditionOperators.$IN]: ["/hello/world", "/hello/world2"] } + } + } + ]) + }, + { + operator: PermissionConditionOperators.$GLOB, + childPermission: createMongoAbility([ + { + action: ["create"], + subject: "secrets", + conditions: { + secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**/world" } + } + } + ]) + } + ])("Child $operator truthy cases", ({ childPermission }) => { + const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission); + expect(permissionBoundary.isValid).toBeTruthy(); + }); + + test.each([ + { + operator: PermissionConditionOperators.$EQ, + childPermission: createMongoAbility([ + { + action: ["create"], + subject: "secrets", + conditions: { + secretPath: { [PermissionConditionOperators.$EQ]: "/print" } + } + } + ]) + }, + { + operator: PermissionConditionOperators.$NEQ, + childPermission: createMongoAbility([ + { + action: ["create"], + subject: "secrets", + conditions: { + secretPath: { [PermissionConditionOperators.$NEQ]: "/hello/world" } + } + } + ]) + }, + { + operator: PermissionConditionOperators.$IN, + childPermission: createMongoAbility([ + { + action: ["create"], + subject: "secrets", + conditions: { + secretPath: { [PermissionConditionOperators.$IN]: ["/", "/hello"] } + } + } + ]) + }, + { + operator: PermissionConditionOperators.$GLOB, + childPermission: createMongoAbility([ + { + action: ["create"], + subject: "secrets", + conditions: { + secretPath: { [PermissionConditionOperators.$GLOB]: "/hello**" } + } + } + ]) + } + ])("Child $operator falsy cases", ({ childPermission }) => { + const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission); + expect(permissionBoundary.isValid).toBeFalsy(); + }); +}); diff --git a/backend/src/lib/casl/boundary.ts b/backend/src/lib/casl/boundary.ts new file mode 100644 index 000000000..15592a7bd --- /dev/null +++ b/backend/src/lib/casl/boundary.ts @@ -0,0 +1,249 @@ +import { MongoAbility } from "@casl/ability"; +import { MongoQuery } from "@ucast/mongo2js"; +import picomatch from "picomatch"; + +import { PermissionConditionOperators } from "./index"; + +type TMissingPermission = { + action: string; + subject: string; + conditions?: MongoQuery; +}; + +type TPermissionConditionShape = { + [PermissionConditionOperators.$EQ]: string; + [PermissionConditionOperators.$NEQ]: string; + [PermissionConditionOperators.$GLOB]: string; + [PermissionConditionOperators.$IN]: string[]; +}; + +const getPermissionSetID = (action: string, subject: string) => `${action}:${subject}`; +const invertTheOperation = (shouldInvert: boolean, operation: boolean) => (shouldInvert ? !operation : operation); +const formatConditionOperator = (condition: TPermissionConditionShape | string) => { + return ( + typeof condition === "string" ? { [PermissionConditionOperators.$EQ]: condition } : condition + ) as TPermissionConditionShape; +}; + +const isOperatorsASubset = (parentSet: TPermissionConditionShape, subset: TPermissionConditionShape) => { + // we compute each operator against each other in left hand side and right hand side + if (subset[PermissionConditionOperators.$EQ] || subset[PermissionConditionOperators.$NEQ]) { + const subsetOperatorValue = subset[PermissionConditionOperators.$EQ] || subset[PermissionConditionOperators.$NEQ]; + const isInverted = !subset[PermissionConditionOperators.$EQ]; + if ( + parentSet[PermissionConditionOperators.$EQ] && + invertTheOperation(isInverted, parentSet[PermissionConditionOperators.$EQ] !== subsetOperatorValue) + ) { + return false; + } + if ( + parentSet[PermissionConditionOperators.$NEQ] && + invertTheOperation(isInverted, parentSet[PermissionConditionOperators.$NEQ] === subsetOperatorValue) + ) { + return false; + } + if ( + parentSet[PermissionConditionOperators.$IN] && + invertTheOperation(isInverted, !parentSet[PermissionConditionOperators.$IN].includes(subsetOperatorValue)) + ) { + return false; + } + // ne and glob cannot match each other + if (parentSet[PermissionConditionOperators.$GLOB] && isInverted) { + return false; + } + if ( + parentSet[PermissionConditionOperators.$GLOB] && + !picomatch.isMatch(subsetOperatorValue, parentSet[PermissionConditionOperators.$GLOB], { strictSlashes: false }) + ) { + return false; + } + } + if (subset[PermissionConditionOperators.$IN]) { + const subsetOperatorValue = subset[PermissionConditionOperators.$IN]; + if ( + parentSet[PermissionConditionOperators.$EQ] && + (subsetOperatorValue.length !== 1 || subsetOperatorValue[0] !== parentSet[PermissionConditionOperators.$EQ]) + ) { + return false; + } + if ( + parentSet[PermissionConditionOperators.$NEQ] && + subsetOperatorValue.includes(parentSet[PermissionConditionOperators.$NEQ]) + ) { + return false; + } + if ( + parentSet[PermissionConditionOperators.$IN] && + !subsetOperatorValue.every((el) => parentSet[PermissionConditionOperators.$IN].includes(el)) + ) { + return false; + } + if ( + parentSet[PermissionConditionOperators.$GLOB] && + !subsetOperatorValue.every((el) => + picomatch.isMatch(el, parentSet[PermissionConditionOperators.$GLOB], { + strictSlashes: false + }) + ) + ) { + return false; + } + } + if (subset[PermissionConditionOperators.$GLOB]) { + const subsetOperatorValue = subset[PermissionConditionOperators.$GLOB]; + const { isGlob } = picomatch.scan(subsetOperatorValue); + // if it's glob, all other fixed operators would make this superset because glob is powerful. like eq + // example: $in [dev, prod] => glob: dev** could mean anything starting with dev: thus is bigger + if ( + isGlob && + Object.keys(parentSet).some( + (el) => el !== PermissionConditionOperators.$GLOB && el !== PermissionConditionOperators.$NEQ + ) + ) { + return false; + } + + if ( + parentSet[PermissionConditionOperators.$EQ] && + parentSet[PermissionConditionOperators.$EQ] !== subsetOperatorValue + ) { + return false; + } + if ( + parentSet[PermissionConditionOperators.$NEQ] && + picomatch.isMatch(parentSet[PermissionConditionOperators.$NEQ], subsetOperatorValue, { + strictSlashes: false + }) + ) { + return false; + } + // if parent set is IN, glob cannot be used for children - It's a bigger scope + if ( + parentSet[PermissionConditionOperators.$IN] && + !parentSet[PermissionConditionOperators.$IN].includes(subsetOperatorValue) + ) { + return false; + } + if ( + parentSet[PermissionConditionOperators.$GLOB] && + !picomatch.isMatch(subsetOperatorValue, parentSet[PermissionConditionOperators.$GLOB], { + strictSlashes: false + }) + ) { + return false; + } + } + return true; +}; + +const isSubsetForSamePermissionSubjectAction = ( + parentSetRules: ReturnType, + subsetRules: ReturnType, + appendToMissingPermission: (condition?: MongoQuery) => void +) => { + const isMissingConditionInParent = parentSetRules.every((el) => !el.conditions); + if (isMissingConditionInParent) return true; + + // all subset rules must pass in comparison to parent rul + return subsetRules.every((subsetRule) => { + const subsetRuleConditions = subsetRule.conditions as Record; + // compare subset rule with all parent rules + const isSubsetOfNonInvertedParentSet = parentSetRules + .filter((el) => !el.inverted) + .some((parentSetRule) => { + // get conditions and iterate + const parentSetRuleConditions = parentSetRule?.conditions as Record; + if (!parentSetRuleConditions) return true; + return Object.keys(parentSetRuleConditions).every((parentConditionField) => { + // if parent condition is missing then it's never a subset + if (!subsetRuleConditions?.[parentConditionField]) return false; + + // standardize the conditions plain string operator => $eq function + const parentRuleConditionOperators = formatConditionOperator(parentSetRuleConditions[parentConditionField]); + const selectedSubsetRuleCondition = subsetRuleConditions?.[parentConditionField]; + const subsetRuleConditionOperators = formatConditionOperator(selectedSubsetRuleCondition); + return isOperatorsASubset(parentRuleConditionOperators, subsetRuleConditionOperators); + }); + }); + + const invertedParentSetRules = parentSetRules.filter((el) => el.inverted); + const isNotSubsetOfInvertedParentSet = invertedParentSetRules.length + ? !invertedParentSetRules.some((parentSetRule) => { + // get conditions and iterate + const parentSetRuleConditions = parentSetRule?.conditions as Record< + string, + TPermissionConditionShape | string + >; + if (!parentSetRuleConditions) return true; + return Object.keys(parentSetRuleConditions).every((parentConditionField) => { + // if parent condition is missing then it's never a subset + if (!subsetRuleConditions?.[parentConditionField]) return false; + + // standardize the conditions plain string operator => $eq function + const parentRuleConditionOperators = formatConditionOperator(parentSetRuleConditions[parentConditionField]); + const selectedSubsetRuleCondition = subsetRuleConditions?.[parentConditionField]; + const subsetRuleConditionOperators = formatConditionOperator(selectedSubsetRuleCondition); + return isOperatorsASubset(parentRuleConditionOperators, subsetRuleConditionOperators); + }); + }) + : true; + const isSubset = isSubsetOfNonInvertedParentSet && isNotSubsetOfInvertedParentSet; + if (!isSubset) { + appendToMissingPermission(subsetRule.conditions); + } + return isSubset; + }); +}; + +export const validatePermissionBoundary = (parentSetPermissions: MongoAbility, subsetPermissions: MongoAbility) => { + const checkedPermissionRules = new Set(); + const missingPermissions: TMissingPermission[] = []; + + subsetPermissions.rules.forEach((subsetPermissionRules) => { + const subsetPermissionSubject = subsetPermissionRules.subject.toString(); + let subsetPermissionActions: string[] = []; + + // actions can be string or string[] + if (typeof subsetPermissionRules.action === "string") { + subsetPermissionActions.push(subsetPermissionRules.action); + } else { + subsetPermissionRules.action.forEach((subsetPermissionAction) => { + subsetPermissionActions.push(subsetPermissionAction); + }); + } + + // if action is already processed ignore + subsetPermissionActions = subsetPermissionActions.filter( + (el) => !checkedPermissionRules.has(getPermissionSetID(el, subsetPermissionSubject)) + ); + + if (!subsetPermissionActions.length) return; + subsetPermissionActions.forEach((subsetPermissionAction) => { + const parentSetRulesOfSubset = parentSetPermissions.possibleRulesFor( + subsetPermissionAction, + subsetPermissionSubject + ); + const nonInveretedOnes = parentSetRulesOfSubset.filter((el) => !el.inverted); + if (!nonInveretedOnes.length) { + missingPermissions.push({ action: subsetPermissionAction, subject: subsetPermissionSubject }); + return; + } + + const subsetRules = subsetPermissions.possibleRulesFor(subsetPermissionAction, subsetPermissionSubject); + isSubsetForSamePermissionSubjectAction(parentSetRulesOfSubset, subsetRules, (conditions) => { + missingPermissions.push({ action: subsetPermissionAction, subject: subsetPermissionSubject, conditions }); + }); + }); + + subsetPermissionActions.forEach((el) => + checkedPermissionRules.add(getPermissionSetID(el, subsetPermissionSubject)) + ); + }); + + if (missingPermissions.length) { + return { isValid: false as const, missingPermissions }; + } + + return { isValid: true }; +}; diff --git a/backend/src/lib/casl/index.ts b/backend/src/lib/casl/index.ts index 71625e181..7a3c05969 100644 --- a/backend/src/lib/casl/index.ts +++ b/backend/src/lib/casl/index.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import { buildMongoQueryMatcher, MongoAbility } from "@casl/ability"; +import { buildMongoQueryMatcher } from "@casl/ability"; import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js"; import picomatch from "picomatch"; @@ -20,37 +20,10 @@ const glob: JsInterpreter> = (node, object, context) => { export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob }); -/** - * Extracts and formats permissions from a CASL Ability object or a raw permission set. - */ -const extractPermissions = (ability: MongoAbility) => { - const permissions: string[] = []; - ability.rules.forEach((permission) => { - if (typeof permission.action === "string") { - permissions.push(`${permission.action}_${permission.subject as string}`); - } else { - permission.action.forEach((permissionAction) => { - permissions.push(`${permissionAction}_${permission.subject as string}`); - }); - } - }); - return permissions; -}; - -/** - * Compares two sets of permissions to determine if the first set is at least as privileged as the second set. - * The function checks if all permissions in the second set are contained within the first set and if the first set has equal or more permissions. - * - */ -export const isAtLeastAsPrivileged = (permissions1: MongoAbility, permissions2: MongoAbility) => { - const set1 = new Set(extractPermissions(permissions1)); - const set2 = new Set(extractPermissions(permissions2)); - - for (const perm of set2) { - if (!set1.has(perm)) { - return false; - } - } - - return set1.size >= set2.size; -}; +export enum PermissionConditionOperators { + $IN = "$in", + $EQ = "$eq", + $NEQ = "$ne", + $GLOB = "$glob", + $ELEMENTMATCH = "$elemMatch" +} diff --git a/backend/src/lib/certificates/extract-certificate.test.ts b/backend/src/lib/certificates/extract-certificate.test.ts new file mode 100644 index 000000000..0d0fc2be6 --- /dev/null +++ b/backend/src/lib/certificates/extract-certificate.test.ts @@ -0,0 +1,42 @@ +import { extractX509CertFromChain } from "./extract-certificate"; + +describe("Extract Certificate Payload", () => { + test("Single chain", () => { + const payload = `-----BEGIN CERTIFICATE----- +MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL +BQAwDTELMAkGA1UEChMCUEgwHhcNMjQxMDI1MTU0MjAzWhcNMjUxMDI1MjE0MjAz +-----END CERTIFICATE-----`; + const result = extractX509CertFromChain(payload); + expect(result).toBeDefined(); + expect(result?.length).toBe(1); + expect(result?.[0]).toEqual(payload); + }); + + test("Multiple chain", () => { + const payload = `-----BEGIN CERTIFICATE----- +MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL +BQAwDTELMAkGA1UEChMCUEgwHhcNMjQxMDI1MTU0MjAzWhcNMjUxMDI1MjE0MjAz +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL +-----END CERTIFICATE-----`; + const result = extractX509CertFromChain(payload); + expect(result).toBeDefined(); + expect(result?.length).toBe(3); + expect(result).toEqual([ + `-----BEGIN CERTIFICATE----- +MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL +BQAwDTELMAkGA1UEChMCUEgwHhcNMjQxMDI1MTU0MjAzWhcNMjUxMDI1MjE0MjAz +-----END CERTIFICATE-----`, + `-----BEGIN CERTIFICATE----- +MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL +-----END CERTIFICATE-----`, + `-----BEGIN CERTIFICATE----- +MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL +-----END CERTIFICATE-----` + ]); + }); +}); diff --git a/backend/src/lib/certificates/extract-certificate.ts b/backend/src/lib/certificates/extract-certificate.ts new file mode 100644 index 000000000..782ceee19 --- /dev/null +++ b/backend/src/lib/certificates/extract-certificate.ts @@ -0,0 +1,51 @@ +import { BadRequestError } from "../errors"; + +export const extractX509CertFromChain = (certificateChain: string): string[] => { + if (!certificateChain) { + throw new BadRequestError({ + message: "Certificate chain is empty or undefined" + }); + } + + const certificates: string[] = []; + let currentPosition = 0; + const chainLength = certificateChain.length; + + while (currentPosition < chainLength) { + // Find the start of a certificate + const beginMarker = "-----BEGIN CERTIFICATE-----"; + const startIndex = certificateChain.indexOf(beginMarker, currentPosition); + + if (startIndex === -1) { + break; // No more certificates found + } + + // Find the end of the certificate + const endMarker = "-----END CERTIFICATE-----"; + const endIndex = certificateChain.indexOf(endMarker, startIndex); + + if (endIndex === -1) { + throw new BadRequestError({ + message: "Malformed certificate chain: Found BEGIN marker without matching END marker" + }); + } + + // Extract the complete certificate including markers + const completeEndIndex = endIndex + endMarker.length; + const certificate = certificateChain.substring(startIndex, completeEndIndex); + + // Add the extracted certificate to our results + certificates.push(certificate); + + // Move position to after this certificate + currentPosition = completeEndIndex; + } + + if (certificates.length === 0) { + throw new BadRequestError({ + message: "No valid certificates found in the chain" + }); + } + + return certificates; +}; diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 638f21e5d..907884433 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -1,7 +1,7 @@ -import { Logger } from "pino"; import { z } from "zod"; import { removeTrailingSlash } from "../fn"; +import { CustomLogger } from "../logger/logger"; import { zpStr } from "../zod"; export const GITLAB_URL = "https://gitlab.com"; @@ -10,7 +10,7 @@ export const GITLAB_URL = "https://gitlab.com"; export const IS_PACKAGED = (process as any)?.pkg !== undefined; const zodStrBool = z - .enum(["true", "false"]) + .string() .optional() .transform((val) => val === "true"); @@ -24,6 +24,7 @@ const databaseReadReplicaSchema = z const envSchema = z .object({ + INFISICAL_PLATFORM_VERSION: zpStr(z.string().optional()), PORT: z.coerce.number().default(IS_PACKAGED ? 8080 : 4000), DISABLE_SECRET_SCANNING: z .enum(["true", "false"]) @@ -55,7 +56,9 @@ const envSchema = z // TODO(akhilmhdh): will be changed to one ENCRYPTION_KEY: zpStr(z.string().optional()), ROOT_ENCRYPTION_KEY: zpStr(z.string().optional()), + QUEUE_WORKERS_ENABLED: zodStrBool.default("true"), HTTPS_ENABLED: zodStrBool, + ROTATION_DEVELOPMENT_MODE: zodStrBool.default("false").optional(), // smtp options SMTP_HOST: zpStr(z.string().optional()), SMTP_IGNORE_TLS: zodStrBool.default("false"), @@ -157,16 +160,104 @@ const envSchema = z INFISICAL_CLOUD: zodStrBool.default("false"), MAINTENANCE_MODE: zodStrBool.default("false"), CAPTCHA_SECRET: zpStr(z.string().optional()), - PLAIN_API_KEY: zpStr(z.string().optional()), - PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()), + CAPTCHA_SITE_KEY: zpStr(z.string().optional()), + INTERCOM_ID: zpStr(z.string().optional()), + + // TELEMETRY + OTEL_TELEMETRY_COLLECTION_ENABLED: zodStrBool.default("false"), + OTEL_EXPORT_OTLP_ENDPOINT: zpStr(z.string().optional()), + OTEL_OTLP_PUSH_INTERVAL: z.coerce.number().default(30000), + OTEL_COLLECTOR_BASIC_AUTH_USERNAME: zpStr(z.string().optional()), + OTEL_COLLECTOR_BASIC_AUTH_PASSWORD: zpStr(z.string().optional()), + OTEL_EXPORT_TYPE: z.enum(["prometheus", "otlp"]).optional(), + + PYLON_API_KEY: zpStr(z.string().optional()), DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"), SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"), WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()), WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()), - ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true") + ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true"), + + // HSM + HSM_LIB_PATH: zpStr(z.string().optional()), + HSM_PIN: zpStr(z.string().optional()), + HSM_KEY_LABEL: zpStr(z.string().optional()), + HSM_SLOT: z.coerce.number().optional().default(0), + + USE_PG_QUEUE: zodStrBool.default("false"), + SHOULD_INIT_PG_QUEUE: zodStrBool.default("false"), + + /* Gateway----------------------------------------------------------------------------- */ + GATEWAY_INFISICAL_STATIC_IP_ADDRESS: zpStr(z.string().optional()), + GATEWAY_RELAY_ADDRESS: zpStr(z.string().optional()), + GATEWAY_RELAY_REALM: zpStr(z.string().optional()), + GATEWAY_RELAY_AUTH_SECRET: zpStr(z.string().optional()), + + DYNAMIC_SECRET_ALLOW_INTERNAL_IP: zodStrBool.default("false"), + /* ----------------------------------------------------------------------------- */ + + /* App Connections ----------------------------------------------------------------------------- */ + ALLOW_INTERNAL_IP_CONNECTIONS: zodStrBool.default("false"), + + // aws + INF_APP_CONNECTION_AWS_ACCESS_KEY_ID: zpStr(z.string().optional()), + INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY: zpStr(z.string().optional()), + + // github oauth + INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID: zpStr(z.string().optional()), + INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET: zpStr(z.string().optional()), + + // github app + INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID: zpStr(z.string().optional()), + INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET: zpStr(z.string().optional()), + INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY: zpStr(z.string().optional()), + INF_APP_CONNECTION_GITHUB_APP_SLUG: zpStr(z.string().optional()), + INF_APP_CONNECTION_GITHUB_APP_ID: zpStr(z.string().optional()), + + // gcp app + INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL: zpStr(z.string().optional()), + + // azure app + INF_APP_CONNECTION_AZURE_CLIENT_ID: zpStr(z.string().optional()), + INF_APP_CONNECTION_AZURE_CLIENT_SECRET: zpStr(z.string().optional()), + + // datadog + SHOULD_USE_DATADOG_TRACER: zodStrBool.default("false"), + DATADOG_PROFILING_ENABLED: zodStrBool.default("false"), + DATADOG_ENV: zpStr(z.string().optional().default("prod")), + DATADOG_SERVICE: zpStr(z.string().optional().default("infisical-core")), + DATADOG_HOSTNAME: zpStr(z.string().optional()), + + /* CORS ----------------------------------------------------------------------------- */ + + CORS_ALLOWED_ORIGINS: zpStr( + z + .string() + .optional() + .transform((val) => { + if (!val) return undefined; + return JSON.parse(val) as string[]; + }) + ), + + CORS_ALLOWED_HEADERS: zpStr( + z + .string() + .optional() + .transform((val) => { + if (!val) return undefined; + return JSON.parse(val) as string[]; + }) + ) }) + // To ensure that basic encryption is always possible. + .refine( + (data) => Boolean(data.ENCRYPTION_KEY) || Boolean(data.ROOT_ENCRYPTION_KEY), + "Either ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY must be defined." + ) .transform((data) => ({ ...data, + DB_READ_REPLICAS: data.DB_READ_REPLICAS ? databaseReadReplicaSchema.parse(JSON.parse(data.DB_READ_REPLICAS)) : undefined, @@ -174,24 +265,30 @@ const envSchema = z isSmtpConfigured: Boolean(data.SMTP_HOST), isRedisConfigured: Boolean(data.REDIS_URL), isDevelopmentMode: data.NODE_ENV === "development", + isRotationDevelopmentMode: data.NODE_ENV === "development" && data.ROTATION_DEVELOPMENT_MODE, isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED, + isSecretScanningConfigured: Boolean(data.SECRET_SCANNING_GIT_APP_ID) && Boolean(data.SECRET_SCANNING_PRIVATE_KEY) && Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET), + isHsmConfigured: + Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined, + samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG, SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",") })); -let envCfg: Readonly>; +export type TEnvConfig = Readonly>; +let envCfg: TEnvConfig; export const getConfig = () => envCfg; // cannot import singleton logger directly as it needs config to load various transport -export const initEnvConfig = (logger: Logger) => { +export const initEnvConfig = (logger?: CustomLogger) => { const parsedEnv = envSchema.safeParse(process.env); if (!parsedEnv.success) { - logger.error("Invalid environment variables. Check the error below"); - logger.error(parsedEnv.error.issues); + (logger ?? console).error("Invalid environment variables. Check the error below"); + (logger ?? console).error(parsedEnv.error.issues); process.exit(-1); } diff --git a/backend/src/lib/crypto/cache.ts b/backend/src/lib/crypto/cache.ts new file mode 100644 index 000000000..9f36d360b --- /dev/null +++ b/backend/src/lib/crypto/cache.ts @@ -0,0 +1,10 @@ +import crypto from "node:crypto"; + +export const generateCacheKeyFromData = (data: unknown) => + crypto + .createHash("md5") + .update(JSON.stringify(data)) + .digest("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); diff --git a/backend/src/lib/crypto/cipher/cipher.ts b/backend/src/lib/crypto/cipher/cipher.ts index 7bc16b470..718c8ad5e 100644 --- a/backend/src/lib/crypto/cipher/cipher.ts +++ b/backend/src/lib/crypto/cipher/cipher.ts @@ -1,6 +1,6 @@ import crypto from "crypto"; -import { SymmetricEncryption, TSymmetricEncryptionFns } from "./types"; +import { SymmetricKeyAlgorithm, TSymmetricEncryptionFns } from "./types"; const getIvLength = () => { return 12; @@ -10,7 +10,9 @@ const getTagLength = () => { return 16; }; -export const symmetricCipherService = (type: SymmetricEncryption): TSymmetricEncryptionFns => { +export const symmetricCipherService = ( + type: SymmetricKeyAlgorithm.AES_GCM_128 | SymmetricKeyAlgorithm.AES_GCM_256 +): TSymmetricEncryptionFns => { const IV_LENGTH = getIvLength(); const TAG_LENGTH = getTagLength(); diff --git a/backend/src/lib/crypto/cipher/index.ts b/backend/src/lib/crypto/cipher/index.ts index 41dbcf639..27373a009 100644 --- a/backend/src/lib/crypto/cipher/index.ts +++ b/backend/src/lib/crypto/cipher/index.ts @@ -1,2 +1,2 @@ export { symmetricCipherService } from "./cipher"; -export { SymmetricEncryption } from "./types"; +export { AllowedEncryptionKeyAlgorithms, SymmetricKeyAlgorithm } from "./types"; diff --git a/backend/src/lib/crypto/cipher/types.ts b/backend/src/lib/crypto/cipher/types.ts index f490d6a66..e2f63ce5e 100644 --- a/backend/src/lib/crypto/cipher/types.ts +++ b/backend/src/lib/crypto/cipher/types.ts @@ -1,7 +1,18 @@ -export enum SymmetricEncryption { +import { z } from "zod"; + +import { AsymmetricKeyAlgorithm } from "../sign/types"; + +// Supported symmetric encrypt/decrypt algorithms +export enum SymmetricKeyAlgorithm { AES_GCM_256 = "aes-256-gcm", AES_GCM_128 = "aes-128-gcm" } +export const SymmetricKeyAlgorithmEnum = z.enum(Object.values(SymmetricKeyAlgorithm) as [string, ...string[]]).options; + +export const AllowedEncryptionKeyAlgorithms = z.enum([ + ...Object.values(SymmetricKeyAlgorithm), + ...Object.values(AsymmetricKeyAlgorithm) +] as [string, ...string[]]).options; export type TSymmetricEncryptionFns = { encrypt: (text: Buffer, key: Buffer) => Buffer; diff --git a/backend/src/lib/crypto/encryption.ts b/backend/src/lib/crypto/encryption.ts index 258a6d285..f495681f1 100644 --- a/backend/src/lib/crypto/encryption.ts +++ b/backend/src/lib/crypto/encryption.ts @@ -116,7 +116,7 @@ export const decryptAsymmetric = ({ ciphertext, nonce, publicKey, privateKey }: export const generateSymmetricKey = (size = 32) => crypto.randomBytes(size).toString("base64"); -export const generateHash = (value: string) => crypto.createHash("sha256").update(value).digest("hex"); +export const generateHash = (value: string | Buffer) => crypto.createHash("sha256").update(value).digest("hex"); export const generateAsymmetricKeyPair = () => { const pair = nacl.box.keyPair(); diff --git a/backend/src/lib/crypto/hashtext.ts b/backend/src/lib/crypto/hashtext.ts new file mode 100644 index 000000000..221608382 --- /dev/null +++ b/backend/src/lib/crypto/hashtext.ts @@ -0,0 +1,29 @@ +// used for postgres lock +// this is something postgres does under the hood +// convert any string to a unique number +export const hashtext = (text: string) => { + // Convert text to UTF8 bytes array for consistent behavior with PostgreSQL + const encoder = new TextEncoder(); + const bytes = encoder.encode(text); + + // Implementation of hash_any + let result = 0; + + for (let i = 0; i < bytes.length; i += 1) { + // eslint-disable-next-line no-bitwise + result = ((result << 5) + result) ^ bytes[i]; + // Keep within 32-bit integer range + // eslint-disable-next-line no-bitwise + result >>>= 0; + } + + // Convert to signed 32-bit integer like PostgreSQL + // eslint-disable-next-line no-bitwise + return result | 0; +}; + +export const pgAdvisoryLockHashText = (text: string) => { + const hash = hashtext(text); + // Ensure positive value within PostgreSQL integer range + return Math.abs(hash) % 2 ** 31; +}; diff --git a/backend/src/lib/crypto/sign/index.ts b/backend/src/lib/crypto/sign/index.ts new file mode 100644 index 000000000..5680cd27a --- /dev/null +++ b/backend/src/lib/crypto/sign/index.ts @@ -0,0 +1,2 @@ +export { signingService } from "./signing"; +export { AsymmetricKeyAlgorithm, SigningAlgorithm } from "./types"; diff --git a/backend/src/lib/crypto/sign/signing.ts b/backend/src/lib/crypto/sign/signing.ts new file mode 100644 index 000000000..66f36dc0f --- /dev/null +++ b/backend/src/lib/crypto/sign/signing.ts @@ -0,0 +1,564 @@ +import { execFile } from "child_process"; +import crypto from "crypto"; +import fs from "fs/promises"; +import path from "path"; +import { promisify } from "util"; + +import { BadRequestError } from "@app/lib/errors"; +import { cleanTemporaryDirectory, createTemporaryDirectory, writeToTemporaryFile } from "@app/lib/files"; +import { logger } from "@app/lib/logger"; + +import { AsymmetricKeyAlgorithm, SigningAlgorithm, TAsymmetricSignVerifyFns } from "./types"; + +const execFileAsync = promisify(execFile); + +interface SigningParams { + hashAlgorithm: SupportedHashAlgorithm; + padding?: number; + saltLength?: number; +} + +enum SupportedHashAlgorithm { + SHA256 = "sha256", + SHA384 = "sha384", + SHA512 = "sha512" +} + +const COMMAND_TIMEOUT = 15_000; + +const SHA256_DIGEST_LENGTH = 32; +const SHA384_DIGEST_LENGTH = 48; +const SHA512_DIGEST_LENGTH = 64; + +/** + * Service for cryptographic signing and verification operations using asymmetric keys + * + * @param algorithm The key algorithm itself. The signing algorithm is supplied in the individual sign/verify functions. + * @returns Object with sign and verify functions + */ +export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSignVerifyFns => { + const $getSigningParams = (signingAlgorithm: SigningAlgorithm): SigningParams => { + switch (signingAlgorithm) { + // RSA PSS + case SigningAlgorithm.RSASSA_PSS_SHA_512: + return { + hashAlgorithm: SupportedHashAlgorithm.SHA512, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: SHA512_DIGEST_LENGTH + }; + case SigningAlgorithm.RSASSA_PSS_SHA_256: + return { + hashAlgorithm: SupportedHashAlgorithm.SHA256, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: SHA256_DIGEST_LENGTH + }; + case SigningAlgorithm.RSASSA_PSS_SHA_384: + return { + hashAlgorithm: SupportedHashAlgorithm.SHA384, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: SHA384_DIGEST_LENGTH + }; + + // RSA PKCS#1 v1.5 + case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_512: + return { + hashAlgorithm: SupportedHashAlgorithm.SHA512, + padding: crypto.constants.RSA_PKCS1_PADDING + }; + case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_384: + return { + hashAlgorithm: SupportedHashAlgorithm.SHA384, + padding: crypto.constants.RSA_PKCS1_PADDING + }; + case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_256: + return { + hashAlgorithm: SupportedHashAlgorithm.SHA256, + padding: crypto.constants.RSA_PKCS1_PADDING + }; + + // ECDSA + case SigningAlgorithm.ECDSA_SHA_256: + return { hashAlgorithm: SupportedHashAlgorithm.SHA256 }; + case SigningAlgorithm.ECDSA_SHA_384: + return { hashAlgorithm: SupportedHashAlgorithm.SHA384 }; + case SigningAlgorithm.ECDSA_SHA_512: + return { hashAlgorithm: SupportedHashAlgorithm.SHA512 }; + + default: + throw new Error(`Unsupported signing algorithm: ${signingAlgorithm as string}`); + } + }; + + const $getEcCurveName = (keyAlgorithm: AsymmetricKeyAlgorithm): { full: string; short: string } => { + // We will support more in the future + switch (keyAlgorithm) { + case AsymmetricKeyAlgorithm.ECC_NIST_P256: + return { + full: "prime256v1", + short: "p256" + }; + default: + throw new Error(`Unsupported EC curve: ${keyAlgorithm}`); + } + }; + + const $validateAlgorithmWithKeyType = (signingAlgorithm: SigningAlgorithm) => { + const isRsaKey = algorithm.startsWith("RSA"); + const isEccKey = algorithm.startsWith("ECC"); + + const isRsaAlgorithm = signingAlgorithm.startsWith("RSASSA"); + const isEccAlgorithm = signingAlgorithm.startsWith("ECDSA"); + + if (isRsaKey && !isRsaAlgorithm) { + throw new BadRequestError({ message: `KMS RSA key cannot be used with ${signingAlgorithm}` }); + } + + if (isEccKey && !isEccAlgorithm) { + throw new BadRequestError({ message: `KMS ECC key cannot be used with ${signingAlgorithm}` }); + } + }; + + const $signRsaDigest = async ( + digest: Buffer, + privateKey: Buffer, + hashAlgorithm: SupportedHashAlgorithm, + signingAlgorithm: SigningAlgorithm + ) => { + const tempDir = await createTemporaryDirectory("kms-rsa-sign"); + const digestPath = path.join(tempDir, "digest.bin"); + const sigPath = path.join(tempDir, "signature.bin"); + const keyPath = path.join(tempDir, "key.pem"); + + try { + await writeToTemporaryFile(digestPath, digest); + await writeToTemporaryFile(keyPath, privateKey); + + const { stderr } = await execFileAsync( + "openssl", + [ + "pkeyutl", + "-sign", + "-in", + digestPath, + "-inkey", + keyPath, + "-pkeyopt", + `digest:${hashAlgorithm}`, + "-out", + sigPath + ], + { + maxBuffer: 10 * 1024 * 1024, + timeout: COMMAND_TIMEOUT + } + ); + + if (stderr) { + logger.error(stderr, "KMS: Failed to sign RSA digest"); + throw new BadRequestError({ + message: "Failed to sign RSA digest due to signing error" + }); + } + const signature = await fs.readFile(sigPath); + + if (!signature) { + throw new BadRequestError({ + message: + "No signature was created. Make sure you are using an appropriate signing algorithm that uses the same hashing algorithm as the one used to create the digest." + }); + } + + return signature; + } catch (err) { + logger.error(err, "KMS: Failed to sign RSA digest"); + throw new BadRequestError({ + message: `Failed to sign RSA digest with ${signingAlgorithm} due to signing error. Ensure that your digest is hashed with ${hashAlgorithm.toUpperCase()}.` + }); + } finally { + await cleanTemporaryDirectory(tempDir); + } + }; + + const $signEccDigest = async ( + digest: Buffer, + privateKey: Buffer, + hashAlgorithm: SupportedHashAlgorithm, + signingAlgorithm: SigningAlgorithm + ) => { + const tempDir = await createTemporaryDirectory("ecc-sign"); + const digestPath = path.join(tempDir, "digest.bin"); + const keyPath = path.join(tempDir, "key.pem"); + const sigPath = path.join(tempDir, "signature.bin"); + + try { + await writeToTemporaryFile(digestPath, digest); + await writeToTemporaryFile(keyPath, privateKey); + + const { stderr } = await execFileAsync( + "openssl", + [ + "pkeyutl", + "-sign", + "-in", + digestPath, + "-inkey", + keyPath, + "-pkeyopt", + `digest:${hashAlgorithm}`, + "-out", + sigPath + ], + { + maxBuffer: 10 * 1024 * 1024, + timeout: COMMAND_TIMEOUT + } + ); + + if (stderr) { + logger.error(stderr, "KMS: Failed to sign ECC digest"); + throw new BadRequestError({ + message: "Failed to sign ECC digest due to signing error" + }); + } + + const signature = await fs.readFile(sigPath); + + if (!signature) { + throw new BadRequestError({ + message: + "No signature was created. Make sure you are using an appropriate signing algorithm that uses the same hashing algorithm as the one used to create the digest." + }); + } + + return signature; + } catch (err) { + logger.error(err, "KMS: Failed to sign ECC digest"); + throw new BadRequestError({ + message: `Failed to sign ECC digest with ${signingAlgorithm} due to signing error. Ensure that your digest is hashed with ${hashAlgorithm.toUpperCase()}.` + }); + } finally { + await cleanTemporaryDirectory(tempDir); + } + }; + + const $verifyEccDigest = async ( + digest: Buffer, + signature: Buffer, + publicKey: Buffer, + hashAlgorithm: SupportedHashAlgorithm + ) => { + const tempDir = await createTemporaryDirectory("ecc-signature-verification"); + const publicKeyFile = path.join(tempDir, "public-key.pem"); + const sigFile = path.join(tempDir, "signature.sig"); + const digestFile = path.join(tempDir, "digest.bin"); + + try { + await writeToTemporaryFile(publicKeyFile, publicKey); + await writeToTemporaryFile(sigFile, signature); + await writeToTemporaryFile(digestFile, digest); + + await execFileAsync( + "openssl", + [ + "pkeyutl", + "-verify", + "-in", + digestFile, + "-inkey", + publicKeyFile, + "-pubin", // Important for EC public keys + "-sigfile", + sigFile, + "-pkeyopt", + `digest:${hashAlgorithm}` + ], + { timeout: COMMAND_TIMEOUT } + ); + + return true; + } catch (error) { + const err = error as { stderr: string }; + + if ( + !err?.stderr?.toLowerCase()?.includes("signature verification failure") && + !err?.stderr?.toLowerCase()?.includes("bad signature") + ) { + logger.error(error, "KMS: Failed to verify ECC signature"); + } + return false; + } finally { + await cleanTemporaryDirectory(tempDir); + } + }; + + const $verifyRsaDigest = async ( + digest: Buffer, + signature: Buffer, + publicKey: Buffer, + hashAlgorithm: SupportedHashAlgorithm + ) => { + const tempDir = await createTemporaryDirectory("kms-signature-verification"); + const publicKeyFile = path.join(tempDir, "public-key.pub"); + const signatureFile = path.join(tempDir, "signature.sig"); + const digestFile = path.join(tempDir, "digest.bin"); + + try { + await writeToTemporaryFile(publicKeyFile, publicKey); + await writeToTemporaryFile(signatureFile, signature); + await writeToTemporaryFile(digestFile, digest); + + await execFileAsync( + "openssl", + [ + "pkeyutl", + "-verify", + "-in", + digestFile, + "-inkey", + publicKeyFile, + "-pubin", + "-sigfile", + signatureFile, + "-pkeyopt", + `digest:${hashAlgorithm}` + ], + { timeout: COMMAND_TIMEOUT } + ); + + // it'll throw if the verification was not successful + return true; + } catch (error) { + const err = error as { stdout: string }; + + if (!err?.stdout?.toLowerCase()?.includes("signature verification failure")) { + logger.error(error, "KMS: Failed to verify signature"); + } + return false; + } finally { + await cleanTemporaryDirectory(tempDir); + } + }; + + const verifyDigestFunctionsMap: Record< + AsymmetricKeyAlgorithm, + (data: Buffer, signature: Buffer, publicKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => Promise + > = { + [AsymmetricKeyAlgorithm.ECC_NIST_P256]: $verifyEccDigest, + [AsymmetricKeyAlgorithm.RSA_4096]: $verifyRsaDigest + }; + + const signDigestFunctionsMap: Record< + AsymmetricKeyAlgorithm, + ( + data: Buffer, + privateKey: Buffer, + hashAlgorithm: SupportedHashAlgorithm, + signingAlgorithm: SigningAlgorithm + ) => Promise + > = { + [AsymmetricKeyAlgorithm.ECC_NIST_P256]: $signEccDigest, + [AsymmetricKeyAlgorithm.RSA_4096]: $signRsaDigest + }; + + const sign = async ( + data: Buffer, + privateKey: Buffer, + signingAlgorithm: SigningAlgorithm, + isDigest: boolean + ): Promise => { + $validateAlgorithmWithKeyType(signingAlgorithm); + + const { hashAlgorithm, padding, saltLength } = $getSigningParams(signingAlgorithm); + + if (isDigest) { + if (signingAlgorithm.startsWith("RSASSA_PSS")) { + throw new BadRequestError({ + message: "RSA PSS does not support digested input" + }); + } + + const signFunction = signDigestFunctionsMap[algorithm]; + + if (!signFunction) { + throw new BadRequestError({ + message: `Digested input is not supported for key algorithm ${algorithm}` + }); + } + + const signature = await signFunction(data, privateKey, hashAlgorithm, signingAlgorithm); + return signature; + } + + const privateKeyObject = crypto.createPrivateKey({ + key: privateKey, + format: "pem", + type: "pkcs8" + }); + + // For RSA signatures + if (signingAlgorithm.startsWith("RSA")) { + const signer = crypto.createSign(hashAlgorithm); + signer.update(data); + + return signer.sign({ + key: privateKeyObject, + padding, + ...(signingAlgorithm.includes("PSS") ? { saltLength } : {}) + }); + } + if (signingAlgorithm.startsWith("ECDSA")) { + // For ECDSA signatures + const signer = crypto.createSign(hashAlgorithm); + signer.update(data); + return signer.sign({ + key: privateKeyObject, + dsaEncoding: "der" + }); + } + throw new BadRequestError({ + message: `Signing algorithm ${signingAlgorithm} not implemented` + }); + }; + + const verify = async ( + data: Buffer, + signature: Buffer, + publicKey: Buffer, + signingAlgorithm: SigningAlgorithm, + isDigest: boolean + ): Promise => { + try { + $validateAlgorithmWithKeyType(signingAlgorithm); + + const { hashAlgorithm, padding, saltLength } = $getSigningParams(signingAlgorithm); + + if (isDigest) { + if (signingAlgorithm.startsWith("RSASSA_PSS")) { + throw new BadRequestError({ + message: "RSA PSS does not support digested input" + }); + } + + const verifyFunction = verifyDigestFunctionsMap[algorithm]; + + if (!verifyFunction) { + throw new BadRequestError({ + message: `Digested input is not supported for key algorithm ${algorithm}` + }); + } + + const signatureValid = await verifyFunction(data, signature, publicKey, hashAlgorithm); + + return signatureValid; + } + + const publicKeyObject = crypto.createPublicKey({ + key: publicKey, + format: "der", + type: "spki" + }); + + // For RSA signatures + if (signingAlgorithm.startsWith("RSA")) { + const verifier = crypto.createVerify(hashAlgorithm); + verifier.update(data); + + return verifier.verify( + { + key: publicKeyObject, + padding, + ...(signingAlgorithm.includes("PSS") ? { saltLength } : {}) + }, + signature + ); + } + // For ECDSA signatures + if (signingAlgorithm.startsWith("ECDSA")) { + const verifier = crypto.createVerify(hashAlgorithm); + verifier.update(data); + return verifier.verify( + { + key: publicKeyObject, + dsaEncoding: "der" + }, + signature + ); + } + throw new BadRequestError({ + message: `Verification for algorithm ${signingAlgorithm} not implemented` + }); + } catch (error) { + if (error instanceof BadRequestError) { + throw error; + } + logger.error(error, "KMS: Failed to verify signature"); + return false; + } + }; + + const generateAsymmetricPrivateKey = async () => { + const { privateKey } = await new Promise<{ privateKey: string }>((resolve, reject) => { + if (algorithm.startsWith("RSA")) { + crypto.generateKeyPair( + "rsa", + { + modulusLength: Number(algorithm.split("_")[1]), + publicKeyEncoding: { type: "spki", format: "pem" }, + privateKeyEncoding: { type: "pkcs8", format: "pem" } + }, + (err, _, pk) => { + if (err) { + reject(err); + } else { + resolve({ privateKey: pk }); + } + } + ); + } else { + const { full: namedCurve } = $getEcCurveName(algorithm); + + crypto.generateKeyPair( + "ec", + { + namedCurve, + publicKeyEncoding: { type: "spki", format: "pem" }, + privateKeyEncoding: { type: "pkcs8", format: "pem" } + }, + (err, _, pk) => { + if (err) { + reject(err); + } else { + resolve({ + privateKey: pk + }); + } + } + ); + } + }); + + return Buffer.from(privateKey); + }; + + const getPublicKeyFromPrivateKey = (privateKey: Buffer) => { + const privateKeyObj = crypto.createPrivateKey({ + key: privateKey, + format: "pem", + type: "pkcs8" + }); + + const publicKey = crypto.createPublicKey(privateKeyObj).export({ + type: "spki", + format: "der" + }); + + return publicKey; + }; + + return { + sign, + verify, + generateAsymmetricPrivateKey, + getPublicKeyFromPrivateKey + }; +}; diff --git a/backend/src/lib/crypto/sign/types.ts b/backend/src/lib/crypto/sign/types.ts new file mode 100644 index 000000000..aa81b4057 --- /dev/null +++ b/backend/src/lib/crypto/sign/types.ts @@ -0,0 +1,45 @@ +import { z } from "zod"; + +export type TAsymmetricSignVerifyFns = { + sign: (data: Buffer, key: Buffer, signingAlgorithm: SigningAlgorithm, isDigest: boolean) => Promise; + verify: ( + data: Buffer, + signature: Buffer, + key: Buffer, + signingAlgorithm: SigningAlgorithm, + isDigest: boolean + ) => Promise; + generateAsymmetricPrivateKey: () => Promise; + getPublicKeyFromPrivateKey: (privateKey: Buffer) => Buffer; +}; + +// Supported asymmetric key types +export enum AsymmetricKeyAlgorithm { + RSA_4096 = "RSA_4096", + ECC_NIST_P256 = "ECC_NIST_P256" +} + +export const AsymmetricKeyAlgorithmEnum = z.enum( + Object.values(AsymmetricKeyAlgorithm) as [string, ...string[]] +).options; + +export enum SigningAlgorithm { + // RSA PSS algorithms + // These are NOT deterministic and include randomness. + // This means that the output signature is different each time for the same input. + RSASSA_PSS_SHA_512 = "RSASSA_PSS_SHA_512", + RSASSA_PSS_SHA_384 = "RSASSA_PSS_SHA_384", + RSASSA_PSS_SHA_256 = "RSASSA_PSS_SHA_256", + + // RSA PKCS#1 v1.5 algorithms + // These are deterministic and the output is the same each time for the same input. + RSASSA_PKCS1_V1_5_SHA_512 = "RSASSA_PKCS1_V1_5_SHA_512", + RSASSA_PKCS1_V1_5_SHA_384 = "RSASSA_PKCS1_V1_5_SHA_384", + RSASSA_PKCS1_V1_5_SHA_256 = "RSASSA_PKCS1_V1_5_SHA_256", + + // ECDSA algorithms + // None of these are deterministic and include randomness like RSA PSS. + ECDSA_SHA_512 = "ECDSA_SHA_512", + ECDSA_SHA_384 = "ECDSA_SHA_384", + ECDSA_SHA_256 = "ECDSA_SHA_256" +} diff --git a/backend/src/lib/error-codes/database.ts b/backend/src/lib/error-codes/database.ts new file mode 100644 index 000000000..1a8dd41ae --- /dev/null +++ b/backend/src/lib/error-codes/database.ts @@ -0,0 +1,4 @@ +export enum DatabaseErrorCode { + ForeignKeyViolation = "23503", + UniqueViolation = "23505" +} diff --git a/backend/src/lib/error-codes/index.ts b/backend/src/lib/error-codes/index.ts new file mode 100644 index 000000000..c30cd664d --- /dev/null +++ b/backend/src/lib/error-codes/index.ts @@ -0,0 +1 @@ +export * from "./database"; diff --git a/backend/src/lib/errors/index.ts b/backend/src/lib/errors/index.ts index 0818cfe7d..a5df64caf 100644 --- a/backend/src/lib/errors/index.ts +++ b/backend/src/lib/errors/index.ts @@ -1,4 +1,5 @@ /* eslint-disable max-classes-per-file */ + export class DatabaseError extends Error { name: string; @@ -52,10 +53,35 @@ export class ForbiddenRequestError extends Error { error: unknown; - constructor({ name, error, message }: { message?: string; name?: string; error?: unknown } = {}) { + details?: unknown; + + constructor({ + name, + error, + message, + details + }: { message?: string; name?: string; error?: unknown; details?: unknown } = {}) { super(message ?? "You are not allowed to access this resource"); this.name = name || "ForbiddenError"; this.error = error; + this.details = details; + } +} + +export class PermissionBoundaryError extends ForbiddenRequestError { + constructor({ + message, + name, + error, + details + }: { + message?: string; + name?: string; + error?: unknown; + details?: unknown; + }) { + super({ message, name, error, details }); + this.name = "PermissionBoundaryError"; } } @@ -133,3 +159,15 @@ export class ScimRequestError extends Error { this.status = status; } } + +export class OidcAuthError extends Error { + name: string; + + error: unknown; + + constructor({ name, error, message }: { message?: string; name?: string; error?: unknown }) { + super(message || "Something went wrong"); + this.name = name || "OidcAuthError"; + this.error = error; + } +} diff --git a/backend/src/lib/files/files.ts b/backend/src/lib/files/files.ts new file mode 100644 index 000000000..063d71d09 --- /dev/null +++ b/backend/src/lib/files/files.ts @@ -0,0 +1,35 @@ +import crypto from "crypto"; +import fs from "fs/promises"; +import os from "os"; +import path from "path"; + +import { logger } from "@app/lib/logger"; + +const baseDir = path.join(os.tmpdir(), "infisical"); +const randomPath = () => `${crypto.randomBytes(32).toString("hex")}`; + +export const createTemporaryDirectory = async (name: string) => { + const tempDirPath = path.join(baseDir, `${name}-${randomPath()}`); + await fs.mkdir(tempDirPath, { recursive: true }); + + return tempDirPath; +}; + +export const removeTemporaryBaseDirectory = async () => { + await fs.rm(baseDir, { force: true, recursive: true }).catch((err) => { + logger.error(err, `Failed to remove temporary base directory [path=${baseDir}]`); + }); +}; + +export const cleanTemporaryDirectory = async (dirPath: string) => { + await fs.rm(dirPath, { recursive: true, force: true }).catch((err) => { + logger.error(err, `Failed to cleanup temporary directory [path=${dirPath}]`); + }); +}; + +export const writeToTemporaryFile = async (tempDirPath: string, data: string | Buffer) => { + await fs.writeFile(tempDirPath, data, { mode: 0o600 }).catch((err) => { + logger.error(err, `Failed to write to temporary file [path=${tempDirPath}]`); + throw err; + }); +}; diff --git a/backend/src/lib/files/index.ts b/backend/src/lib/files/index.ts new file mode 100644 index 000000000..b2cba4b62 --- /dev/null +++ b/backend/src/lib/files/index.ts @@ -0,0 +1 @@ +export * from "./files"; diff --git a/backend/src/lib/fn/string.ts b/backend/src/lib/fn/string.ts index 26e8f27df..2fa4c9166 100644 --- a/backend/src/lib/fn/string.ts +++ b/backend/src/lib/fn/string.ts @@ -1,4 +1,5 @@ import path from "path"; +import RE2 from "re2"; // given two paths irrespective of ending with / or not // this will return true if its equal @@ -14,3 +15,7 @@ export const prefixWithSlash = (str: string) => { if (str.startsWith("/")) return str; return `/${str}`; }; + +const vowelRegex = new RE2(/^[aeiou]/i); + +export const startsWithVowel = (str: string) => vowelRegex.test(str); diff --git a/backend/src/lib/gateway/index.ts b/backend/src/lib/gateway/index.ts new file mode 100644 index 000000000..84d801dda --- /dev/null +++ b/backend/src/lib/gateway/index.ts @@ -0,0 +1,354 @@ +/* eslint-disable no-await-in-loop */ +import crypto from "node:crypto"; +import net from "node:net"; + +import quicDefault, * as quicModule from "@infisical/quic"; + +import { BadRequestError } from "../errors"; +import { logger } from "../logger"; + +const DEFAULT_MAX_RETRIES = 3; +const DEFAULT_RETRY_DELAY = 1000; // 1 second + +const quic = quicDefault || quicModule; + +const parseSubjectDetails = (data: string) => { + const values: Record = {}; + data.split("\n").forEach((el) => { + const [key, value] = el.split("="); + values[key.trim()] = value.trim(); + }); + return values; +}; + +type TTlsOption = { ca: string; cert: string; key: string }; + +const createQuicConnection = async ( + relayHost: string, + relayPort: number, + tlsOptions: TTlsOption, + identityId: string, + orgId: string +) => { + const client = await quic.QUICClient.createQUICClient({ + host: relayHost, + port: relayPort, + config: { + ca: tlsOptions.ca, + cert: tlsOptions.cert, + key: tlsOptions.key, + applicationProtos: ["infisical-gateway"], + verifyPeer: true, + verifyCallback: async (certs) => { + if (!certs || certs.length === 0) return quic.native.CryptoError.CertificateRequired; + const serverCertificate = new crypto.X509Certificate(Buffer.from(certs[0])); + const caCertificate = new crypto.X509Certificate(tlsOptions.ca); + const isValidServerCertificate = serverCertificate.checkIssued(caCertificate); + if (!isValidServerCertificate) return quic.native.CryptoError.BadCertificate; + + const subjectDetails = parseSubjectDetails(serverCertificate.subject); + if (subjectDetails.OU !== "Gateway" || subjectDetails.CN !== identityId || subjectDetails.O !== orgId) { + return quic.native.CryptoError.CertificateUnknown; + } + + if (new Date() > new Date(serverCertificate.validTo) || new Date() < new Date(serverCertificate.validFrom)) { + return quic.native.CryptoError.CertificateExpired; + } + + const formatedRelayHost = + process.env.NODE_ENV === "development" ? relayHost.replace("host.docker.internal", "127.0.0.1") : relayHost; + if (!serverCertificate.checkIP(formatedRelayHost)) return quic.native.CryptoError.BadCertificate; + }, + maxIdleTimeout: 90000, + keepAliveIntervalTime: 30000 + }, + crypto: { + ops: { + randomBytes: async (data) => { + crypto.getRandomValues(new Uint8Array(data)); + } + } + } + }); + return client; +}; + +type TPingGatewayAndVerifyDTO = { + relayHost: string; + relayPort: number; + tlsOptions: TTlsOption; + maxRetries?: number; + identityId: string; + orgId: string; +}; + +export const pingGatewayAndVerify = async ({ + relayHost, + relayPort, + tlsOptions, + maxRetries = DEFAULT_MAX_RETRIES, + identityId, + orgId +}: TPingGatewayAndVerifyDTO) => { + let lastError: Error | null = null; + const quicClient = await createQuicConnection(relayHost, relayPort, tlsOptions, identityId, orgId).catch((err) => { + throw new BadRequestError({ + message: (err as Error)?.message, + error: err as Error + }); + }); + + for (let attempt = 1; attempt <= maxRetries; attempt += 1) { + try { + const stream = quicClient.connection.newStream("bidi"); + const pingWriter = stream.writable.getWriter(); + await pingWriter.write(Buffer.from("PING\n")); + pingWriter.releaseLock(); + + // Read PONG response + const reader = stream.readable.getReader(); + const { value, done } = await reader.read(); + + if (done) { + throw new Error("Gateway closed before receiving PONG"); + } + + const response = Buffer.from(value).toString(); + + if (response !== "PONG\n" && response !== "PONG") { + throw new Error(`Failed to Ping. Unexpected response: ${response}`); + } + + reader.releaseLock(); + return; + } catch (err) { + lastError = err as Error; + + if (attempt < maxRetries) { + await new Promise((resolve) => { + setTimeout(resolve, DEFAULT_RETRY_DELAY); + }); + } + } finally { + await quicClient.destroy(); + } + } + + logger.error(lastError); + throw new BadRequestError({ + message: `Failed to ping gateway after ${maxRetries} attempts. Last error: ${lastError?.message}` + }); +}; + +interface TProxyServer { + server: net.Server; + port: number; + cleanup: () => Promise; + getProxyError: () => string; +} + +const setupProxyServer = async ({ + targetPort, + targetHost, + tlsOptions, + relayHost, + relayPort, + identityId, + orgId +}: { + targetHost: string; + targetPort: number; + relayPort: number; + relayHost: string; + tlsOptions: TTlsOption; + identityId: string; + orgId: string; +}): Promise => { + const quicClient = await createQuicConnection(relayHost, relayPort, tlsOptions, identityId, orgId).catch((err) => { + throw new BadRequestError({ + error: err as Error + }); + }); + const proxyErrorMsg = [""]; + + return new Promise((resolve, reject) => { + const server = net.createServer(); + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + server.on("connection", async (clientConn) => { + try { + clientConn.setKeepAlive(true, 30000); // 30 seconds + clientConn.setNoDelay(true); + + const stream = quicClient.connection.newStream("bidi"); + // Send FORWARD-TCP command + const forwardWriter = stream.writable.getWriter(); + await forwardWriter.write(Buffer.from(`FORWARD-TCP ${targetHost}:${targetPort}\n`)); + forwardWriter.releaseLock(); + + // Set up bidirectional copy + const setupCopy = () => { + // Client to QUIC + // eslint-disable-next-line + (async () => { + const writer = stream.writable.getWriter(); + + // Create a handler for client data + clientConn.on("data", (chunk) => { + writer.write(chunk).catch((err) => { + proxyErrorMsg.push((err as Error)?.message); + }); + }); + + // Handle client connection close + clientConn.on("end", () => { + writer.close().catch((err) => { + logger.error(err); + }); + }); + + clientConn.on("error", (clientConnErr) => { + writer.abort(clientConnErr?.message).catch((err) => { + proxyErrorMsg.push((err as Error)?.message); + }); + }); + })(); + + // QUIC to Client + void (async () => { + try { + const reader = stream.readable.getReader(); + + let reading = true; + while (reading) { + const { value, done } = await reader.read(); + + if (done) { + reading = false; + clientConn.end(); // Close client connection when QUIC stream ends + break; + } + + // Write data to TCP client + const canContinue = clientConn.write(Buffer.from(value)); + + // Handle backpressure + if (!canContinue) { + await new Promise((res) => { + clientConn.once("drain", res); + }); + } + } + } catch (err) { + proxyErrorMsg.push((err as Error)?.message); + clientConn.destroy(); + } + })(); + }; + + setupCopy(); + // Handle connection closure + clientConn.on("close", () => { + stream.destroy().catch((err) => { + proxyErrorMsg.push((err as Error)?.message); + }); + }); + + const cleanup = async () => { + clientConn?.destroy(); + await stream.destroy(); + }; + + clientConn.on("error", (clientConnErr) => { + logger.error(clientConnErr, "Client socket error"); + cleanup().catch((err) => { + logger.error(err, "Client conn cleanup"); + }); + }); + + clientConn.on("end", () => { + cleanup().catch((err) => { + logger.error(err, "Client conn end"); + }); + }); + } catch (err) { + logger.error(err, "Failed to establish target connection:"); + clientConn.end(); + reject(err); + } + }); + + server.on("error", (err) => { + reject(err); + }); + + server.on("close", () => { + quicClient?.destroy().catch((err) => { + logger.error(err, "Failed to destroy quic client"); + }); + }); + + server.listen(0, () => { + const address = server.address(); + if (!address || typeof address === "string") { + server.close(); + reject(new Error("Failed to get server port")); + return; + } + + logger.info("Gateway proxy started"); + resolve({ + server, + port: address.port, + cleanup: async () => { + server.close(); + await quicClient?.destroy(); + }, + getProxyError: () => proxyErrorMsg.join(",") + }); + }); + }); +}; + +interface ProxyOptions { + targetHost: string; + targetPort: number; + relayHost: string; + relayPort: number; + tlsOptions: TTlsOption; + identityId: string; + orgId: string; +} + +export const withGatewayProxy = async ( + callback: (port: number) => Promise, + options: ProxyOptions +): Promise => { + const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId } = options; + + // Setup the proxy server + const { port, cleanup, getProxyError } = await setupProxyServer({ + targetHost, + targetPort, + relayPort, + relayHost, + tlsOptions, + identityId, + orgId + }); + + try { + // Execute the callback with the allocated port + await callback(port); + } catch (err) { + const proxyErrorMessage = getProxyError(); + if (proxyErrorMessage) { + logger.error(new Error(proxyErrorMessage), "Failed to proxy"); + } + logger.error(err, "Failed to do gateway"); + throw new BadRequestError({ message: proxyErrorMessage || (err as Error)?.message }); + } finally { + // Ensure cleanup happens regardless of success or failure + await cleanup(); + } +}; diff --git a/backend/src/lib/ip/index.ts b/backend/src/lib/ip/index.ts index 6503165f6..0b35a2759 100644 --- a/backend/src/lib/ip/index.ts +++ b/backend/src/lib/ip/index.ts @@ -103,6 +103,10 @@ export const isValidIpOrCidr = (ip: string): boolean => { return false; }; +export const isValidIp = (ip: string) => { + return net.isIPv4(ip) || net.isIPv6(ip); +}; + export type TIp = { ipAddress: string; type: IPType; diff --git a/backend/src/lib/ip/ipRange.ts b/backend/src/lib/ip/ipRange.ts new file mode 100644 index 000000000..c2a77d6f9 --- /dev/null +++ b/backend/src/lib/ip/ipRange.ts @@ -0,0 +1,61 @@ +import { BlockList } from "node:net"; + +import { BadRequestError } from "../errors"; +// Define BlockList instances for each range type +const ipv4RangeLists: Record = { + unspecified: new BlockList(), + broadcast: new BlockList(), + multicast: new BlockList(), + linkLocal: new BlockList(), + loopback: new BlockList(), + carrierGradeNat: new BlockList(), + private: new BlockList(), + reserved: new BlockList() +}; + +// Add IPv4 CIDR ranges to each BlockList +ipv4RangeLists.unspecified.addSubnet("0.0.0.0", 8); +ipv4RangeLists.broadcast.addAddress("255.255.255.255"); +ipv4RangeLists.multicast.addSubnet("224.0.0.0", 4); +ipv4RangeLists.linkLocal.addSubnet("169.254.0.0", 16); +ipv4RangeLists.loopback.addSubnet("127.0.0.0", 8); +ipv4RangeLists.carrierGradeNat.addSubnet("100.64.0.0", 10); + +// IPv4 Private ranges +ipv4RangeLists.private.addSubnet("10.0.0.0", 8); +ipv4RangeLists.private.addSubnet("172.16.0.0", 12); +ipv4RangeLists.private.addSubnet("192.168.0.0", 16); + +// IPv4 Reserved ranges +ipv4RangeLists.reserved.addSubnet("192.0.0.0", 24); +ipv4RangeLists.reserved.addSubnet("192.0.2.0", 24); +ipv4RangeLists.reserved.addSubnet("192.88.99.0", 24); +ipv4RangeLists.reserved.addSubnet("198.18.0.0", 15); +ipv4RangeLists.reserved.addSubnet("198.51.100.0", 24); +ipv4RangeLists.reserved.addSubnet("203.0.113.0", 24); +ipv4RangeLists.reserved.addSubnet("240.0.0.0", 4); + +/** + * Checks if an IP address (IPv4) is private or public + * inspired by: https://github.com/whitequark/ipaddr.js/blob/main/lib/ipaddr.js + */ +export const getIpRange = (ip: string): string => { + try { + const rangeLists = ipv4RangeLists; + // Check each range type + for (const rangeName in rangeLists) { + if (Object.hasOwn(rangeLists, rangeName)) { + if (rangeLists[rangeName].check(ip)) { + return rangeName; + } + } + } + + // If no range matched, it's a public address + return "unicast"; + } catch (error) { + throw new BadRequestError({ message: "Invalid IP address", error }); + } +}; + +export const isPrivateIp = (ip: string) => getIpRange(ip) !== "unicast"; diff --git a/backend/src/lib/knex/connection.ts b/backend/src/lib/knex/connection.ts index 993615a0b..68b40e18f 100644 --- a/backend/src/lib/knex/connection.ts +++ b/backend/src/lib/knex/connection.ts @@ -1,6 +1,8 @@ import { URL } from "url"; // Import the URL class -export const getDbConnectionHost = (urlString: string) => { +export const getDbConnectionHost = (urlString?: string) => { + if (!urlString) return null; + try { const url = new URL(urlString); // Split hostname and port (if provided) diff --git a/backend/src/lib/knex/dynamic.ts b/backend/src/lib/knex/dynamic.ts index b8bc8ab57..a57464fac 100644 --- a/backend/src/lib/knex/dynamic.ts +++ b/backend/src/lib/knex/dynamic.ts @@ -2,11 +2,17 @@ import { Knex } from "knex"; import { UnauthorizedError } from "../errors"; -type TKnexDynamicPrimitiveOperator = { - operator: "eq" | "ne" | "startsWith" | "endsWith"; - value: string; - field: Extract; -}; +type TKnexDynamicPrimitiveOperator = + | { + operator: "eq" | "ne" | "startsWith" | "endsWith"; + value: string; + field: Extract; + } + | { + operator: "notIn"; + value: string[]; + field: Extract; + }; type TKnexDynamicInOperator = { operator: "in"; @@ -48,6 +54,10 @@ export const buildDynamicKnexQuery = ( void queryBuilder.whereILike(filterAst.field, `%${filterAst.value}`); break; } + case "notIn": { + void queryBuilder.whereNotIn(filterAst.field, filterAst.value); + break; + } case "and": { filterAst.value.forEach((el) => { void queryBuilder.andWhere((subQueryBuilder) => { diff --git a/backend/src/lib/knex/index.ts b/backend/src/lib/knex/index.ts index f55d8e6e6..d43d2af8e 100644 --- a/backend/src/lib/knex/index.ts +++ b/backend/src/lib/knex/index.ts @@ -7,6 +7,7 @@ import { buildDynamicKnexQuery, TKnexDynamicOperator } from "./dynamic"; export * from "./connection"; export * from "./join"; +export * from "./prependTableNameToFindFilter"; export * from "./select"; export const withTransaction = (db: Knex, dal: K) => ({ @@ -20,11 +21,12 @@ export const withTransaction = (db: Knex, dal: K) => ({ export type TFindFilter = Partial & { $in?: Partial<{ [k in keyof R]: R[k][] }>; + $notNull?: Array; $search?: Partial<{ [k in keyof R]: R[k] }>; $complex?: TKnexDynamicOperator; }; export const buildFindFilter = - ({ $in, $search, $complex, ...filter }: TFindFilter) => + ({ $in, $notNull, $search, $complex, ...filter }: TFindFilter) => (bd: Knex.QueryBuilder) => { void bd.where(filter); if ($in) { @@ -34,6 +36,13 @@ export const buildFindFilter = } }); } + + if ($notNull?.length) { + $notNull.forEach((key) => { + void bd.whereNotNull(key as never); + }); + } + if ($search) { Object.entries($search).forEach(([key, val]) => { if (val) { diff --git a/backend/src/lib/knex/prependTableNameToFindFilter.ts b/backend/src/lib/knex/prependTableNameToFindFilter.ts new file mode 100644 index 000000000..3fb1dabb3 --- /dev/null +++ b/backend/src/lib/knex/prependTableNameToFindFilter.ts @@ -0,0 +1,13 @@ +import { TableName } from "@app/db/schemas"; +import { buildFindFilter } from "@app/lib/knex/index"; + +type TFindFilterParameters = Parameters>[0]; + +export const prependTableNameToFindFilter = (tableName: TableName, filterObj: object): TFindFilterParameters => + Object.fromEntries( + Object.entries(filterObj).map(([key, value]) => + key.startsWith("$") + ? [key, value ? prependTableNameToFindFilter(tableName, value as object) : value] + : [`${tableName}.${key}`, value] + ) + ); diff --git a/backend/src/lib/logger/logger.ts b/backend/src/lib/logger/logger.ts index 942efc40a..170a0285f 100644 --- a/backend/src/lib/logger/logger.ts +++ b/backend/src/lib/logger/logger.ts @@ -1,6 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ // logger follows a singleton pattern // easier to use it that's all. +import { requestContext } from "@fastify/request-context"; import pino, { Logger } from "pino"; import { z } from "zod"; @@ -13,14 +15,37 @@ const logLevelToSeverityLookup: Record = { "60": "CRITICAL" }; -// eslint-disable-next-line import/no-mutable-exports -export let logger: Readonly; // akhilmhdh: // The logger is not placed in the main app config to avoid a circular dependency. // The config requires the logger to display errors when an invalid environment is supplied. // On the other hand, the logger needs the config to obtain credentials for AWS or other transports. // By keeping the logger separate, it becomes an independent package. +// We define our own custom logger interface to enforce structure to the logging methods. + +export interface CustomLogger extends Omit { + info: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (obj: unknown, msg?: string, ...args: any[]): void; + }; + + error: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (obj: unknown, msg?: string, ...args: any[]): void; + }; + warn: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (obj: unknown, msg?: string, ...args: any[]): void; + }; + debug: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (obj: unknown, msg?: string, ...args: any[]): void; + }; +} + +// eslint-disable-next-line import/no-mutable-exports +export let logger: Readonly; + const loggerConfig = z.object({ AWS_CLOUDWATCH_LOG_GROUP_NAME: z.string().default("infisical-log-stream"), AWS_CLOUDWATCH_LOG_REGION: z.string().default("us-east-1"), @@ -62,7 +87,18 @@ const redactedKeys = [ "config" ]; -export const initLogger = async () => { +const UNKNOWN_REQUEST_ID = "UNKNOWN_REQUEST_ID"; + +const extractReqId = () => { + try { + return requestContext.get("reqId") || UNKNOWN_REQUEST_ID; + } catch (err) { + console.log("failed to get request context", err); + return UNKNOWN_REQUEST_ID; + } +}; + +export const initLogger = () => { const cfg = loggerConfig.parse(process.env); const targets: pino.TransportMultiOptions["targets"][number][] = [ { @@ -94,6 +130,30 @@ export const initLogger = async () => { targets }); + const wrapLogger = (originalLogger: Logger): CustomLogger => { + // eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any + originalLogger.info = (obj: unknown, msg?: string, ...args: any[]) => { + return originalLogger.child({ reqId: extractReqId() }).info(obj, msg, ...args); + }; + + // eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any + originalLogger.error = (obj: unknown, msg?: string, ...args: any[]) => { + return originalLogger.child({ reqId: extractReqId() }).error(obj, msg, ...args); + }; + + // eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any + originalLogger.warn = (obj: unknown, msg?: string, ...args: any[]) => { + return originalLogger.child({ reqId: extractReqId() }).warn(obj, msg, ...args); + }; + + // eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any + originalLogger.debug = (obj: unknown, msg?: string, ...args: any[]) => { + return originalLogger.child({ reqId: extractReqId() }).debug(obj, msg, ...args); + }; + + return originalLogger; + }; + logger = pino( { mixin(_context, level) { @@ -113,5 +173,6 @@ export const initLogger = async () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument transport ); - return logger; + + return wrapLogger(logger); }; diff --git a/backend/src/lib/ms/index.ts b/backend/src/lib/ms/index.ts new file mode 100644 index 000000000..5a0d165cd --- /dev/null +++ b/backend/src/lib/ms/index.ts @@ -0,0 +1,15 @@ +import msFn, { StringValue } from "ms"; + +import { BadRequestError } from "../errors"; + +export const ms = (val: string) => { + if (typeof val !== "string") { + throw new BadRequestError({ message: `Date must be string` }); + } + + try { + return msFn(val as StringValue); + } catch { + throw new BadRequestError({ message: `Invalid date format string: ${val}` }); + } +}; diff --git a/backend/src/lib/search-resource/db.ts b/backend/src/lib/search-resource/db.ts new file mode 100644 index 000000000..fc450d9f9 --- /dev/null +++ b/backend/src/lib/search-resource/db.ts @@ -0,0 +1,141 @@ +import { Knex } from "knex"; + +import { SearchResourceOperators, TSearchResourceOperator } from "./search"; + +const buildKnexQuery = ( + query: Knex.QueryBuilder, + // when it's multiple table field means it's field1 or field2 + fields: string | string[], + operator: SearchResourceOperators, + value: unknown +) => { + switch (operator) { + case SearchResourceOperators.$eq: { + if (typeof value !== "string" && typeof value !== "number") + throw new Error("Invalid value type for $eq operator"); + + if (typeof fields === "string") { + return void query.where(fields, "=", value); + } + + return void query.where((qb) => { + return fields.forEach((el, index) => { + if (index === 0) { + return void qb.where(el, "=", value); + } + return void qb.orWhere(el, "=", value); + }); + }); + } + + case SearchResourceOperators.$neq: { + if (typeof value !== "string" && typeof value !== "number") + throw new Error("Invalid value type for $neq operator"); + + if (typeof fields === "string") { + return void query.where(fields, "<>", value); + } + + return void query.where((qb) => { + return fields.forEach((el, index) => { + if (index === 0) { + return void qb.where(el, "<>", value); + } + return void qb.orWhere(el, "<>", value); + }); + }); + } + case SearchResourceOperators.$in: { + if (!Array.isArray(value)) throw new Error("Invalid value type for $in operator"); + + if (typeof fields === "string") { + return void query.whereIn(fields, value); + } + + return void query.where((qb) => { + return fields.forEach((el, index) => { + if (index === 0) { + return void qb.whereIn(el, value); + } + return void qb.orWhereIn(el, value); + }); + }); + } + case SearchResourceOperators.$contains: { + if (typeof value !== "string") throw new Error("Invalid value type for $contains operator"); + + if (typeof fields === "string") { + return void query.whereILike(fields, `%${value}%`); + } + + return void query.where((qb) => { + return fields.forEach((el, index) => { + if (index === 0) { + return void qb.whereILike(el, `%${value}%`); + } + return void qb.orWhereILike(el, `%${value}%`); + }); + }); + } + default: + throw new Error(`Unsupported operator: ${String(operator)}`); + } +}; + +export const buildKnexFilterForSearchResource = ( + rootQuery: Knex.QueryBuilder, + searchFilter: T & { $or?: T[] }, + getAttributeField: (attr: K) => string | string[] | null +) => { + const { $or: orFilters = [] } = searchFilter; + (Object.keys(searchFilter) as K[]).forEach((key) => { + // akhilmhdh: yes, we could have split in top. This is done to satisfy ts type error + if (key === "$or") return; + + const dbField = getAttributeField(key); + if (!dbField) throw new Error(`DB field not found for ${String(key)}`); + + const dbValue = searchFilter[key]; + if (typeof dbValue === "string" || typeof dbValue === "number") { + buildKnexQuery(rootQuery, dbField, SearchResourceOperators.$eq, dbValue); + return; + } + + Object.keys(dbValue as Record).forEach((el) => { + buildKnexQuery( + rootQuery, + dbField, + el as SearchResourceOperators, + (dbValue as Record)[el as SearchResourceOperators] + ); + }); + }); + + if (orFilters.length) { + void rootQuery.andWhere((andQb) => { + return orFilters.forEach((orFilter) => { + return void andQb.orWhere((qb) => { + (Object.keys(orFilter) as K[]).forEach((key) => { + const dbField = getAttributeField(key); + if (!dbField) throw new Error(`DB field not found for ${String(key)}`); + + const dbValue = orFilter[key]; + if (typeof dbValue === "string" || typeof dbValue === "number") { + buildKnexQuery(qb, dbField, SearchResourceOperators.$eq, dbValue); + return; + } + + Object.keys(dbValue as Record).forEach((el) => { + buildKnexQuery( + qb, + dbField, + el as SearchResourceOperators, + (dbValue as Record)[el as SearchResourceOperators] + ); + }); + }); + }); + }); + }); + } +}; diff --git a/backend/src/lib/search-resource/search.ts b/backend/src/lib/search-resource/search.ts new file mode 100644 index 000000000..6431bf953 --- /dev/null +++ b/backend/src/lib/search-resource/search.ts @@ -0,0 +1,43 @@ +import { z } from "zod"; + +export enum SearchResourceOperators { + $eq = "$eq", + $neq = "$neq", + $in = "$in", + $contains = "$contains" +} + +export const SearchResourceOperatorSchema = z.union([ + z.string(), + z.number(), + z + .object({ + [SearchResourceOperators.$eq]: z.string().optional(), + [SearchResourceOperators.$neq]: z.string().optional(), + [SearchResourceOperators.$in]: z.string().array().optional(), + [SearchResourceOperators.$contains]: z.string().array().optional() + }) + .partial() +]); + +export type TSearchResourceOperator = z.infer; + +export type TSearchResource = { + [k: string]: z.ZodOptional< + z.ZodUnion< + [ + z.ZodEffects, + z.ZodObject<{ + [SearchResourceOperators.$eq]?: z.ZodOptional>; + [SearchResourceOperators.$neq]?: z.ZodOptional>; + [SearchResourceOperators.$in]?: z.ZodOptional>>; + [SearchResourceOperators.$contains]?: z.ZodOptional>; + }> + ] + > + >; +}; + +export const buildSearchZodSchema = (schema: z.ZodObject) => { + return schema.extend({ $or: schema.array().max(5).optional() }).optional(); +}; diff --git a/backend/src/lib/telemetry/instrumentation.ts b/backend/src/lib/telemetry/instrumentation.ts new file mode 100644 index 000000000..faa7560d3 --- /dev/null +++ b/backend/src/lib/telemetry/instrumentation.ts @@ -0,0 +1,103 @@ +import opentelemetry, { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api"; +import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto"; +import { PrometheusExporter } from "@opentelemetry/exporter-prometheus"; +import { registerInstrumentations } from "@opentelemetry/instrumentation"; +import { HttpInstrumentation } from "@opentelemetry/instrumentation-http"; +import { Resource } from "@opentelemetry/resources"; +import { AggregationTemporality, MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"; +import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions"; +import tracer from "dd-trace"; +import dotenv from "dotenv"; + +import { initEnvConfig } from "../config/env"; + +dotenv.config(); + +const initTelemetryInstrumentation = ({ + exportType, + otlpURL, + otlpUser, + otlpPassword, + otlpPushInterval +}: { + exportType?: string; + otlpURL?: string; + otlpUser?: string; + otlpPassword?: string; + otlpPushInterval?: number; +}) => { + diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG); + + const resource = Resource.default().merge( + new Resource({ + [ATTR_SERVICE_NAME]: "infisical-core", + [ATTR_SERVICE_VERSION]: "0.1.0" + }) + ); + + const metricReaders = []; + switch (exportType) { + case "prometheus": { + const promExporter = new PrometheusExporter(); + metricReaders.push(promExporter); + break; + } + case "otlp": { + const otlpExporter = new OTLPMetricExporter({ + url: `${otlpURL}/v1/metrics`, + headers: { + Authorization: `Basic ${btoa(`${otlpUser}:${otlpPassword}`)}` + }, + temporalityPreference: AggregationTemporality.DELTA + }); + metricReaders.push( + new PeriodicExportingMetricReader({ + exporter: otlpExporter, + exportIntervalMillis: otlpPushInterval + }) + ); + break; + } + default: + throw new Error("Invalid OTEL export type"); + } + + const meterProvider = new MeterProvider({ + resource, + readers: metricReaders + }); + + opentelemetry.metrics.setGlobalMeterProvider(meterProvider); + + registerInstrumentations({ + instrumentations: [new HttpInstrumentation()] + }); +}; + +const setupTelemetry = () => { + const appCfg = initEnvConfig(); + + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + console.log("Initializing telemetry instrumentation"); + initTelemetryInstrumentation({ + otlpURL: appCfg.OTEL_EXPORT_OTLP_ENDPOINT, + otlpUser: appCfg.OTEL_COLLECTOR_BASIC_AUTH_USERNAME, + otlpPassword: appCfg.OTEL_COLLECTOR_BASIC_AUTH_PASSWORD, + otlpPushInterval: appCfg.OTEL_OTLP_PUSH_INTERVAL, + exportType: appCfg.OTEL_EXPORT_TYPE + }); + } + + if (appCfg.SHOULD_USE_DATADOG_TRACER) { + console.log("Initializing Datadog tracer"); + tracer.init({ + profiling: appCfg.DATADOG_PROFILING_ENABLED, + version: appCfg.INFISICAL_PLATFORM_VERSION, + env: appCfg.DATADOG_ENV, + service: appCfg.DATADOG_SERVICE, + hostname: appCfg.DATADOG_HOSTNAME + }); + } +}; + +void setupTelemetry(); diff --git a/backend/src/lib/template/dot-access.ts b/backend/src/lib/template/dot-access.ts new file mode 100644 index 000000000..ec3208feb --- /dev/null +++ b/backend/src/lib/template/dot-access.ts @@ -0,0 +1,34 @@ +/** + * Safely retrieves a value from a nested object using dot notation path + */ +export const getStringValueByDot = ( + obj: Record | null | undefined, + path: string, + defaultValue?: string +): string | undefined => { + // Handle null or undefined input + if (!obj) { + return defaultValue; + } + + const parts = path.split("."); + let current: unknown = obj; + + for (const part of parts) { + const isObject = typeof current === "object" && !Array.isArray(current) && current !== null; + if (!isObject) { + return defaultValue; + } + if (!Object.hasOwn(current as object, part)) { + // Check if the property exists as an own property + return defaultValue; + } + current = (current as Record)[part]; + } + + if (typeof current !== "string") { + return defaultValue; + } + + return current; +}; diff --git a/backend/src/lib/template/validate-handlebars.ts b/backend/src/lib/template/validate-handlebars.ts new file mode 100644 index 000000000..a83c9efc2 --- /dev/null +++ b/backend/src/lib/template/validate-handlebars.ts @@ -0,0 +1,21 @@ +import handlebars from "handlebars"; + +import { BadRequestError } from "../errors"; +import { logger } from "../logger"; + +type SanitizationArg = { + allowedExpressions?: (arg: string) => boolean; +}; + +export const validateHandlebarTemplate = (templateName: string, template: string, dto: SanitizationArg) => { + const parsedAst = handlebars.parse(template); + parsedAst.body.forEach((el) => { + if (el.type === "ContentStatement") return; + if (el.type === "MustacheStatement" && "path" in el) { + const { path } = el as { type: "MustacheStatement"; path: { type: "PathExpression"; original: string } }; + if (path.type === "PathExpression" && dto?.allowedExpressions?.(path.original)) return; + } + logger.error(el, "Template sanitization failed"); + throw new BadRequestError({ message: `Template sanitization failed: ${templateName}` }); + }); +}; diff --git a/backend/src/lib/turn/credentials.ts b/backend/src/lib/turn/credentials.ts new file mode 100644 index 000000000..37dcaa78b --- /dev/null +++ b/backend/src/lib/turn/credentials.ts @@ -0,0 +1,16 @@ +import crypto from "node:crypto"; + +const TURN_TOKEN_TTL = 24 * 60 * 60 * 1000; // 24 hours in milliseconds +export const getTurnCredentials = (id: string, authSecret: string, ttl = TURN_TOKEN_TTL) => { + const timestamp = Math.floor((Date.now() + ttl) / 1000); + const username = `${timestamp}:${id}`; + + const hmac = crypto.createHmac("sha1", authSecret); + hmac.update(username); + const password = hmac.digest("base64"); + + return { + username, + password + }; +}; diff --git a/backend/src/lib/types/index.ts b/backend/src/lib/types/index.ts index 6ebf91f36..9f063172f 100644 --- a/backend/src/lib/types/index.ts +++ b/backend/src/lib/types/index.ts @@ -41,8 +41,22 @@ export type RequiredKeys = { [K in keyof T]-?: undefined extends T[K] ? never : K; }[keyof T]; +export type BufferKeysToString = { + [K in keyof T]: T[K] extends Buffer + ? string + : T[K] extends Buffer | null + ? string | null + : T[K] extends Buffer | undefined + ? string | undefined + : T[K] extends Buffer | null | undefined + ? string | null | undefined + : T[K]; +}; + export type PickRequired = Pick>; +export type DiscriminativePick = T extends unknown ? Pick : never; + export enum EnforcementLevel { Hard = "hard", Soft = "soft" diff --git a/backend/src/lib/validator/index.ts b/backend/src/lib/validator/index.ts index 4340d0210..2400af8ee 100644 --- a/backend/src/lib/validator/index.ts +++ b/backend/src/lib/validator/index.ts @@ -1,2 +1,4 @@ export { isDisposableEmail } from "./validate-email"; +export { isValidFolderName, isValidSecretPath } from "./validate-folder-name"; export { blockLocalAndPrivateIpAddresses } from "./validate-url"; +export { isUuidV4 } from "./validate-uuid"; diff --git a/backend/src/lib/validator/validate-folder-name.ts b/backend/src/lib/validator/validate-folder-name.ts new file mode 100644 index 000000000..e357dbf9b --- /dev/null +++ b/backend/src/lib/validator/validate-folder-name.ts @@ -0,0 +1,14 @@ +import { CharacterType, characterValidator } from "./validate-string"; + +// regex to allow only alphanumeric, dash, underscore +export const isValidFolderName = characterValidator([ + CharacterType.AlphaNumeric, + CharacterType.Hyphen, + CharacterType.Underscore +]); + +export const isValidSecretPath = (path: string) => + path + .split("/") + .filter((el) => el.length) + .every((name) => isValidFolderName(name)); diff --git a/backend/src/lib/validator/validate-string.test.ts b/backend/src/lib/validator/validate-string.test.ts new file mode 100644 index 000000000..73e172896 --- /dev/null +++ b/backend/src/lib/validator/validate-string.test.ts @@ -0,0 +1,23 @@ +import { CharacterType, characterValidator } from "./validate-string"; + +describe("validate-string", () => { + test("Check alphabets", () => { + expect(characterValidator([CharacterType.Alphabets])("hello")).toBeTruthy(); + expect(characterValidator([CharacterType.Alphabets])("hello world")).toBeFalsy(); + expect(characterValidator([CharacterType.Alphabets, CharacterType.Spaces])("hello world")).toBeTruthy(); + }); + + test("Check numbers", () => { + expect(characterValidator([CharacterType.Numbers])("1234567890")).toBeTruthy(); + expect(characterValidator([CharacterType.AlphaNumeric])("helloWORLD1234567890")).toBeTruthy(); + expect(characterValidator([CharacterType.AlphaNumeric])("helloWORLD1234567890-")).toBeFalsy(); + }); + + test("Check special characters", () => { + expect(characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen])("Hello-World")).toBeTruthy(); + expect(characterValidator([CharacterType.AlphaNumeric, CharacterType.Plus])("Hello+World")).toBeTruthy(); + expect(characterValidator([CharacterType.AlphaNumeric, CharacterType.Underscore])("Hello_World")).toBeTruthy(); + expect(characterValidator([CharacterType.AlphaNumeric, CharacterType.Colon])("Hello:World")).toBeTruthy(); + expect(characterValidator([CharacterType.AlphaNumeric, CharacterType.Underscore])("Hello World")).toBeFalsy(); + }); +}); diff --git a/backend/src/lib/validator/validate-string.ts b/backend/src/lib/validator/validate-string.ts new file mode 100644 index 000000000..4e89d313f --- /dev/null +++ b/backend/src/lib/validator/validate-string.ts @@ -0,0 +1,113 @@ +import RE2 from "re2"; +import { z } from "zod"; + +export enum CharacterType { + Alphabets = "alphabets", + Numbers = "numbers", + AlphaNumeric = "alpha-numeric", + Spaces = "spaces", + SpecialCharacters = "specialCharacters", + Punctuation = "punctuation", + Period = "period", // . + Underscore = "underscore", // _ + Colon = "colon", // : + ForwardSlash = "forwardSlash", // / + Equals = "equals", // = + Plus = "plus", // + + Hyphen = "hyphen", // - + At = "at", // @ + // Additional individual characters that might be useful + Asterisk = "asterisk", // * + Ampersand = "ampersand", // & + Question = "question", // ? + Hash = "hash", // # + Percent = "percent", // % + Dollar = "dollar", // $ + Caret = "caret", // ^ + Backtick = "backtick", // ` + Pipe = "pipe", // | + Backslash = "backslash", // \ + OpenParen = "openParen", // ( + CloseParen = "closeParen", // ) + OpenBracket = "openBracket", // [ + CloseBracket = "closeBracket", // ] + OpenBrace = "openBrace", // { + CloseBrace = "closeBrace", // } + LessThan = "lessThan", // < + GreaterThan = "greaterThan", // > + SingleQuote = "singleQuote", // ' + DoubleQuote = "doubleQuote", // " + Comma = "comma", // , + Semicolon = "semicolon", // ; + Exclamation = "exclamation", // ! + Fullstop = "fullStop" // . +} + +/** + * Validates if a string contains only specific types of characters + */ +export const characterValidator = (allowedCharacters: CharacterType[]) => { + // Create a regex pattern based on allowed character types + const patternMap: Record = { + [CharacterType.Alphabets]: "a-zA-Z", + [CharacterType.Numbers]: "0-9", + [CharacterType.AlphaNumeric]: "a-zA-Z0-9", + [CharacterType.Spaces]: "\\s", + [CharacterType.SpecialCharacters]: "!@#$%^&*()_+\\-=\\[\\]{}|;:'\",.<>/?\\\\", + [CharacterType.Punctuation]: "\\.\\,\\;\\:\\!\\?", + [CharacterType.Colon]: "\\:", + [CharacterType.ForwardSlash]: "\\/", + [CharacterType.Underscore]: "_", + [CharacterType.Hyphen]: "\\-", + [CharacterType.Period]: "\\.", + [CharacterType.Equals]: "=", + [CharacterType.Plus]: "\\+", + [CharacterType.At]: "@", + [CharacterType.Asterisk]: "\\*", + [CharacterType.Ampersand]: "&", + [CharacterType.Question]: "\\?", + [CharacterType.Hash]: "#", + [CharacterType.Percent]: "%", + [CharacterType.Dollar]: "\\$", + [CharacterType.Caret]: "\\^", + [CharacterType.Backtick]: "`", + [CharacterType.Pipe]: "\\|", + [CharacterType.Backslash]: "\\\\", + [CharacterType.OpenParen]: "\\(", + [CharacterType.CloseParen]: "\\)", + [CharacterType.OpenBracket]: "\\[", + [CharacterType.CloseBracket]: "\\]", + [CharacterType.OpenBrace]: "\\{", + [CharacterType.CloseBrace]: "\\}", + [CharacterType.LessThan]: "<", + [CharacterType.GreaterThan]: ">", + [CharacterType.SingleQuote]: "'", + [CharacterType.DoubleQuote]: '\\"', + [CharacterType.Comma]: ",", + [CharacterType.Semicolon]: ";", + [CharacterType.Exclamation]: "!", + [CharacterType.Fullstop]: "." + }; + + // Combine patterns from allowed characters + const combinedPattern = allowedCharacters.map((char) => patternMap[char]).join(""); + + // Create a regex that matches only the allowed characters + const regex = new RE2(`^[${combinedPattern}]+$`); + + /** + * Validates if the input string contains only the allowed character types + * @param input String to validate + * @returns Boolean indicating if the string is valid + */ + return function validate(input: string): boolean { + return regex.test(input); + }; +}; + +export const zodValidateCharacters = (allowedCharacters: CharacterType[]) => { + const validator = characterValidator(allowedCharacters); + return (schema: z.ZodString, fieldName: string) => { + return schema.refine(validator, { message: `${fieldName} can only contain ${allowedCharacters.join(",")}` }); + }; +}; diff --git a/backend/src/lib/validator/validate-url.test.ts b/backend/src/lib/validator/validate-url.test.ts new file mode 100644 index 000000000..3ed5cb446 --- /dev/null +++ b/backend/src/lib/validator/validate-url.test.ts @@ -0,0 +1,15 @@ +import { isFQDN } from "./validate-url"; + +describe("isFQDN", () => { + test("Non wildcard", () => { + expect(isFQDN("www.example.com")).toBeTruthy(); + }); + + test("Wildcard", () => { + expect(isFQDN("*.example.com", { allow_wildcard: true })).toBeTruthy(); + }); + + test("Wildcard FQDN fails on option allow_wildcard false", () => { + expect(isFQDN("*.example.com")).toBeFalsy(); + }); +}); diff --git a/backend/src/lib/validator/validate-url.ts b/backend/src/lib/validator/validate-url.ts index fccebf47b..b555869d7 100644 --- a/backend/src/lib/validator/validate-url.ts +++ b/backend/src/lib/validator/validate-url.ts @@ -1,18 +1,130 @@ -import { getConfig } from "../config/env"; +import dns from "node:dns/promises"; + +import { isIPv4 } from "net"; +import RE2 from "re2"; + +import { getConfig } from "@app/lib/config/env"; + import { BadRequestError } from "../errors"; +import { isPrivateIp } from "../ip/ipRange"; -export const blockLocalAndPrivateIpAddresses = (url: string) => { - const validUrl = new URL(url); +export const blockLocalAndPrivateIpAddresses = async (url: string) => { const appCfg = getConfig(); - // on cloud local ips are not allowed - if ( - appCfg.isCloud && - (validUrl.host === "host.docker.internal" || - validUrl.host.match(/^10\.\d+\.\d+\.\d+/) || - validUrl.host.match(/^192\.168\.\d+\.\d+/)) - ) - throw new BadRequestError({ message: "Local IPs not allowed as URL" }); - if (validUrl.host === "localhost" || validUrl.host === "127.0.0.1") - throw new BadRequestError({ message: "Localhost not allowed" }); + if (appCfg.isDevelopmentMode) return; + + const validUrl = new URL(url); + const inputHostIps: string[] = []; + if (isIPv4(validUrl.host)) { + inputHostIps.push(validUrl.host); + } else { + if (validUrl.host === "localhost" || validUrl.host === "host.docker.internal") { + throw new BadRequestError({ message: "Local IPs not allowed as URL" }); + } + const resolvedIps = await dns.resolve4(validUrl.host); + inputHostIps.push(...resolvedIps); + } + const isInternalIp = inputHostIps.some((el) => isPrivateIp(el)); + if (isInternalIp && !appCfg.ALLOW_INTERNAL_IP_CONNECTIONS) + throw new BadRequestError({ message: "Local IPs not allowed as URL" }); +}; + +type FQDNOptions = { + require_tld?: boolean; + allow_underscores?: boolean; + allow_trailing_dot?: boolean; + allow_numeric_tld?: boolean; + allow_wildcard?: boolean; + ignore_max_length?: boolean; +}; + +const defaultFqdnOptions: FQDNOptions = { + require_tld: true, + allow_underscores: false, + allow_trailing_dot: false, + allow_numeric_tld: false, + allow_wildcard: false, + ignore_max_length: false +}; + +// credits: https://github.com/validatorjs/validator.js/blob/f5da7fb6ed59b94695e6fcb2e970c80029509919/src/lib/isFQDN.js#L13 +export const isFQDN = (str: string, options: FQDNOptions = {}): boolean => { + if (typeof str !== "string") { + throw new TypeError("Expected a string"); + } + + // Apply default options + const opts: FQDNOptions = { + ...defaultFqdnOptions, + ...options + }; + + let testStr = str; + /* Remove the optional trailing dot before checking validity */ + if (opts.allow_trailing_dot && str[str.length - 1] === ".") { + testStr = testStr.substring(0, str.length - 1); + } + + /* Remove the optional wildcard before checking validity */ + if (opts.allow_wildcard === true && str.indexOf("*.") === 0) { + testStr = testStr.substring(2); + } + + const parts = testStr.split("."); + const tld = parts[parts.length - 1]; + + if (opts.require_tld) { + // disallow fqdns without tld + if (parts.length < 2) { + return false; + } + + if ( + !opts.allow_numeric_tld && + !new RE2(/^([a-z\u00A1-\u00A8\u00AA-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}|xn[a-z0-9-]{2,})$/i).test(tld) + ) { + return false; + } + + // disallow spaces + if (new RE2(/\s/).test(tld)) { + return false; + } + } + + // reject numeric TLDs + if (!opts.allow_numeric_tld && new RE2(/^\d+$/).test(tld)) { + return false; + } + + const partRegex = new RE2(/^[a-z_\u00a1-\uffff0-9-]+$/i); + const fullWidthRegex = new RE2(/[\uff01-\uff5e]/); + const hyphenRegex = new RE2(/^-|-$/); + const underscoreRegex = new RE2(/_/); + + return parts.every((part) => { + if (part.length > 63 && !opts.ignore_max_length) { + return false; + } + + if (!partRegex.test(part)) { + return false; + } + + // disallow full-width chars + if (fullWidthRegex.test(part)) { + return false; + } + + // disallow parts starting or ending with hyphen + if (hyphenRegex.test(part)) { + return false; + } + + if (!opts.allow_underscores && underscoreRegex.test(part)) { + return false; + } + + return true; + }); }; diff --git a/backend/src/lib/validator/validate-uuid.ts b/backend/src/lib/validator/validate-uuid.ts new file mode 100644 index 000000000..a75e147f7 --- /dev/null +++ b/backend/src/lib/validator/validate-uuid.ts @@ -0,0 +1,3 @@ +import { z } from "zod"; + +export const isUuidV4 = (uuid: string) => z.string().uuid().safeParse(uuid).success; diff --git a/backend/src/lib/zod/index.ts b/backend/src/lib/zod/index.ts index 4d3fea8c7..21c59b6ba 100644 --- a/backend/src/lib/zod/index.ts +++ b/backend/src/lib/zod/index.ts @@ -1,3 +1,4 @@ +import RE2 from "re2"; import { z, ZodTypeAny } from "zod"; // this is a patched zod string to remove empty string to undefined @@ -11,3 +12,8 @@ export const zpStr = (schema: T, opt: { stripNull: boolean export const zodBuffer = z.custom((data) => Buffer.isBuffer(data) || data instanceof Uint8Array, { message: "Expected binary data (Buffer Or Uint8Array)" }); + +export const re2Validator = (pattern: string | RegExp) => { + const re2Pattern = new RE2(pattern); + return (value: string) => re2Pattern.test(value); +}; diff --git a/backend/src/main.ts b/backend/src/main.ts index f71a1fe95..c3b5a0900 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,10 +1,15 @@ -import dotenv from "dotenv"; -import path from "path"; +import "./lib/telemetry/instrumentation"; +import dotenv from "dotenv"; +import { Redis } from "ioredis"; + +import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns"; + +import { runMigrations } from "./auto-start-migrations"; import { initAuditLogDbConnection, initDbConnection } from "./db"; import { keyStoreFactory } from "./keystore/keystore"; -import { formatSmtpConfig, initEnvConfig, IS_PACKAGED } from "./lib/config/env"; -import { isMigrationMode } from "./lib/fn"; +import { formatSmtpConfig, initEnvConfig } from "./lib/config/env"; +import { removeTemporaryBaseDirectory } from "./lib/files"; import { initLogger } from "./lib/logger"; import { queueServiceFactory } from "./queue"; import { main } from "./server/app"; @@ -14,65 +19,90 @@ import { smtpServiceFactory } from "./services/smtp/smtp-service"; dotenv.config(); const run = async () => { - const logger = await initLogger(); - const appCfg = initEnvConfig(logger); + const logger = initLogger(); + const envConfig = initEnvConfig(logger); + + await removeTemporaryBaseDirectory(); + const db = initDbConnection({ - dbConnectionUri: appCfg.DB_CONNECTION_URI, - dbRootCert: appCfg.DB_ROOT_CERT, - readReplicas: appCfg.DB_READ_REPLICAS?.map((el) => ({ + dbConnectionUri: envConfig.DB_CONNECTION_URI, + dbRootCert: envConfig.DB_ROOT_CERT, + readReplicas: envConfig.DB_READ_REPLICAS?.map((el) => ({ dbRootCert: el.DB_ROOT_CERT, dbConnectionUri: el.DB_CONNECTION_URI })) }); - const auditLogDb = appCfg.AUDIT_LOGS_DB_CONNECTION_URI + const auditLogDb = envConfig.AUDIT_LOGS_DB_CONNECTION_URI ? initAuditLogDbConnection({ - dbConnectionUri: appCfg.AUDIT_LOGS_DB_CONNECTION_URI, - dbRootCert: appCfg.AUDIT_LOGS_DB_ROOT_CERT + dbConnectionUri: envConfig.AUDIT_LOGS_DB_CONNECTION_URI, + dbRootCert: envConfig.AUDIT_LOGS_DB_ROOT_CERT }) : undefined; - // Case: App is running in packaged mode (binary), and migration mode is enabled. - // Run the migrations and exit the process after completion. - if (IS_PACKAGED && isMigrationMode()) { - try { - logger.info("Running Postgres migrations.."); - await db.migrate.latest({ - directory: path.join(__dirname, "./db/migrations") - }); - logger.info("Postgres migrations completed"); - } catch (err) { - logger.error(err, "Failed to run migrations"); - process.exit(1); - } - - process.exit(0); - } + await runMigrations({ applicationDb: db, auditLogDb, logger }); const smtp = smtpServiceFactory(formatSmtpConfig()); - const queue = queueServiceFactory(appCfg.REDIS_URL); - const keyStore = keyStoreFactory(appCfg.REDIS_URL); - const server = await main({ db, auditLogDb, smtp, logger, queue, keyStore }); + const queue = queueServiceFactory(envConfig.REDIS_URL, { + dbConnectionUrl: envConfig.DB_CONNECTION_URI, + dbRootCert: envConfig.DB_ROOT_CERT + }); + + await queue.initialize(); + + const keyStore = keyStoreFactory(envConfig.REDIS_URL); + const redis = new Redis(envConfig.REDIS_URL); + + const hsmModule = initializeHsmModule(envConfig); + hsmModule.initialize(); + + const server = await main({ + db, + auditLogDb, + hsmModule: hsmModule.getModule(), + smtp, + logger, + queue, + keyStore, + redis, + envConfig + }); const bootstrap = await bootstrapCheck({ db }); // eslint-disable-next-line process.on("SIGINT", async () => { await server.close(); + await queue.shutdown(); await db.destroy(); + await removeTemporaryBaseDirectory(); + hsmModule.finalize(); process.exit(0); }); // eslint-disable-next-line process.on("SIGTERM", async () => { await server.close(); + await queue.shutdown(); await db.destroy(); + await removeTemporaryBaseDirectory(); + hsmModule.finalize(); process.exit(0); }); + if (!envConfig.isDevelopmentMode) { + process.on("uncaughtException", (error) => { + logger.error(error, "CRITICAL ERROR: Uncaught Exception"); + }); + + process.on("unhandledRejection", (error) => { + logger.error(error, "CRITICAL ERROR: Unhandled Promise Rejection"); + }); + } + await server.listen({ - port: appCfg.PORT, - host: appCfg.HOST, + port: envConfig.PORT, + host: envConfig.HOST, listenTextResolver: (address) => { void bootstrap(); return address; diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index 457eebcc1..ae1a3e821 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -1,17 +1,31 @@ import { Job, JobsOptions, Queue, QueueOptions, RepeatOptions, Worker, WorkerListener } from "bullmq"; import Redis from "ioredis"; +import PgBoss, { WorkOptions } from "pg-boss"; import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas"; import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; +import { + TSecretRotationRotateSecretsJobPayload, + TSecretRotationSendNotificationJobPayload +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; import { TScanFullRepoEventPayload, TScanPushEventPayload } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types"; +import { getConfig } from "@app/lib/config/env"; +import { logger } from "@app/lib/logger"; import { TFailedIntegrationSyncEmailsPayload, TIntegrationSyncPayload, TSyncSecretsDTO } from "@app/services/secret/secret-types"; +import { + TQueueSecretSyncImportSecretsByIdDTO, + TQueueSecretSyncRemoveSecretsByIdDTO, + TQueueSecretSyncSyncSecretsByIdDTO, + TQueueSendSecretSyncActionFailedNotificationsDTO +} from "@app/services/secret-sync/secret-sync-types"; +import { TWebhookPayloads } from "@app/services/webhook/webhook-types"; export enum QueueName { SecretRotation = "secret-rotation", @@ -33,7 +47,9 @@ export enum QueueName { SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication ProjectV3Migration = "project-v3-migration", AccessTokenStatusUpdate = "access-token-status-update", - ImportSecretsFromExternalSource = "import-secrets-from-external-source" + ImportSecretsFromExternalSource = "import-secrets-from-external-source", + AppConnectionSecretSync = "app-connection-secret-sync", + SecretRotationV2 = "secret-rotation-v2" } export enum QueueJobs { @@ -58,7 +74,14 @@ export enum QueueJobs { ProjectV3Migration = "project-v3-migration", IdentityAccessTokenStatusUpdate = "identity-access-token-status-update", ServiceTokenStatusUpdate = "service-token-status-update", - ImportSecretsFromExternalSource = "import-secrets-from-external-source" + ImportSecretsFromExternalSource = "import-secrets-from-external-source", + SecretSyncSyncSecrets = "secret-sync-sync-secrets", + SecretSyncImportSecrets = "secret-sync-import-secrets", + SecretSyncRemoveSecrets = "secret-sync-remove-secrets", + SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications", + SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations", + SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets", + SecretRotationV2SendNotification = "secret-rotation-v2-send-notification" } export type TQueueJobTypes = { @@ -93,7 +116,7 @@ export type TQueueJobTypes = { }; [QueueName.SecretWebhook]: { name: QueueJobs.SecWebhook; - payload: { projectId: string; environment: string; secretPath: string; depth?: number }; + payload: TWebhookPayloads; }; [QueueName.AccessTokenStatusUpdate]: @@ -181,20 +204,79 @@ export type TQueueJobTypes = { }; }; }; + [QueueName.AppConnectionSecretSync]: + | { + name: QueueJobs.SecretSyncSyncSecrets; + payload: TQueueSecretSyncSyncSecretsByIdDTO; + } + | { + name: QueueJobs.SecretSyncImportSecrets; + payload: TQueueSecretSyncImportSecretsByIdDTO; + } + | { + name: QueueJobs.SecretSyncRemoveSecrets; + payload: TQueueSecretSyncRemoveSecretsByIdDTO; + } + | { + name: QueueJobs.SecretSyncSendActionFailedNotifications; + payload: TQueueSendSecretSyncActionFailedNotificationsDTO; + }; + [QueueName.SecretRotationV2]: + | { + name: QueueJobs.SecretRotationV2QueueRotations; + payload: undefined; + } + | { + name: QueueJobs.SecretRotationV2RotateSecrets; + payload: TSecretRotationRotateSecretsJobPayload; + } + | { + name: QueueJobs.SecretRotationV2SendNotification; + payload: TSecretRotationSendNotificationJobPayload; + }; }; export type TQueueServiceFactory = ReturnType; -export const queueServiceFactory = (redisUrl: string) => { +export const queueServiceFactory = ( + redisUrl: string, + { dbConnectionUrl, dbRootCert }: { dbConnectionUrl: string; dbRootCert?: string } +) => { const connection = new Redis(redisUrl, { maxRetriesPerRequest: null }); const queueContainer = {} as Record< QueueName, Queue >; + + const pgBoss = new PgBoss({ + connectionString: dbConnectionUrl, + archiveCompletedAfterSeconds: 60, + cronMonitorIntervalSeconds: 5, + archiveFailedAfterSeconds: 1000, // we want to keep failed jobs for a longer time so that it can be retried + deleteAfterSeconds: 30, + ssl: dbRootCert + ? { + rejectUnauthorized: true, + ca: Buffer.from(dbRootCert, "base64").toString("ascii") + } + : false + }); + + const queueContainerPg = {} as Record; + const workerContainer = {} as Record< QueueName, Worker >; + const initialize = async () => { + logger.info("Initializing pg-queue..."); + await pgBoss.start(); + + pgBoss.on("error", (error) => { + logger.error(error, "pg-queue error"); + }); + }; + const start = ( name: T, jobFn: (job: Job, token?: string) => Promise, @@ -209,10 +291,34 @@ export const queueServiceFactory = (redisUrl: string) => { connection }); - workerContainer[name] = new Worker(name, jobFn, { - ...queueSettings, - connection - }); + const appCfg = getConfig(); + if (appCfg.QUEUE_WORKERS_ENABLED) { + workerContainer[name] = new Worker(name, jobFn, { + ...queueSettings, + connection + }); + } + }; + + const startPg = async ( + jobName: QueueJobs, + jobsFn: (jobs: PgBoss.JobWithMetadata[]) => Promise, + options: WorkOptions & { + workerCount: number; + } + ) => { + if (queueContainerPg[jobName]) { + throw new Error(`${jobName} queue is already initialized`); + } + + await pgBoss.createQueue(jobName); + queueContainerPg[jobName] = true; + + await Promise.all( + Array.from({ length: options.workerCount }).map(() => + pgBoss.work(jobName, { ...options, includeMetadata: true }, jobsFn) + ) + ); }; const listen = < @@ -223,6 +329,11 @@ export const queueServiceFactory = (redisUrl: string) => { event: U, listener: WorkerListener[U] ) => { + const appCfg = getConfig(); + if (!appCfg.QUEUE_WORKERS_ENABLED) { + return; + } + const worker = workerContainer[name]; worker.on(event, listener); }; @@ -238,6 +349,27 @@ export const queueServiceFactory = (redisUrl: string) => { await q.add(job, data, opts); }; + const queuePg = async ( + job: TQueueJobTypes[T]["name"], + data: TQueueJobTypes[T]["payload"], + opts?: PgBoss.SendOptions & { jobId?: string } + ) => { + await pgBoss.send({ + name: job, + data, + options: opts + }); + }; + + const schedulePg = async ( + job: TQueueJobTypes[T]["name"], + cron: string, + data: TQueueJobTypes[T]["payload"], + opts?: PgBoss.ScheduleOptions & { jobId?: string } + ) => { + await pgBoss.schedule(job, cron, data, opts); + }; + const stopRepeatableJob = async ( name: T, job: TQueueJobTypes[T]["name"], @@ -250,6 +382,13 @@ export const queueServiceFactory = (redisUrl: string) => { } }; + const getRepeatableJobs = (name: QueueName, startOffset?: number, endOffset?: number) => { + const q = queueContainer[name]; + if (!q) throw new Error(`Queue '${name}' not initialized`); + + return q.getRepeatableJobs(startOffset, endOffset); + }; + const stopRepeatableJobByJobId = async (name: T, jobId: string) => { const q = queueContainer[name]; const job = await q.getJob(jobId); @@ -259,6 +398,11 @@ export const queueServiceFactory = (redisUrl: string) => { return q.removeRepeatableByKey(job.repeatJobKey); }; + const stopRepeatableJobByKey = async (name: T, repeatJobKey: string) => { + const q = queueContainer[name]; + return q.removeRepeatableByKey(repeatJobKey); + }; + const stopJobById = async (name: T, jobId: string) => { const q = queueContainer[name]; const job = await q.getJob(jobId); @@ -274,5 +418,20 @@ export const queueServiceFactory = (redisUrl: string) => { await Promise.all(Object.values(workerContainer).map((worker) => worker.close())); }; - return { start, listen, queue, shutdown, stopRepeatableJob, stopRepeatableJobByJobId, clearQueue, stopJobById }; + return { + initialize, + start, + listen, + queue, + shutdown, + stopRepeatableJob, + stopRepeatableJobByJobId, + stopRepeatableJobByKey, + clearQueue, + stopJobById, + getRepeatableJobs, + startPg, + queuePg, + schedulePg + }; }; diff --git a/backend/src/server/app.ts b/backend/src/server/app.ts index b768d0db5..3f5c477ef 100644 --- a/backend/src/server/app.ts +++ b/backend/src/server/app.ts @@ -10,22 +10,27 @@ import fastifyFormBody from "@fastify/formbody"; import helmet from "@fastify/helmet"; import type { FastifyRateLimitOptions } from "@fastify/rate-limit"; import ratelimiter from "@fastify/rate-limit"; +import { fastifyRequestContext } from "@fastify/request-context"; import fastify from "fastify"; +import { Redis } from "ioredis"; import { Knex } from "knex"; -import { Logger } from "pino"; +import { HsmModule } from "@app/ee/services/hsm/hsm-types"; import { TKeyStoreFactory } from "@app/keystore/keystore"; -import { getConfig, IS_PACKAGED } from "@app/lib/config/env"; +import { getConfig, IS_PACKAGED, TEnvConfig } from "@app/lib/config/env"; +import { CustomLogger } from "@app/lib/logger/logger"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TQueueServiceFactory } from "@app/queue"; import { TSmtpService } from "@app/services/smtp/smtp-service"; import { globalRateLimiterCfg } from "./config/rateLimiter"; import { addErrorsToResponseSchemas } from "./plugins/add-errors-to-response-schemas"; +import { apiMetrics } from "./plugins/api-metrics"; import { fastifyErrHandler } from "./plugins/error-handler"; -import { registerExternalNextjs } from "./plugins/external-nextjs"; import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "./plugins/fastify-zod"; import { fastifyIp } from "./plugins/ip"; import { maintenanceMode } from "./plugins/maintenanceMode"; +import { registerServeUI } from "./plugins/serve-ui"; import { fastifySwagger } from "./plugins/swagger"; import { registerRoutes } from "./routes"; @@ -33,24 +38,32 @@ type TMain = { auditLogDb?: Knex; db: Knex; smtp: TSmtpService; - logger?: Logger; + logger?: CustomLogger; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory; + hsmModule: HsmModule; + redis: Redis; + envConfig: TEnvConfig; }; // Run the server! -export const main = async ({ db, auditLogDb, smtp, logger, queue, keyStore }: TMain) => { +export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, keyStore, redis, envConfig }: TMain) => { const appCfg = getConfig(); + const server = fastify({ logger: appCfg.NODE_ENV === "test" ? false : logger, + genReqId: () => `req-${alphaNumericNanoId(14)}`, trustProxy: true, - connectionTimeout: 30 * 1000, - ignoreTrailingSlash: true + + connectionTimeout: appCfg.isHsmConfigured ? 90_000 : 30_000, + ignoreTrailingSlash: true, + pluginTimeout: 40_000 }).withTypeProvider(); server.setValidatorCompiler(validatorCompiler); server.setSerializerCompiler(serializerCompiler); + server.decorate("redis", redis); server.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_, body, done) => { try { const strBody = body instanceof Buffer ? body.toString() : body; @@ -75,19 +88,32 @@ export const main = async ({ db, auditLogDb, smtp, logger, queue, keyStore }: TM await server.register(cors, { credentials: true, - origin: appCfg.SITE_URL || true + ...(appCfg.CORS_ALLOWED_ORIGINS?.length + ? { + origin: [...appCfg.CORS_ALLOWED_ORIGINS, ...(appCfg.SITE_URL ? [appCfg.SITE_URL] : [])] + } + : { + origin: appCfg.SITE_URL || true + }), + ...(appCfg.CORS_ALLOWED_HEADERS?.length && { + allowedHeaders: appCfg.CORS_ALLOWED_HEADERS + }) }); await server.register(addErrorsToResponseSchemas); // pull ip based on various proxy headers await server.register(fastifyIp); + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + await server.register(apiMetrics); + } + await server.register(fastifySwagger); await server.register(fastifyFormBody); await server.register(fastifyErrHandler); // Rate limiters and security headers - if (appCfg.isProductionMode) { + if (appCfg.isProductionMode && appCfg.isCloud) { await server.register(ratelimiter, globalRateLimiterCfg()); } @@ -95,15 +121,19 @@ export const main = async ({ db, auditLogDb, smtp, logger, queue, keyStore }: TM await server.register(maintenanceMode); - await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore }); + await server.register(fastifyRequestContext, { + defaultStoreValues: (req) => ({ + reqId: req.id, + log: req.log.child({ reqId: req.id }) + }) + }); - if (appCfg.isProductionMode) { - await server.register(registerExternalNextjs, { - standaloneMode: appCfg.STANDALONE_MODE || IS_PACKAGED, - dir: path.join(__dirname, IS_PACKAGED ? "../../../" : "../../"), - port: appCfg.PORT - }); - } + await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore, hsmModule, envConfig }); + + await server.register(registerServeUI, { + standaloneMode: appCfg.STANDALONE_MODE || IS_PACKAGED, + dir: path.join(__dirname, IS_PACKAGED ? "../../../" : "../../") + }); await server.ready(); server.swagger(); diff --git a/backend/src/server/boot-strap-check.ts b/backend/src/server/boot-strap-check.ts index ceaef59e7..7db2a71e8 100644 --- a/backend/src/server/boot-strap-check.ts +++ b/backend/src/server/boot-strap-check.ts @@ -46,10 +46,10 @@ export const bootstrapCheck = async ({ db }: BootstrapOpt) => { await createTransport(smtpCfg) .verify() .then(async () => { - console.info("SMTP successfully connected"); + console.info(`SMTP - Verified connection to ${appCfg.SMTP_HOST}:${appCfg.SMTP_PORT}`); }) - .catch((err) => { - console.error(`SMTP - Failed to connect to ${appCfg.SMTP_HOST}:${appCfg.SMTP_PORT}`); + .catch((err: Error) => { + console.error(`SMTP - Failed to connect to ${appCfg.SMTP_HOST}:${appCfg.SMTP_PORT} - ${err.message}`); logger.error(err); }); diff --git a/backend/src/server/config/rateLimiter.ts b/backend/src/server/config/rateLimiter.ts index 176d44183..681442d1b 100644 --- a/backend/src/server/config/rateLimiter.ts +++ b/backend/src/server/config/rateLimiter.ts @@ -93,3 +93,10 @@ export const userEngagementLimit: RateLimitOptions = { max: 5, keyGenerator: (req) => req.realIp }; + +export const publicSshCaLimit: RateLimitOptions = { + timeWindow: 60 * 1000, + hook: "preValidation", + max: 30, // conservative default + keyGenerator: (req) => req.realIp +}; diff --git a/backend/src/server/lib/schemas.ts b/backend/src/server/lib/schemas.ts new file mode 100644 index 000000000..9f93eaea0 --- /dev/null +++ b/backend/src/server/lib/schemas.ts @@ -0,0 +1,50 @@ +import slugify from "@sindresorhus/slugify"; +import { z } from "zod"; + +import { CharacterType, characterValidator } from "@app/lib/validator/validate-string"; + +interface SlugSchemaInputs { + min?: number; + max?: number; + field?: string; +} + +export const slugSchema = ({ min = 1, max = 32, field = "Slug" }: SlugSchemaInputs = {}) => { + return z + .string() + .trim() + .min(min, { + message: `${field} field must be at least ${min} lowercase character${min === 1 ? "" : "s"}` + }) + .max(max, { + message: `${field} field must be at most ${max} lowercase character${max === 1 ? "" : "s"}` + }) + .refine((v) => slugify(v, { lowercase: true }) === v, { + message: `${field} field can only contain lowercase letters, numbers, and hyphens` + }); +}; + +export const GenericResourceNameSchema = z + .string() + .trim() + .min(1, { message: "Name must be at least 1 character" }) + .max(64, { message: "Name must be 64 or fewer characters" }) + .refine( + (val) => + characterValidator([ + CharacterType.AlphaNumeric, + CharacterType.Hyphen, + CharacterType.Underscore, + CharacterType.Spaces + ])(val), + "Name can only contain alphanumeric characters, dashes, underscores, and spaces" + ); + +export const BaseSecretNameSchema = z.string().trim().min(1); + +export const SecretNameSchema = BaseSecretNameSchema.refine( + (el) => !el.includes(" "), + "Secret name cannot contain spaces." +) + .refine((el) => !el.includes(":"), "Secret name cannot contain colon.") + .refine((el) => !el.includes("/"), "Secret name cannot contain forward slash."); diff --git a/backend/src/server/plugins/api-metrics.ts b/backend/src/server/plugins/api-metrics.ts new file mode 100644 index 000000000..2e3a20a23 --- /dev/null +++ b/backend/src/server/plugins/api-metrics.ts @@ -0,0 +1,21 @@ +import opentelemetry from "@opentelemetry/api"; +import fp from "fastify-plugin"; + +export const apiMetrics = fp(async (fastify) => { + const apiMeter = opentelemetry.metrics.getMeter("API"); + const latencyHistogram = apiMeter.createHistogram("API_latency", { + unit: "ms" + }); + + fastify.addHook("onResponse", async (request, reply) => { + const { method } = request; + const route = request.routerPath; + const { statusCode } = reply; + + latencyHistogram.record(reply.elapsedTime, { + route, + method, + statusCode + }); + }); +}); diff --git a/backend/src/server/plugins/audit-log.ts b/backend/src/server/plugins/audit-log.ts index 3f49778e8..3b02b1528 100644 --- a/backend/src/server/plugins/audit-log.ts +++ b/backend/src/server/plugins/audit-log.ts @@ -32,13 +32,21 @@ export const getUserAgentType = (userAgent: string | undefined) => { export const injectAuditLogInfo = fp(async (server: FastifyZodProvider) => { server.decorateRequest("auditLogInfo", null); server.addHook("onRequest", async (req) => { - if (!req.auth) return; const userAgent = req.headers["user-agent"] ?? ""; const payload = { ipAddress: req.realIp, userAgent, userAgentType: getUserAgentType(userAgent) } as typeof req.auditLogInfo; + + if (!req.auth) { + payload.actor = { + type: ActorType.UNKNOWN_USER, + metadata: {} + }; + req.auditLogInfo = payload; + return; + } if (req.auth.actor === ActorType.USER) { payload.actor = { type: ActorType.USER, diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 9d239a405..ad5291a13 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -1,3 +1,4 @@ +import { requestContext } from "@fastify/request-context"; import { FastifyRequest } from "fastify"; import fp from "fastify-plugin"; import jwt, { JwtPayload } from "jsonwebtoken"; @@ -8,6 +9,7 @@ import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; import { ActorType, AuthMethod, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type"; import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types"; +import { getServerCfg } from "@app/services/super-admin/super-admin-service"; export type TAuthMode = | { @@ -43,6 +45,7 @@ export type TAuthMode = identityName: string; orgId: string; authMethod: null; + isInstanceAdmin?: boolean; } | { authMode: AuthMode.SCIM_TOKEN; @@ -129,14 +132,22 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => { } case AuthMode.IDENTITY_ACCESS_TOKEN: { const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken(token, req.realIp); + const serverCfg = await getServerCfg(); req.auth = { authMode: AuthMode.IDENTITY_ACCESS_TOKEN, actor, orgId: identity.orgId, identityId: identity.identityId, identityName: identity.name, - authMethod: null + authMethod: null, + isInstanceAdmin: serverCfg?.adminIdentityIds?.includes(identity.identityId) }; + if (token?.identityAuth?.oidc) { + requestContext.set("identityAuthInfo", { + identityId: identity.identityId, + oidc: token?.identityAuth?.oidc + }); + } break; } case AuthMode.SERVICE_TOKEN: { diff --git a/backend/src/server/plugins/auth/superAdmin.ts b/backend/src/server/plugins/auth/superAdmin.ts index f5868f130..4ca9ba373 100644 --- a/backend/src/server/plugins/auth/superAdmin.ts +++ b/backend/src/server/plugins/auth/superAdmin.ts @@ -1,16 +1,18 @@ import { FastifyReply, FastifyRequest, HookHandlerDoneFunction } from "fastify"; import { ForbiddenRequestError } from "@app/lib/errors"; -import { ActorType } from "@app/services/auth/auth-type"; +import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; export const verifySuperAdmin = ( req: T, _res: FastifyReply, done: HookHandlerDoneFunction ) => { - if (req.auth.actor !== ActorType.USER || !req.auth.user.superAdmin) - throw new ForbiddenRequestError({ - message: "Requires elevated super admin privileges" - }); - done(); + if (isSuperAdmin(req.auth)) { + return done(); + } + + throw new ForbiddenRequestError({ + message: "Requires elevated super admin privileges" + }); }; diff --git a/backend/src/server/plugins/error-handler.ts b/backend/src/server/plugins/error-handler.ts index 007902a17..c8170a023 100644 --- a/backend/src/server/plugins/error-handler.ts +++ b/backend/src/server/plugins/error-handler.ts @@ -1,8 +1,10 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, PureAbility } from "@casl/ability"; +import opentelemetry from "@opentelemetry/api"; import fastifyPlugin from "fastify-plugin"; import jwt from "jsonwebtoken"; import { ZodError } from "zod"; +import { getConfig } from "@app/lib/config/env"; import { BadRequestError, DatabaseError, @@ -10,6 +12,8 @@ import { GatewayTimeoutError, InternalServerError, NotFoundError, + OidcAuthError, + PermissionBoundaryError, RateLimitError, ScimRequestError, UnauthorizedError @@ -26,6 +30,7 @@ enum HttpStatusCodes { NotFound = 404, Unauthorized = 401, Forbidden = 403, + UnprocessableContent = 422, // eslint-disable-next-line @typescript-eslint/no-shadow InternalServerError = 500, GatewayTimeout = 504, @@ -33,79 +38,137 @@ enum HttpStatusCodes { } export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => { + const appCfg = getConfig(); + + const apiMeter = opentelemetry.metrics.getMeter("API"); + const errorHistogram = apiMeter.createHistogram("API_errors", { + description: "API errors by type, status code, and name", + unit: "1" + }); + server.setErrorHandler((error, req, res) => { req.log.error(error); + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + const { method } = req; + const route = req.routerPath; + const errorType = + error instanceof jwt.JsonWebTokenError ? "TokenError" : error.constructor.name || "UnknownError"; + + errorHistogram.record(1, { + route, + method, + type: errorType, + name: error.name + }); + } + if (error instanceof BadRequestError) { void res .status(HttpStatusCodes.BadRequest) - .send({ statusCode: HttpStatusCodes.BadRequest, message: error.message, error: error.name }); + .send({ reqId: req.id, statusCode: HttpStatusCodes.BadRequest, message: error.message, error: error.name }); } else if (error instanceof NotFoundError) { void res .status(HttpStatusCodes.NotFound) - .send({ statusCode: HttpStatusCodes.NotFound, message: error.message, error: error.name }); + .send({ reqId: req.id, statusCode: HttpStatusCodes.NotFound, message: error.message, error: error.name }); } else if (error instanceof UnauthorizedError) { - void res - .status(HttpStatusCodes.Unauthorized) - .send({ statusCode: HttpStatusCodes.Unauthorized, message: error.message, error: error.name }); - } else if (error instanceof DatabaseError || error instanceof InternalServerError) { - void res - .status(HttpStatusCodes.InternalServerError) - .send({ statusCode: HttpStatusCodes.InternalServerError, message: "Something went wrong", error: error.name }); - } else if (error instanceof GatewayTimeoutError) { - void res - .status(HttpStatusCodes.GatewayTimeout) - .send({ statusCode: HttpStatusCodes.GatewayTimeout, message: error.message, error: error.name }); - } else if (error instanceof ZodError) { - void res - .status(HttpStatusCodes.Unauthorized) - .send({ statusCode: HttpStatusCodes.Unauthorized, error: "ValidationFailure", message: error.issues }); - } else if (error instanceof ForbiddenError) { - void res.status(HttpStatusCodes.Forbidden).send({ - statusCode: HttpStatusCodes.Forbidden, - error: "PermissionDenied", - message: `You are not allowed to ${error.action} on ${error.subjectType} - ${JSON.stringify(error.subject)}` - }); - } else if (error instanceof ForbiddenRequestError) { - void res.status(HttpStatusCodes.Forbidden).send({ - statusCode: HttpStatusCodes.Forbidden, + void res.status(HttpStatusCodes.Unauthorized).send({ + reqId: req.id, + statusCode: HttpStatusCodes.Unauthorized, message: error.message, error: error.name }); + } else if (error instanceof DatabaseError) { + void res.status(HttpStatusCodes.InternalServerError).send({ + reqId: req.id, + statusCode: HttpStatusCodes.InternalServerError, + message: "Something went wrong", + error: error.name + }); + } else if (error instanceof InternalServerError) { + void res.status(HttpStatusCodes.InternalServerError).send({ + reqId: req.id, + statusCode: HttpStatusCodes.InternalServerError, + message: error.message ?? "Something went wrong", + error: error.name + }); + } else if (error instanceof GatewayTimeoutError) { + void res.status(HttpStatusCodes.GatewayTimeout).send({ + reqId: req.id, + statusCode: HttpStatusCodes.GatewayTimeout, + message: error.message, + error: error.name + }); + } else if (error instanceof ZodError) { + void res.status(HttpStatusCodes.UnprocessableContent).send({ + reqId: req.id, + statusCode: HttpStatusCodes.UnprocessableContent, + error: "ValidationFailure", + message: error.issues + }); + } else if (error instanceof ForbiddenError) { + void res.status(HttpStatusCodes.Forbidden).send({ + reqId: req.id, + statusCode: HttpStatusCodes.Forbidden, + error: "PermissionDenied", + message: `You are not allowed to ${error.action} on ${error.subjectType}`, + details: (error.ability as PureAbility).rulesFor(error.action as string, error.subjectType).map((el) => ({ + action: el.action, + inverted: el.inverted, + subject: el.subject, + conditions: el.conditions + })) + }); + } else if (error instanceof ForbiddenRequestError || error instanceof PermissionBoundaryError) { + void res.status(HttpStatusCodes.Forbidden).send({ + reqId: req.id, + statusCode: HttpStatusCodes.Forbidden, + message: error.message, + error: error.name, + details: error?.details + }); } else if (error instanceof RateLimitError) { void res.status(HttpStatusCodes.TooManyRequests).send({ + reqId: req.id, statusCode: HttpStatusCodes.TooManyRequests, message: error.message, error: error.name }); } else if (error instanceof ScimRequestError) { void res.status(error.status).send({ + reqId: req.id, schemas: error.schemas, status: error.status, detail: error.detail }); - // Handle JWT errors and make them more human-readable for the end-user. + } else if (error instanceof OidcAuthError) { + void res.status(HttpStatusCodes.InternalServerError).send({ + reqId: req.id, + statusCode: HttpStatusCodes.InternalServerError, + message: error.message, + error: error.name + }); } else if (error instanceof jwt.JsonWebTokenError) { - const message = (() => { - if (error.message === JWTErrors.JwtExpired) { - return "Your token has expired. Please re-authenticate."; - } - if (error.message === JWTErrors.JwtMalformed) { - return "The provided access token is malformed. Please use a valid token or generate a new one and try again."; - } - if (error.message === JWTErrors.InvalidAlgorithm) { - return "The access token is signed with an invalid algorithm. Please provide a valid token and try again."; - } + let errorMessage = error.message; - return error.message; - })(); + if (error.message === JWTErrors.JwtExpired) { + errorMessage = "Your token has expired. Please re-authenticate."; + } else if (error.message === JWTErrors.JwtMalformed) { + errorMessage = + "The provided access token is malformed. Please use a valid token or generate a new one and try again."; + } else if (error.message === JWTErrors.InvalidAlgorithm) { + errorMessage = + "The access token is signed with an invalid algorithm. Please provide a valid token and try again."; + } void res.status(HttpStatusCodes.Forbidden).send({ + reqId: req.id, statusCode: HttpStatusCodes.Forbidden, error: "TokenError", - message + message: errorMessage }); } else { void res.status(HttpStatusCodes.InternalServerError).send({ + reqId: req.id, statusCode: HttpStatusCodes.InternalServerError, error: "InternalServerError", message: "Something went wrong" diff --git a/backend/src/server/plugins/external-nextjs.ts b/backend/src/server/plugins/external-nextjs.ts deleted file mode 100644 index 754817035..000000000 --- a/backend/src/server/plugins/external-nextjs.ts +++ /dev/null @@ -1,76 +0,0 @@ -// this plugins allows to run infisical in standalone mode -// standalone mode = infisical backend and nextjs frontend in one server -// this way users don't need to deploy two things -import path from "node:path"; - -import { IS_PACKAGED } from "@app/lib/config/env"; - -// to enabled this u need to set standalone mode to true -export const registerExternalNextjs = async ( - server: FastifyZodProvider, - { - standaloneMode, - dir, - port - }: { - standaloneMode?: boolean; - dir: string; - port: number; - } -) => { - if (standaloneMode) { - const frontendName = IS_PACKAGED ? "frontend" : "frontend-build"; - const nextJsBuildPath = path.join(dir, frontendName); - - const { default: conf } = (await import( - path.join(dir, `${frontendName}/.next/required-server-files.json`), - // @ts-expect-error type - { - assert: { type: "json" } - } - )) as { default: { config: string } }; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let NextServer: any; - - if (!IS_PACKAGED) { - /* eslint-disable */ - const { default: nextServer } = ( - await import(path.join(dir, `${frontendName}/node_modules/next/dist/server/next-server.js`)) - ).default; - - NextServer = nextServer; - } else { - /* eslint-disable */ - const nextServer = await import(path.join(dir, `${frontendName}/node_modules/next/dist/server/next-server.js`)); - - NextServer = nextServer.default; - } - - const nextApp = new NextServer({ - dev: false, - dir: nextJsBuildPath, - port, - conf: conf.config, - hostname: "local", - customServer: false - }); - - server.route({ - method: ["GET", "PUT", "PATCH", "POST", "DELETE"], - url: "/*", - schema: { - hide: true - }, - handler: (req, res) => - nextApp - .getRequestHandler()(req.raw, res.raw) - .then(() => { - res.hijack(); - }) - }); - server.addHook("onClose", () => nextApp.close()); - await nextApp.prepare(); - /* eslint-enable */ - } -}; diff --git a/backend/src/server/plugins/fastify-zod.ts b/backend/src/server/plugins/fastify-zod.ts index 02636a066..4e898a9b5 100644 --- a/backend/src/server/plugins/fastify-zod.ts +++ b/backend/src/server/plugins/fastify-zod.ts @@ -49,14 +49,17 @@ function resolveSchema(maybeSchema: ZodAny | { properties: ZodAny }): Pick { - return ({ schema, url }: { schema: Schema; url: string }) => { + return ({ schema = {}, url }: { schema: Schema; url: string }) => { if (!schema) { return { - schema, + schema: { hide: true }, url }; } + if (typeof schema.hide === "undefined") { + schema.hide = true; + } const { response, headers, querystring, body, params, hide, ...rest } = schema; const transformed: FreeformRecord = {}; diff --git a/backend/src/server/plugins/secret-scanner.ts b/backend/src/server/plugins/secret-scanner.ts index d20008de7..65dbbb87f 100644 --- a/backend/src/server/plugins/secret-scanner.ts +++ b/backend/src/server/plugins/secret-scanner.ts @@ -1,3 +1,4 @@ +import type { EmitterWebhookEventName } from "@octokit/webhooks/dist-types/types"; import { PushEvent } from "@octokit/webhooks-types"; import { Probot } from "probot"; import SmeeClient from "smee-client"; @@ -19,7 +20,7 @@ export const registerSecretScannerGhApp = async (server: FastifyZodProvider) => app.on("installation", async (context) => { const { payload } = context; - logger.info("Installed secret scanner to:", { repositories: payload.repositories }); + logger.info({ repositories: payload.repositories }, "Installed secret scanner to"); }); app.on("push", async (context) => { @@ -54,17 +55,17 @@ export const registerSecretScannerGhApp = async (server: FastifyZodProvider) => rateLimit: writeLimit }, handler: async (req, res) => { - const eventName = req.headers["x-github-event"]; + const eventName = req.headers["x-github-event"] as EmitterWebhookEventName; const signatureSHA256 = req.headers["x-hub-signature-256"] as string; const id = req.headers["x-github-delivery"] as string; + await probot.webhooks.verifyAndReceive({ id, - // @ts-expect-error type name: eventName, - payload: req.body as string, + payload: JSON.stringify(req.body), signature: signatureSHA256 }); - void res.send("ok"); + return res.send("ok"); } }); } diff --git a/backend/src/server/plugins/serve-ui.ts b/backend/src/server/plugins/serve-ui.ts new file mode 100644 index 000000000..9f91d9774 --- /dev/null +++ b/backend/src/server/plugins/serve-ui.ts @@ -0,0 +1,64 @@ +import path from "node:path"; + +import staticServe from "@fastify/static"; + +import { getConfig, IS_PACKAGED } from "@app/lib/config/env"; + +// to enabled this u need to set standalone mode to true +export const registerServeUI = async ( + server: FastifyZodProvider, + { + standaloneMode, + dir + }: { + standaloneMode?: boolean; + dir: string; + } +) => { + // use this only for frontend runtime static non-sensitive configuration in standalone mode + // that app needs before loading like posthog dsn key + // for most of the other usecase use server config + server.route({ + method: "GET", + url: "/runtime-ui-env.js", + schema: { + hide: true + }, + handler: (_req, res) => { + const appCfg = getConfig(); + void res.type("application/javascript"); + const config = { + CAPTCHA_SITE_KEY: appCfg.CAPTCHA_SITE_KEY, + POSTHOG_API_KEY: appCfg.POSTHOG_PROJECT_API_KEY, + INTERCOM_ID: appCfg.INTERCOM_ID, + TELEMETRY_CAPTURING_ENABLED: appCfg.TELEMETRY_ENABLED + }; + const js = `window.__INFISICAL_RUNTIME_ENV__ = Object.freeze(${JSON.stringify(config)});`; + return res.send(js); + } + }); + + if (standaloneMode) { + const frontendName = IS_PACKAGED ? "frontend" : "frontend-build"; + const frontendPath = path.join(dir, frontendName); + await server.register(staticServe, { + root: frontendPath, + wildcard: false + }); + + server.route({ + method: "GET", + url: "/*", + schema: { + hide: true + }, + handler: (request, reply) => { + if (request.url.startsWith("/api")) { + reply.callNotFound(); + return; + } + return reply.sendFile("index.html"); + } + }); + } +}; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index b2d097092..2c1b768fb 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1,6 +1,6 @@ import { CronJob } from "cron"; -// import { Redis } from "ioredis"; import { Knex } from "knex"; +import { monitorEventLoopDelay } from "perf_hooks"; import { z } from "zod"; import { registerCertificateEstRouter } from "@app/ee/routes/est/certificate-est-router"; @@ -28,12 +28,24 @@ import { dynamicSecretLeaseQueueServiceFactory } from "@app/ee/services/dynamic- import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service"; import { externalKmsDALFactory } from "@app/ee/services/external-kms/external-kms-dal"; import { externalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service"; +import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal"; +import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; +import { orgGatewayConfigDALFactory } from "@app/ee/services/gateway/org-gateway-config-dal"; +import { projectGatewayDALFactory } from "@app/ee/services/gateway/project-gateway-dal"; import { groupDALFactory } from "@app/ee/services/group/group-dal"; import { groupServiceFactory } from "@app/ee/services/group/group-service"; import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; +import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; +import { HsmModule } from "@app/ee/services/hsm/hsm-types"; import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal"; import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { identityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service"; +import { kmipClientCertificateDALFactory } from "@app/ee/services/kmip/kmip-client-certificate-dal"; +import { kmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal"; +import { kmipOperationServiceFactory } from "@app/ee/services/kmip/kmip-operation-service"; +import { kmipOrgConfigDALFactory } from "@app/ee/services/kmip/kmip-org-config-dal"; +import { kmipOrgServerCertificateDALFactory } from "@app/ee/services/kmip/kmip-org-server-certificate-dal"; +import { kmipServiceFactory } from "@app/ee/services/kmip/kmip-service"; import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal"; import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; import { ldapGroupMapDALFactory } from "@app/ee/services/ldap-config/ldap-group-map-dal"; @@ -64,6 +76,9 @@ import { secretReplicationServiceFactory } from "@app/ee/services/secret-replica import { secretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal"; import { secretRotationQueueFactory } from "@app/ee/services/secret-rotation/secret-rotation-queue"; import { secretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service"; +import { secretRotationV2DALFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-dal"; +import { secretRotationV2QueueServiceFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-queue"; +import { secretRotationV2ServiceFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-service"; import { gitAppDALFactory } from "@app/ee/services/secret-scanning/git-app-dal"; import { gitAppInstallSessionDALFactory } from "@app/ee/services/secret-scanning/git-app-install-session-dal"; import { secretScanningDALFactory } from "@app/ee/services/secret-scanning/secret-scanning-dal"; @@ -74,15 +89,29 @@ import { snapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-da import { snapshotFolderDALFactory } from "@app/ee/services/secret-snapshot/snapshot-folder-dal"; import { snapshotSecretDALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-dal"; import { snapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-v2-dal"; +import { sshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; +import { sshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal"; +import { sshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh-certificate-authority-service"; +import { sshCertificateBodyDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-body-dal"; +import { sshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal"; +import { sshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal"; +import { sshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service"; +import { sshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal"; +import { sshHostLoginUserMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-user-mapping-dal"; +import { sshHostServiceFactory } from "@app/ee/services/ssh-host/ssh-host-service"; +import { sshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login-user-dal"; import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal"; import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service"; import { TKeyStoreFactory } from "@app/keystore/keystore"; -import { getConfig } from "@app/lib/config/env"; +import { getConfig, TEnvConfig } from "@app/lib/config/env"; +import { logger } from "@app/lib/logger"; import { TQueueServiceFactory } from "@app/queue"; import { readLimit } from "@app/server/config/rateLimiter"; import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue"; import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal"; import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service"; +import { appConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { appConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; import { authDALFactory } from "@app/services/auth/auth-dal"; import { authLoginServiceFactory } from "@app/services/auth/auth-login-service"; import { authPaswordServiceFactory } from "@app/services/auth/auth-password-service"; @@ -120,6 +149,8 @@ import { identityAzureAuthDALFactory } from "@app/services/identity-azure-auth/i import { identityAzureAuthServiceFactory } from "@app/services/identity-azure-auth/identity-azure-auth-service"; import { identityGcpAuthDALFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-dal"; import { identityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service"; +import { identityJwtAuthDALFactory } from "@app/services/identity-jwt-auth/identity-jwt-auth-dal"; +import { identityJwtAuthServiceFactory } from "@app/services/identity-jwt-auth/identity-jwt-auth-service"; import { identityKubernetesAuthDALFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-dal"; import { identityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service"; import { identityOidcAuthDALFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-dal"; @@ -157,6 +188,7 @@ import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-co import { projectDALFactory } from "@app/services/project/project-dal"; import { projectQueueFactory } from "@app/services/project/project-queue"; import { projectServiceFactory } from "@app/services/project/project-service"; +import { projectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal"; import { projectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { projectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { projectEnvDALFactory } from "@app/services/project-env/project-env-dal"; @@ -169,6 +201,7 @@ import { projectUserMembershipRoleDALFactory } from "@app/services/project-membe import { projectRoleDALFactory } from "@app/services/project-role/project-role-dal"; import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service"; import { dailyResourceCleanUpQueueServiceFactory } from "@app/services/resource-cleanup/resource-cleanup-queue"; +import { resourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal"; import { secretDALFactory } from "@app/services/secret/secret-dal"; import { secretQueueFactory } from "@app/services/secret/secret-queue"; import { secretServiceFactory } from "@app/services/secret/secret-service"; @@ -183,6 +216,9 @@ import { secretImportDALFactory } from "@app/services/secret-import/secret-impor import { secretImportServiceFactory } from "@app/services/secret-import/secret-import-service"; import { secretSharingDALFactory } from "@app/services/secret-sharing/secret-sharing-dal"; import { secretSharingServiceFactory } from "@app/services/secret-sharing/secret-sharing-service"; +import { secretSyncDALFactory } from "@app/services/secret-sync/secret-sync-dal"; +import { secretSyncQueueFactory } from "@app/services/secret-sync/secret-sync-queue"; +import { secretSyncServiceFactory } from "@app/services/secret-sync/secret-sync-service"; import { secretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; import { secretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service"; import { secretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal"; @@ -200,6 +236,8 @@ import { getServerCfg, superAdminServiceFactory } from "@app/services/super-admi import { telemetryDALFactory } from "@app/services/telemetry/telemetry-dal"; import { telemetryQueueServiceFactory } from "@app/services/telemetry/telemetry-queue"; import { telemetryServiceFactory } from "@app/services/telemetry/telemetry-service"; +import { totpConfigDALFactory } from "@app/services/totp/totp-config-dal"; +import { totpServiceFactory } from "@app/services/totp/totp-service"; import { userDALFactory } from "@app/services/user/user-dal"; import { userServiceFactory } from "@app/services/user/user-service"; import { userAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; @@ -218,15 +256,28 @@ import { registerV1Routes } from "./v1"; import { registerV2Routes } from "./v2"; import { registerV3Routes } from "./v3"; +const histogram = monitorEventLoopDelay({ resolution: 20 }); +histogram.enable(); + export const registerRoutes = async ( server: FastifyZodProvider, { auditLogDb, db, + hsmModule, smtp: smtpService, queue: queueService, - keyStore - }: { auditLogDb?: Knex; db: Knex; smtp: TSmtpService; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory } + keyStore, + envConfig + }: { + auditLogDb?: Knex; + db: Knex; + hsmModule: HsmModule; + smtp: TSmtpService; + queue: TQueueServiceFactory; + keyStore: TKeyStoreFactory; + envConfig: TEnvConfig; + } ) => { const appCfg = getConfig(); await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" }); @@ -246,6 +297,7 @@ export const registerRoutes = async ( const apiKeyDAL = apiKeyDALFactory(db); const projectDAL = projectDALFactory(db); + const projectSshConfigDAL = projectSshConfigDALFactory(db); const projectMembershipDAL = projectMembershipDALFactory(db); const projectUserAdditionalPrivilegeDAL = projectUserAdditionalPrivilegeDALFactory(db); const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(db); @@ -263,7 +315,7 @@ export const registerRoutes = async ( const secretVersionTagDAL = secretVersionTagDALFactory(db); const secretBlindIndexDAL = secretBlindIndexDALFactory(db); - const secretV2BridgeDAL = secretV2BridgeDALFactory(db); + const secretV2BridgeDAL = secretV2BridgeDALFactory({ db, keyStore }); const secretVersionV2BridgeDAL = secretVersionV2BridgeDALFactory(db); const secretVersionTagV2BridgeDAL = secretVersionV2TagBridgeDALFactory(db); @@ -287,12 +339,15 @@ export const registerRoutes = async ( const identityAwsAuthDAL = identityAwsAuthDALFactory(db); const identityGcpAuthDAL = identityGcpAuthDALFactory(db); const identityOidcAuthDAL = identityOidcAuthDALFactory(db); + const identityJwtAuthDAL = identityJwtAuthDALFactory(db); const identityAzureAuthDAL = identityAzureAuthDALFactory(db); const auditLogDAL = auditLogDALFactory(auditLogDb ?? db); const auditLogStreamDAL = auditLogStreamDALFactory(db); const trustedIpDAL = trustedIpDALFactory(db); const telemetryDAL = telemetryDALFactory(db); + const appConnectionDAL = appConnectionDALFactory(db); + const secretSyncDAL = secretSyncDALFactory(db, folderDAL); // ee db layer ops const permissionDAL = permissionDALFactory(db); @@ -331,6 +386,15 @@ export const registerRoutes = async ( const dynamicSecretDAL = dynamicSecretDALFactory(db); const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db); + const sshCertificateDAL = sshCertificateDALFactory(db); + const sshCertificateBodyDAL = sshCertificateBodyDALFactory(db); + const sshCertificateAuthorityDAL = sshCertificateAuthorityDALFactory(db); + const sshCertificateAuthoritySecretDAL = sshCertificateAuthoritySecretDALFactory(db); + const sshCertificateTemplateDAL = sshCertificateTemplateDALFactory(db); + const sshHostDAL = sshHostDALFactory(db); + const sshHostLoginUserDAL = sshHostLoginUserDALFactory(db); + const sshHostLoginUserMappingDAL = sshHostLoginUserMappingDALFactory(db); + const kmsDAL = kmskeyDALFactory(db); const internalKmsDAL = internalKmsDALFactory(db); const externalKmsDAL = externalKmsDALFactory(db); @@ -339,10 +403,22 @@ export const registerRoutes = async ( const slackIntegrationDAL = slackIntegrationDALFactory(db); const projectSlackConfigDAL = projectSlackConfigDALFactory(db); const workflowIntegrationDAL = workflowIntegrationDALFactory(db); + const totpConfigDAL = totpConfigDALFactory(db); const externalGroupOrgRoleMappingDAL = externalGroupOrgRoleMappingDALFactory(db); const projectTemplateDAL = projectTemplateDALFactory(db); + const resourceMetadataDAL = resourceMetadataDALFactory(db); + const kmipClientDAL = kmipClientDALFactory(db); + const kmipClientCertificateDAL = kmipClientCertificateDALFactory(db); + const kmipOrgConfigDAL = kmipOrgConfigDALFactory(db); + const kmipOrgServerCertificateDAL = kmipOrgServerCertificateDALFactory(db); + + const orgGatewayConfigDAL = orgGatewayConfigDALFactory(db); + const gatewayDAL = gatewayDALFactory(db); + const projectGatewayDAL = projectGatewayDALFactory(db); + + const secretRotationV2DAL = secretRotationV2DALFactory(db, folderDAL); const permissionService = permissionServiceFactory({ permissionDAL, @@ -351,15 +427,31 @@ export const registerRoutes = async ( serviceTokenDAL, projectDAL }); - const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore }); + const licenseService = licenseServiceFactory({ + permissionService, + orgDAL, + licenseDAL, + keyStore, + identityOrgMembershipDAL, + projectDAL + }); + + const hsmService = hsmServiceFactory({ + hsmModule, + envConfig + }); + const kmsService = kmsServiceFactory({ kmsRootConfigDAL, keyStore, kmsDAL, internalKmsDAL, orgDAL, - projectDAL + projectDAL, + hsmService, + envConfig }); + const externalKmsService = externalKmsServiceFactory({ kmsDAL, kmsService, @@ -375,13 +467,14 @@ export const registerRoutes = async ( permissionService }); - const auditLogQueue = auditLogQueueServiceFactory({ + const auditLogQueue = await auditLogQueueServiceFactory({ auditLogDAL, queueService, projectDAL, licenseService, auditLogStreamDAL }); + const auditLogService = auditLogServiceFactory({ auditLogDAL, permissionService, auditLogQueue }); const auditLogStreamService = auditLogStreamServiceFactory({ licenseService, @@ -394,14 +487,14 @@ export const registerRoutes = async ( permissionService, secretApprovalPolicyDAL, licenseService, - userDAL + userDAL, + secretApprovalRequestDAL }); const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL }); const samlService = samlConfigServiceFactory({ identityMetadataDAL, permissionService, - orgBotDAL, orgDAL, orgMembershipDAL, userDAL, @@ -409,7 +502,8 @@ export const registerRoutes = async ( samlConfigDAL, licenseService, tokenService, - smtpService + smtpService, + kmsService }); const groupService = groupServiceFactory({ userDAL, @@ -421,7 +515,8 @@ export const registerRoutes = async ( projectBotDAL, projectKeyDAL, permissionService, - licenseService + licenseService, + oidcConfigDAL }); const groupProjectService = groupProjectServiceFactory({ groupDAL, @@ -459,7 +554,6 @@ export const registerRoutes = async ( ldapGroupMapDAL, orgDAL, orgMembershipDAL, - orgBotDAL, groupDAL, groupProjectDAL, projectKeyDAL, @@ -471,7 +565,8 @@ export const registerRoutes = async ( permissionService, licenseService, tokenService, - smtpService + smtpService, + kmsService }); const telemetryService = telemetryServiceFactory({ @@ -495,19 +590,37 @@ export const registerRoutes = async ( projectMembershipDAL }); - const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL }); + const totpService = totpServiceFactory({ + totpConfigDAL, + userDAL, + kmsService + }); + + const loginService = authLoginServiceFactory({ + userDAL, + smtpService, + tokenService, + orgDAL, + totpService, + auditLogService + }); const passwordService = authPaswordServiceFactory({ tokenService, smtpService, authDAL, - userDAL + userDAL, + totpConfigDAL }); const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL, projectDAL }); const orgService = orgServiceFactory({ userAliasDAL, + queueService, identityMetadataDAL, + secretDAL, + secretV2BridgeDAL, + folderDAL, licenseService, samlConfigDAL, orgRoleDAL, @@ -528,6 +641,7 @@ export const registerRoutes = async ( groupDAL, orgBotDAL, oidcConfigDAL, + loginService, projectBotService }); const signupService = authSignupServiceFactory({ @@ -554,8 +668,14 @@ export const registerRoutes = async ( }); const superAdminService = superAdminServiceFactory({ userDAL, + identityDAL, + userAliasDAL, + identityTokenAuthDAL, + identityAccessTokenDAL, + identityOrgMembershipDAL, authService: loginService, serverCfgDAL: superAdminDAL, + kmsRootConfigDAL, orgService, keyStore, licenseService, @@ -563,6 +683,7 @@ export const registerRoutes = async ( }); const orgAdminService = orgAdminServiceFactory({ + smtpService, projectDAL, permissionService, projectUserMembershipRoleDAL, @@ -675,6 +796,37 @@ export const registerRoutes = async ( queueService }); + const sshCertificateAuthorityService = sshCertificateAuthorityServiceFactory({ + sshCertificateAuthorityDAL, + sshCertificateAuthoritySecretDAL, + sshCertificateTemplateDAL, + sshCertificateDAL, + sshCertificateBodyDAL, + kmsService, + permissionService + }); + + const sshCertificateTemplateService = sshCertificateTemplateServiceFactory({ + sshCertificateTemplateDAL, + sshCertificateAuthorityDAL, + permissionService + }); + + const sshHostService = sshHostServiceFactory({ + userDAL, + projectDAL, + projectSshConfigDAL, + sshCertificateAuthorityDAL, + sshCertificateAuthoritySecretDAL, + sshCertificateDAL, + sshCertificateBodyDAL, + sshHostDAL, + sshHostLoginUserDAL, + sshHostLoginUserMappingDAL, + permissionService, + kmsService + }); + const certificateAuthorityService = certificateAuthorityServiceFactory({ certificateAuthorityDAL, certificateAuthorityCertDAL, @@ -725,7 +877,8 @@ export const registerRoutes = async ( pkiAlertDAL, pkiCollectionDAL, permissionService, - smtpService + smtpService, + projectDAL }); const pkiCollectionService = pkiCollectionServiceFactory({ @@ -733,7 +886,8 @@ export const registerRoutes = async ( pkiCollectionItemDAL, certificateAuthorityDAL, certificateDAL, - permissionService + permissionService, + projectDAL }); const projectTemplateService = projectTemplateServiceFactory({ @@ -742,10 +896,85 @@ export const registerRoutes = async ( projectTemplateDAL }); + const integrationAuthService = integrationAuthServiceFactory({ + integrationAuthDAL, + integrationDAL, + permissionService, + projectBotService, + kmsService + }); + + const secretSyncQueue = secretSyncQueueFactory({ + queueService, + secretSyncDAL, + folderDAL, + secretImportDAL, + secretV2BridgeDAL, + kmsService, + keyStore, + auditLogService, + smtpService, + projectDAL, + projectMembershipDAL, + projectBotDAL, + secretDAL, + secretBlindIndexDAL, + secretVersionDAL, + secretTagDAL, + secretVersionTagDAL, + secretVersionV2BridgeDAL, + secretVersionTagV2BridgeDAL, + resourceMetadataDAL, + appConnectionDAL + }); + + const secretQueueService = secretQueueFactory({ + keyStore, + queueService, + secretDAL, + folderDAL, + integrationAuthService, + projectBotService, + integrationDAL, + secretImportDAL, + projectEnvDAL, + webhookDAL, + orgDAL, + auditLogService, + userDAL, + projectMembershipDAL, + smtpService, + projectDAL, + projectBotDAL, + secretVersionDAL, + secretBlindIndexDAL, + secretTagDAL, + secretVersionTagDAL, + kmsService, + secretVersionV2BridgeDAL, + secretV2BridgeDAL, + secretVersionTagV2BridgeDAL, + secretRotationDAL, + integrationAuthDAL, + snapshotDAL, + snapshotSecretV2BridgeDAL, + secretApprovalRequestDAL, + projectKeyDAL, + projectUserMembershipRoleDAL, + orgService, + resourceMetadataDAL, + secretSyncQueue + }); + const projectService = projectServiceFactory({ permissionService, projectDAL, + projectSshConfigDAL, + secretDAL, + secretV2BridgeDAL, + queueService, projectQueue: projectQueueService, + projectBotService, identityProjectDAL, identityOrgMembershipDAL, projectKeyDAL, @@ -761,6 +990,11 @@ export const registerRoutes = async ( certificateDAL, pkiAlertDAL, pkiCollectionDAL, + sshCertificateAuthorityDAL, + sshCertificateAuthoritySecretDAL, + sshCertificateDAL, + sshCertificateTemplateDAL, + sshHostDAL, projectUserMembershipRoleDAL, identityProjectMembershipRoleDAL, keyStore, @@ -769,7 +1003,9 @@ export const registerRoutes = async ( certificateTemplateDAL, projectSlackConfigDAL, slackIntegrationDAL, - projectTemplateService + projectTemplateService, + groupProjectDAL, + smtpService }); const projectEnvService = projectEnvServiceFactory({ @@ -812,7 +1048,8 @@ export const registerRoutes = async ( permissionService, webhookDAL, projectEnvDAL, - projectDAL + projectDAL, + kmsService }); const secretTagService = secretTagServiceFactory({ secretTagDAL, permissionService }); @@ -825,48 +1062,6 @@ export const registerRoutes = async ( projectDAL }); - const integrationAuthService = integrationAuthServiceFactory({ - integrationAuthDAL, - integrationDAL, - permissionService, - projectBotService, - kmsService - }); - const secretQueueService = secretQueueFactory({ - keyStore, - queueService, - secretDAL, - folderDAL, - integrationAuthService, - projectBotService, - integrationDAL, - secretImportDAL, - projectEnvDAL, - webhookDAL, - orgDAL, - auditLogService, - userDAL, - projectMembershipDAL, - smtpService, - projectDAL, - projectBotDAL, - secretVersionDAL, - secretBlindIndexDAL, - secretTagDAL, - secretVersionTagDAL, - kmsService, - secretVersionV2BridgeDAL, - secretV2BridgeDAL, - secretVersionTagV2BridgeDAL, - secretRotationDAL, - integrationAuthDAL, - snapshotDAL, - snapshotSecretV2BridgeDAL, - secretApprovalRequestDAL, - projectKeyDAL, - projectUserMembershipRoleDAL, - orgService - }); const secretImportService = secretImportServiceFactory({ licenseService, projectBotService, @@ -900,7 +1095,9 @@ export const registerRoutes = async ( secretApprovalPolicyService, secretApprovalRequestSecretDAL, kmsService, - snapshotService + snapshotService, + resourceMetadataDAL, + keyStore }); const secretApprovalRequestService = secretApprovalRequestServiceFactory({ @@ -927,7 +1124,8 @@ export const registerRoutes = async ( projectEnvDAL, userDAL, licenseService, - projectSlackConfigDAL + projectSlackConfigDAL, + resourceMetadataDAL }); const secretService = secretServiceFactory({ @@ -948,14 +1146,17 @@ export const registerRoutes = async ( secretApprovalRequestDAL, secretApprovalRequestSecretDAL, secretV2BridgeService, - secretApprovalRequestService + secretApprovalRequestService, + licenseService }); const secretSharingService = secretSharingServiceFactory({ permissionService, secretSharingDAL, orgDAL, - kmsService + kmsService, + smtpService, + userDAL }); const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({ @@ -966,7 +1167,10 @@ export const registerRoutes = async ( projectEnvDAL, projectMembershipDAL, projectDAL, - userDAL + userDAL, + accessApprovalRequestDAL, + additionalPrivilegeDAL: projectUserAdditionalPrivilegeDAL, + accessApprovalRequestReviewerDAL }); const accessApprovalRequestService = accessApprovalRequestServiceFactory({ @@ -1003,8 +1207,10 @@ export const registerRoutes = async ( kmsService, secretV2BridgeDAL, secretVersionV2TagBridgeDAL: secretVersionTagV2BridgeDAL, - secretVersionV2BridgeDAL + secretVersionV2BridgeDAL, + resourceMetadataDAL }); + const secretRotationQueue = secretRotationQueueFactory({ telemetryService, secretRotationDAL, @@ -1026,7 +1232,8 @@ export const registerRoutes = async ( secretDAL, folderDAL, projectBotService, - secretV2BridgeDAL + secretV2BridgeDAL, + kmsService }); const integrationService = integrationServiceFactory({ @@ -1056,7 +1263,8 @@ export const registerRoutes = async ( userDAL, permissionService, projectDAL, - accessTokenQueue + accessTokenQueue, + smtpService }); const identityService = identityServiceFactory({ @@ -1115,9 +1323,9 @@ export const registerRoutes = async ( identityKubernetesAuthDAL, identityOrgMembershipDAL, identityAccessTokenDAL, - orgBotDAL, permissionService, - licenseService + licenseService, + kmsService }); const identityGcpAuthService = identityGcpAuthServiceFactory({ identityGcpAuthDAL, @@ -1149,15 +1357,38 @@ export const registerRoutes = async ( identityAccessTokenDAL, permissionService, licenseService, - orgBotDAL + kmsService }); - const dynamicSecretProviders = buildDynamicSecretProviders(); + const identityJwtAuthService = identityJwtAuthServiceFactory({ + identityJwtAuthDAL, + permissionService, + identityAccessTokenDAL, + identityOrgMembershipDAL, + licenseService, + kmsService + }); + + const gatewayService = gatewayServiceFactory({ + permissionService, + gatewayDAL, + kmsService, + licenseService, + orgGatewayConfigDAL, + keyStore, + projectGatewayDAL + }); + + const dynamicSecretProviders = buildDynamicSecretProviders({ + gatewayService + }); const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({ queueService, dynamicSecretLeaseDAL, dynamicSecretProviders, - dynamicSecretDAL + dynamicSecretDAL, + folderDAL, + kmsService }); const dynamicSecretService = dynamicSecretServiceFactory({ projectDAL, @@ -1167,8 +1398,12 @@ export const registerRoutes = async ( dynamicSecretProviders, folderDAL, permissionService, - licenseService + licenseService, + kmsService, + projectGatewayDAL, + resourceMetadataDAL }); + const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({ projectDAL, permissionService, @@ -1177,18 +1412,21 @@ export const registerRoutes = async ( dynamicSecretLeaseDAL, dynamicSecretProviders, folderDAL, - licenseService + licenseService, + kmsService }); const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({ auditLogDAL, queueService, secretVersionDAL, + secretDAL, secretFolderVersionDAL: folderVersionDAL, snapshotDAL, identityAccessTokenDAL, secretSharingDAL, secretVersionV2DAL: secretVersionV2BridgeDAL, - identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL + identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL, + serviceTokenService }); const dailyExpiringPkiItemAlert = dailyExpiringPkiItemAlertQueueServiceFactory({ @@ -1204,13 +1442,21 @@ export const registerRoutes = async ( licenseService, tokenService, smtpService, - orgBotDAL, + kmsService, permissionService, - oidcConfigDAL + oidcConfigDAL, + projectBotDAL, + projectKeyDAL, + projectDAL, + userGroupMembershipDAL, + groupProjectDAL, + groupDAL, + auditLogService }); const userEngagementService = userEngagementServiceFactory({ - userDAL + userDAL, + orgDAL }); const slackService = slackServiceFactory({ @@ -1228,7 +1474,8 @@ export const registerRoutes = async ( const cmekService = cmekServiceFactory({ kmsDAL, kmsService, - permissionService + permissionService, + projectDAL }); const externalMigrationQueue = externalMigrationQueueFactory({ @@ -1244,7 +1491,8 @@ export const registerRoutes = async ( folderDAL, secretDAL: secretV2BridgeDAL, queueService, - secretV2BridgeService + secretV2BridgeService, + resourceMetadataDAL }); const migrationService = externalMigrationServiceFactory({ @@ -1260,11 +1508,78 @@ export const registerRoutes = async ( externalGroupOrgRoleMappingDAL }); + const appConnectionService = appConnectionServiceFactory({ + appConnectionDAL, + permissionService, + kmsService + }); + + const secretSyncService = secretSyncServiceFactory({ + secretSyncDAL, + permissionService, + appConnectionService, + folderDAL, + secretSyncQueue, + projectBotService, + keyStore + }); + + const kmipService = kmipServiceFactory({ + kmipClientDAL, + permissionService, + kmipClientCertificateDAL, + kmipOrgConfigDAL, + kmsService, + kmipOrgServerCertificateDAL, + licenseService + }); + + const kmipOperationService = kmipOperationServiceFactory({ + kmsService, + kmsDAL, + projectDAL, + kmipClientDAL, + permissionService + }); + + const secretRotationV2Service = secretRotationV2ServiceFactory({ + secretRotationV2DAL, + permissionService, + appConnectionService, + folderDAL, + projectBotService, + licenseService, + kmsService, + auditLogService, + secretV2BridgeDAL, + secretTagDAL, + secretVersionTagV2BridgeDAL, + secretVersionV2BridgeDAL, + keyStore, + resourceMetadataDAL, + snapshotService, + secretQueueService, + queueService, + appConnectionDAL + }); + + await secretRotationV2QueueServiceFactory({ + secretRotationV2Service, + secretRotationV2DAL, + queueService, + projectDAL, + projectMembershipDAL, + smtpService + }); + await superAdminService.initServerCfg(); - // + // setup the communication with license key server await licenseService.init(); + // Start HSM service if it's configured/enabled. + await hsmService.startService(); + await telemetryQueue.startTelemetryCheck(); await dailyResourceCleanUp.startCleanUp(); await dailyExpiringPkiItemAlert.startSendingAlerts(); @@ -1311,6 +1626,7 @@ export const registerRoutes = async ( identityAwsAuth: identityAwsAuthService, identityAzureAuth: identityAzureAuthService, identityOidcAuth: identityOidcAuthService, + identityJwtAuth: identityJwtAuthService, accessApprovalPolicy: accessApprovalPolicyService, accessApprovalRequest: accessApprovalRequestService, secretApprovalPolicy: secretApprovalPolicyService, @@ -1324,6 +1640,9 @@ export const registerRoutes = async ( auditLog: auditLogService, auditLogStream: auditLogStreamService, certificate: certificateService, + sshCertificateAuthority: sshCertificateAuthorityService, + sshCertificateTemplate: sshCertificateTemplateService, + sshHost: sshHostService, certificateAuthority: certificateAuthorityService, certificateTemplate: certificateTemplateService, certificateAuthorityCrl: certificateAuthorityCrlService, @@ -1342,13 +1661,21 @@ export const registerRoutes = async ( secretSharing: secretSharingService, userEngagement: userEngagementService, externalKms: externalKmsService, + hsm: hsmService, cmek: cmekService, orgAdmin: orgAdminService, slack: slackService, workflowIntegration: workflowIntegrationService, migration: migrationService, externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService, - projectTemplate: projectTemplateService + projectTemplate: projectTemplateService, + totp: totpService, + appConnection: appConnectionService, + secretSync: secretSyncService, + kmip: kmipService, + kmipOperation: kmipOperationService, + gateway: gatewayService, + secretRotationV2: secretRotationV2Service }); const cronJobs: CronJob[] = []; @@ -1357,10 +1684,15 @@ export const registerRoutes = async ( if (rateLimitSyncJob) { cronJobs.push(rateLimitSyncJob); } + const licenseSyncJob = await licenseService.initializeBackgroundSync(); + if (licenseSyncJob) { + cronJobs.push(licenseSyncJob); + } } server.decorate("store", { - user: userDAL + user: userDAL, + kmipClient: kmipClientDAL }); await server.register(injectIdentity, { userDAL, serviceTokenDAL }); @@ -1391,6 +1723,18 @@ export const registerRoutes = async ( const cfg = getConfig(); const serverCfg = await getServerCfg(); + const meanLagMs = histogram.mean / 1e6; + const maxLagMs = histogram.max / 1e6; + const p99LagMs = histogram.percentile(99) / 1e6; + + logger.info( + `Event loop stats - Mean: ${meanLagMs.toFixed(2)}ms, Max: ${maxLagMs.toFixed(2)}ms, p99: ${p99LagMs.toFixed( + 2 + )}ms` + ); + + logger.info(`Raw event loop stats: ${JSON.stringify(histogram, null, 2)}`); + // try { // await db.raw("SELECT NOW()"); // } catch (err) { diff --git a/backend/src/server/routes/sanitizedSchema/directory-config.ts b/backend/src/server/routes/sanitizedSchema/directory-config.ts new file mode 100644 index 000000000..61be4d9cf --- /dev/null +++ b/backend/src/server/routes/sanitizedSchema/directory-config.ts @@ -0,0 +1,42 @@ +import { LdapConfigsSchema, OidcConfigsSchema, SamlConfigsSchema } from "@app/db/schemas"; + +export const SanitizedSamlConfigSchema = SamlConfigsSchema.pick({ + id: true, + orgId: true, + isActive: true, + lastUsed: true, + createdAt: true, + updatedAt: true, + authProvider: true +}); + +export const SanitizedLdapConfigSchema = LdapConfigsSchema.pick({ + updatedAt: true, + createdAt: true, + isActive: true, + orgId: true, + id: true, + url: true, + searchBase: true, + searchFilter: true, + groupSearchBase: true, + uniqueUserAttribute: true, + groupSearchFilter: true +}); + +export const SanitizedOidcConfigSchema = OidcConfigsSchema.pick({ + id: true, + orgId: true, + isActive: true, + createdAt: true, + updatedAt: true, + lastUsed: true, + issuer: true, + jwksUri: true, + discoveryURL: true, + tokenEndpoint: true, + userinfoEndpoint: true, + configurationType: true, + allowedEmailDomains: true, + authorizationEndpoint: true +}); diff --git a/backend/src/server/routes/santizedSchemas/identitiy-additional-privilege.ts b/backend/src/server/routes/sanitizedSchema/identitiy-additional-privilege.ts similarity index 100% rename from backend/src/server/routes/santizedSchemas/identitiy-additional-privilege.ts rename to backend/src/server/routes/sanitizedSchema/identitiy-additional-privilege.ts diff --git a/backend/src/server/routes/santizedSchemas/permission.ts b/backend/src/server/routes/sanitizedSchema/permission.ts similarity index 100% rename from backend/src/server/routes/santizedSchemas/permission.ts rename to backend/src/server/routes/sanitizedSchema/permission.ts diff --git a/backend/src/server/routes/santizedSchemas/user-additional-privilege.ts b/backend/src/server/routes/sanitizedSchema/user-additional-privilege.ts similarity index 100% rename from backend/src/server/routes/santizedSchemas/user-additional-privilege.ts rename to backend/src/server/routes/sanitizedSchema/user-additional-privilege.ts diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index 87fa2b120..da300981c 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -7,11 +7,13 @@ import { ProjectRolesSchema, ProjectsSchema, SecretApprovalPoliciesSchema, + SecretTagsSchema, UsersSchema } from "@app/db/schemas"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; -import { UnpackedPermissionSchema } from "./santizedSchemas/permission"; +import { UnpackedPermissionSchema } from "./sanitizedSchema/permission"; // sometimes the return data must be santizied to avoid leaking important values // always prefer pick over omit in zod @@ -30,32 +32,58 @@ export const integrationAuthPubSchema = IntegrationAuthsSchema.pick({ export const DefaultResponseErrorsSchema = { 400: z.object({ + reqId: z.string(), statusCode: z.literal(400), message: z.string(), error: z.string() }), 404: z.object({ + reqId: z.string(), statusCode: z.literal(404), message: z.string(), error: z.string() }), 401: z.object({ + reqId: z.string(), statusCode: z.literal(401), - message: z.any(), - error: z.string() - }), - 403: z.object({ - statusCode: z.literal(403), message: z.string(), error: z.string() }), + 403: z.object({ + reqId: z.string(), + statusCode: z.literal(403), + message: z.string(), + details: z.any().optional(), + error: z.string() + }), + // Zod errors return a message of varying shapes and sizes, so z.any() is used here + 422: z.object({ + reqId: z.string(), + statusCode: z.literal(422), + message: z.any(), + error: z.string() + }), 500: z.object({ + reqId: z.string(), statusCode: z.literal(500), message: z.string(), error: z.string() }) }; +export const booleanSchema = z + .union([z.boolean(), z.string().trim()]) + .transform((value) => { + if (typeof value === "string") { + // ie if not empty, 0 or false, return true + return Boolean(value) && Number(value) !== 0 && value.toLowerCase() !== "false"; + } + + return value; + }) + .optional() + .default(true); + export const sapPubSchema = SecretApprovalPoliciesSchema.merge( z.object({ environment: z.object({ @@ -97,9 +125,19 @@ export const secretRawSchema = z.object({ secretReminderNote: z.string().nullable().optional(), secretReminderRepeatDays: z.number().nullable().optional(), skipMultilineEncoding: z.boolean().default(false).nullable().optional(), - metadata: z.unknown().nullable().optional(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + actor: z + .object({ + actorId: z.string().nullable().optional(), + actorType: z.string().nullable().optional(), + name: z.string().nullable().optional(), + membershipId: z.string().nullable().optional() + }) + .optional() + .nullable(), + isRotatedSecret: z.boolean().optional(), + rotationId: z.string().uuid().nullish() }); export const ProjectPermissionSchema = z.object({ @@ -189,12 +227,17 @@ export const SanitizedRoleSchemaV1 = ProjectRolesSchema.extend({ }); export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({ + encryptedInput: true, + keyEncoding: true, + inputCiphertext: true, inputIV: true, inputTag: true, - inputCiphertext: true, - keyEncoding: true, algorithm: true -}); +}).merge( + z.object({ + metadata: ResourceMetadataSchema.optional() + }) +); export const SanitizedAuditLogStreamSchema = z.object({ id: z.string(), @@ -206,6 +249,8 @@ export const SanitizedAuditLogStreamSchema = z.object({ export const SanitizedProjectSchema = ProjectsSchema.pick({ id: true, name: true, + description: true, + type: true, slug: true, autoCapitalization: true, orgId: true, @@ -215,5 +260,14 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({ upgradeStatus: true, pitVersionLimit: true, kmsCertificateKeyId: true, - auditLogsRetentionDays: true + auditLogsRetentionDays: true, + hasDeleteProtection: true +}); + +export const SanitizedTagSchema = SecretTagsSchema.pick({ + id: true, + slug: true, + color: true +}).extend({ + name: z.string() }); diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index bc0c725f0..6eb1804f1 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -1,12 +1,14 @@ +import DOMPurify from "isomorphic-dompurify"; import { z } from "zod"; -import { OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas"; +import { IdentitiesSchema, OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { LoginMethod } from "@app/services/super-admin/super-admin-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; @@ -71,7 +73,21 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { message: "At least one login method should be enabled." }), slackClientId: z.string().optional(), - slackClientSecret: z.string().optional() + slackClientSecret: z.string().optional(), + authConsentContent: z + .string() + .trim() + .refine((content) => DOMPurify.sanitize(content) === content, { + message: "Auth consent content contains unsafe HTML." + }) + .optional(), + pageFrameContent: z + .string() + .trim() + .refine((content) => DOMPurify.sanitize(content) === content, { + message: "Page frame content contains unsafe HTML." + }) + .optional() }), response: { 200: z.object({ @@ -82,7 +98,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { } }, onRequest: (req, res, done) => { - verifyAuth([AuthMode.JWT, AuthMode.API_KEY])(req, res, () => { + verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { verifySuperAdmin(req, res, done); }); }, @@ -102,7 +118,12 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { querystring: z.object({ searchTerm: z.string().default(""), offset: z.coerce.number().default(0), - limit: z.coerce.number().max(100).default(20) + limit: z.coerce.number().max(100).default(20), + // TODO: remove this once z.coerce.boolean() is supported + adminsOnly: z + .string() + .transform((val) => val === "true") + .default("false") }), response: { 200: z.object({ @@ -118,7 +139,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { } }, onRequest: (req, res, done) => { - verifyAuth([AuthMode.JWT])(req, res, () => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { verifySuperAdmin(req, res, done); }); }, @@ -133,6 +154,47 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/identity-management/identities", + config: { + rateLimit: readLimit + }, + schema: { + querystring: z.object({ + searchTerm: z.string().default(""), + offset: z.coerce.number().default(0), + limit: z.coerce.number().max(100).default(20) + }), + response: { + 200: z.object({ + identities: IdentitiesSchema.pick({ + name: true, + id: true + }) + .extend({ + isInstanceAdmin: z.boolean() + }) + .array() + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async (req) => { + const identities = await server.services.superAdmin.getIdentities({ + ...req.query + }); + + return { + identities + }; + } + }); + server.route({ method: "GET", url: "/integrations/slack/config", @@ -148,7 +210,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { } }, onRequest: (req, res, done) => { - verifyAuth([AuthMode.JWT])(req, res, () => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { verifySuperAdmin(req, res, done); }); }, @@ -182,7 +244,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { } }, onRequest: (req, res, done) => { - verifyAuth([AuthMode.JWT])(req, res, () => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { verifySuperAdmin(req, res, done); }); }, @@ -195,6 +257,78 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "PATCH", + url: "/user-management/users/:userId/admin-access", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + userId: z.string() + }) + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async (req) => { + await server.services.superAdmin.grantServerAdminAccessToUser(req.params.userId); + } + }); + + server.route({ + method: "GET", + url: "/encryption-strategies", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: z.object({ + strategies: z + .object({ + strategy: z.nativeEnum(RootKeyEncryptionStrategy), + enabled: z.boolean() + }) + .array() + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + + handler: async () => { + const encryptionDetails = await server.services.superAdmin.getConfiguredEncryptionStrategies(); + return encryptionDetails; + } + }); + + server.route({ + method: "PATCH", + url: "/encryption-strategies", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + strategy: z.nativeEnum(RootKeyEncryptionStrategy) + }) + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async (req) => { + await server.services.superAdmin.updateRootEncryptionStrategy(req.body.strategy); + } + }); + server.route({ method: "POST", url: "/signup", @@ -264,4 +398,141 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }; } }); + + server.route({ + method: "DELETE", + url: "/identity-management/identities/:identityId/super-admin-access", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + identityId: z.string() + }), + response: { + 200: z.object({ + identity: IdentitiesSchema.pick({ + name: true, + id: true + }) + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async (req) => { + const identity = await server.services.superAdmin.deleteIdentitySuperAdminAccess( + req.params.identityId, + req.permission.id + ); + + return { + identity + }; + } + }); + + server.route({ + method: "DELETE", + url: "/user-management/users/:userId/admin-access", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + userId: z.string() + }), + response: { + 200: z.object({ + user: UsersSchema.pick({ + username: true, + firstName: true, + lastName: true, + email: true, + id: true + }) + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async (req) => { + const user = await server.services.superAdmin.deleteUserSuperAdminAccess(req.params.userId); + + return { + user + }; + } + }); + + server.route({ + method: "POST", + url: "/bootstrap", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + email: z.string().email().trim().min(1), + password: z.string().trim().min(1), + organization: z.string().trim().min(1) + }), + response: { + 200: z.object({ + message: z.string(), + user: UsersSchema.pick({ + username: true, + firstName: true, + lastName: true, + email: true, + id: true, + superAdmin: true + }), + organization: OrganizationsSchema.pick({ + id: true, + name: true, + slug: true + }), + identity: IdentitiesSchema.pick({ + id: true, + name: true + }).extend({ + credentials: z.object({ + token: z.string() + }) // would just be Token AUTH for now + }) + }) + } + }, + handler: async (req) => { + const { user, organization, machineIdentity } = await server.services.superAdmin.bootstrapInstance({ + ...req.body, + organizationName: req.body.organization + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.AdminInit, + distinctId: user.user.username ?? "", + properties: { + username: user.user.username, + email: user.user.email ?? "", + lastName: user.user.lastName || "", + firstName: user.user.firstName || "" + } + }); + + return { + message: "Successfully bootstrapped instance", + user: user.user, + organization, + identity: machineIdentity + }; + } + }); }; diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts new file mode 100644 index 000000000..0bc8f7c59 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts @@ -0,0 +1,341 @@ +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags, AppConnections } from "@app/lib/api-docs"; +import { startsWithVowel } from "@app/lib/fn"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps"; +import { TAppConnection, TAppConnectionInput } from "@app/services/app-connection/app-connection-types"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerAppConnectionEndpoints = ({ + server, + app, + createSchema, + updateSchema, + sanitizedResponseSchema +}: { + app: AppConnection; + server: FastifyZodProvider; + createSchema: z.ZodType<{ + name: string; + method: I["method"]; + credentials: I["credentials"]; + description?: string | null; + isPlatformManagedCredentials?: boolean; + }>; + updateSchema: z.ZodType<{ + name?: string; + credentials?: I["credentials"]; + description?: string | null; + isPlatformManagedCredentials?: boolean; + }>; + sanitizedResponseSchema: z.ZodTypeAny; +}) => { + const appName = APP_CONNECTION_NAME_MAP[app]; + + server.route({ + method: "GET", + url: `/`, + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.AppConnections], + description: `List the ${appName} Connections for the current organization.`, + response: { + 200: z.object({ appConnections: sanitizedResponseSchema.array() }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const appConnections = (await server.services.appConnection.listAppConnectionsByOrg(req.permission, app)) as T[]; + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.GET_APP_CONNECTIONS, + metadata: { + app, + count: appConnections.length, + connectionIds: appConnections.map((connection) => connection.id) + } + } + }); + + return { appConnections }; + } + }); + + server.route({ + method: "GET", + url: "/available", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.AppConnections], + description: `List the ${appName} Connections the current user has permission to establish connections with.`, + response: { + 200: z.object({ + appConnections: z + .object({ + app: z.literal(app), + name: z.string(), + id: z.string().uuid() + }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const appConnections = await server.services.appConnection.listAvailableAppConnectionsForUser( + app, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.GET_AVAILABLE_APP_CONNECTIONS_DETAILS, + metadata: { + app, + count: appConnections.length, + connectionIds: appConnections.map((connection) => connection.id) + } + } + }); + + return { appConnections }; + } + }); + + server.route({ + method: "GET", + url: "/:connectionId", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.AppConnections], + description: `Get the specified ${appName} Connection by ID.`, + params: z.object({ + connectionId: z.string().uuid().describe(AppConnections.GET_BY_ID(app).connectionId) + }), + response: { + 200: z.object({ appConnection: sanitizedResponseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { connectionId } = req.params; + + const appConnection = (await server.services.appConnection.findAppConnectionById( + app, + connectionId, + req.permission + )) as T; + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.GET_APP_CONNECTION, + metadata: { + connectionId + } + } + }); + + return { appConnection }; + } + }); + + server.route({ + method: "GET", + url: `/connection-name/:connectionName`, + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.AppConnections], + description: `Get the specified ${appName} Connection by name.`, + params: z.object({ + connectionName: z + .string() + .trim() + .min(1, "Connection name required") + .describe(AppConnections.GET_BY_NAME(app).connectionName) + }), + response: { + 200: z.object({ appConnection: sanitizedResponseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { connectionName } = req.params; + + const appConnection = (await server.services.appConnection.findAppConnectionByName( + app, + connectionName, + req.permission + )) as T; + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.GET_APP_CONNECTION, + metadata: { + connectionId: appConnection.id + } + } + }); + + return { appConnection }; + } + }); + + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.AppConnections], + description: `Create ${ + startsWithVowel(appName) ? "an" : "a" + } ${appName} Connection for the current organization.`, + body: createSchema, + response: { + 200: z.object({ appConnection: sanitizedResponseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { name, method, credentials, description, isPlatformManagedCredentials } = req.body; + + const appConnection = (await server.services.appConnection.createAppConnection( + { name, method, app, credentials, description, isPlatformManagedCredentials }, + req.permission + )) as T; + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.CREATE_APP_CONNECTION, + metadata: { + name, + method, + app, + connectionId: appConnection.id, + isPlatformManagedCredentials + } + } + }); + + return { appConnection }; + } + }); + + server.route({ + method: "PATCH", + url: "/:connectionId", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.AppConnections], + description: `Update the specified ${appName} Connection.`, + params: z.object({ + connectionId: z.string().uuid().describe(AppConnections.UPDATE(app).connectionId) + }), + body: updateSchema, + response: { + 200: z.object({ appConnection: sanitizedResponseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { name, credentials, description, isPlatformManagedCredentials } = req.body; + const { connectionId } = req.params; + + const appConnection = (await server.services.appConnection.updateAppConnection( + { name, credentials, connectionId, description, isPlatformManagedCredentials }, + req.permission + )) as T; + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.UPDATE_APP_CONNECTION, + metadata: { + name, + description, + credentialsUpdated: Boolean(credentials), + connectionId, + isPlatformManagedCredentials + } + } + }); + + return { appConnection }; + } + }); + + server.route({ + method: "DELETE", + url: `/:connectionId`, + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.AppConnections], + description: `Delete the specified ${appName} Connection.`, + params: z.object({ + connectionId: z.string().uuid().describe(AppConnections.DELETE(app).connectionId) + }), + response: { + 200: z.object({ appConnection: sanitizedResponseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { connectionId } = req.params; + + const appConnection = (await server.services.appConnection.deleteAppConnection( + app, + connectionId, + req.permission + )) as T; + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.DELETE_APP_CONNECTION, + metadata: { + connectionId + } + } + }); + + return { appConnection }; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts new file mode 100644 index 000000000..25bef7270 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -0,0 +1,139 @@ +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags } from "@app/lib/api-docs"; +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { Auth0ConnectionListItemSchema, SanitizedAuth0ConnectionSchema } from "@app/services/app-connection/auth0"; +import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws"; +import { + AzureAppConfigurationConnectionListItemSchema, + SanitizedAzureAppConfigurationConnectionSchema +} from "@app/services/app-connection/azure-app-configuration"; +import { + AzureKeyVaultConnectionListItemSchema, + SanitizedAzureKeyVaultConnectionSchema +} from "@app/services/app-connection/azure-key-vault"; +import { + CamundaConnectionListItemSchema, + SanitizedCamundaConnectionSchema +} from "@app/services/app-connection/camunda"; +import { + DatabricksConnectionListItemSchema, + SanitizedDatabricksConnectionSchema +} from "@app/services/app-connection/databricks"; +import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp"; +import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github"; +import { + HumanitecConnectionListItemSchema, + SanitizedHumanitecConnectionSchema +} from "@app/services/app-connection/humanitec"; +import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql"; +import { + PostgresConnectionListItemSchema, + SanitizedPostgresConnectionSchema +} from "@app/services/app-connection/postgres"; +import { + SanitizedTerraformCloudConnectionSchema, + TerraformCloudConnectionListItemSchema +} from "@app/services/app-connection/terraform-cloud"; +import { SanitizedVercelConnectionSchema, VercelConnectionListItemSchema } from "@app/services/app-connection/vercel"; +import { + SanitizedWindmillConnectionSchema, + WindmillConnectionListItemSchema +} from "@app/services/app-connection/windmill"; +import { AuthMode } from "@app/services/auth/auth-type"; + +// can't use discriminated due to multiple schemas for certain apps +const SanitizedAppConnectionSchema = z.union([ + ...SanitizedAwsConnectionSchema.options, + ...SanitizedGitHubConnectionSchema.options, + ...SanitizedGcpConnectionSchema.options, + ...SanitizedAzureKeyVaultConnectionSchema.options, + ...SanitizedAzureAppConfigurationConnectionSchema.options, + ...SanitizedDatabricksConnectionSchema.options, + ...SanitizedHumanitecConnectionSchema.options, + ...SanitizedTerraformCloudConnectionSchema.options, + ...SanitizedVercelConnectionSchema.options, + ...SanitizedPostgresConnectionSchema.options, + ...SanitizedMsSqlConnectionSchema.options, + ...SanitizedCamundaConnectionSchema.options, + ...SanitizedWindmillConnectionSchema.options, + ...SanitizedAuth0ConnectionSchema.options +]); + +const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ + AwsConnectionListItemSchema, + GitHubConnectionListItemSchema, + GcpConnectionListItemSchema, + AzureKeyVaultConnectionListItemSchema, + AzureAppConfigurationConnectionListItemSchema, + DatabricksConnectionListItemSchema, + HumanitecConnectionListItemSchema, + TerraformCloudConnectionListItemSchema, + VercelConnectionListItemSchema, + PostgresConnectionListItemSchema, + MsSqlConnectionListItemSchema, + CamundaConnectionListItemSchema, + WindmillConnectionListItemSchema, + Auth0ConnectionListItemSchema +]); + +export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/options", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.AppConnections], + description: "List the available App Connection Options.", + response: { + 200: z.object({ + appConnectionOptions: AppConnectionOptionsSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: () => { + const appConnectionOptions = server.services.appConnection.listAppConnectionOptions(); + return { appConnectionOptions }; + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.AppConnections], + description: "List all the App Connections for the current organization.", + response: { + 200: z.object({ appConnections: SanitizedAppConnectionSchema.array() }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const appConnections = await server.services.appConnection.listAppConnectionsByOrg(req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.GET_APP_CONNECTIONS, + metadata: { + count: appConnections.length, + connectionIds: appConnections.map((connection) => connection.id) + } + } + }); + + return { appConnections }; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/auth0-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/auth0-connection-router.ts new file mode 100644 index 000000000..db17c8eac --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/auth0-connection-router.ts @@ -0,0 +1,51 @@ +import { z } from "zod"; + +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateAuth0ConnectionSchema, + SanitizedAuth0ConnectionSchema, + UpdateAuth0ConnectionSchema +} from "@app/services/app-connection/auth0"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerAuth0ConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.Auth0, + server, + sanitizedResponseSchema: SanitizedAuth0ConnectionSchema, + createSchema: CreateAuth0ConnectionSchema, + updateSchema: UpdateAuth0ConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + + server.route({ + method: "GET", + url: `/:connectionId/clients`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z.object({ + clients: z.object({ name: z.string(), id: z.string() }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const clients = await server.services.appConnection.auth0.listClients(connectionId, req.permission); + + return { clients }; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/aws-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/aws-connection-router.ts new file mode 100644 index 000000000..674e6e417 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/aws-connection-router.ts @@ -0,0 +1,62 @@ +import { z } from "zod"; + +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums"; +import { + CreateAwsConnectionSchema, + SanitizedAwsConnectionSchema, + UpdateAwsConnectionSchema +} from "@app/services/app-connection/aws"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerAwsConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.AWS, + server, + sanitizedResponseSchema: SanitizedAwsConnectionSchema, + createSchema: CreateAwsConnectionSchema, + updateSchema: UpdateAwsConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + + server.route({ + method: "GET", + url: `/:connectionId/kms-keys`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + querystring: z.object({ + region: z.nativeEnum(AWSRegion), + destination: z.enum([SecretSync.AWSParameterStore, SecretSync.AWSSecretsManager]) + }), + response: { + 200: z.object({ + kmsKeys: z.object({ alias: z.string(), id: z.string() }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const kmsKeys = await server.services.appConnection.aws.listKmsKeys( + { + connectionId, + ...req.query + }, + req.permission + ); + + return { kmsKeys }; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/azure-app-configuration-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/azure-app-configuration-connection-router.ts new file mode 100644 index 000000000..3f3ca7a1a --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/azure-app-configuration-connection-router.ts @@ -0,0 +1,18 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateAzureAppConfigurationConnectionSchema, + SanitizedAzureAppConfigurationConnectionSchema, + UpdateAzureAppConfigurationConnectionSchema +} from "@app/services/app-connection/azure-app-configuration"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerAzureAppConfigurationConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.AzureAppConfiguration, + server, + sanitizedResponseSchema: SanitizedAzureAppConfigurationConnectionSchema, + createSchema: CreateAzureAppConfigurationConnectionSchema, + updateSchema: UpdateAzureAppConfigurationConnectionSchema + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/azure-key-vault-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/azure-key-vault-connection-router.ts new file mode 100644 index 000000000..7097ed98b --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/azure-key-vault-connection-router.ts @@ -0,0 +1,18 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateAzureKeyVaultConnectionSchema, + SanitizedAzureKeyVaultConnectionSchema, + UpdateAzureKeyVaultConnectionSchema +} from "@app/services/app-connection/azure-key-vault"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerAzureKeyVaultConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.AzureKeyVault, + server, + sanitizedResponseSchema: SanitizedAzureKeyVaultConnectionSchema, + createSchema: CreateAzureKeyVaultConnectionSchema, + updateSchema: UpdateAzureKeyVaultConnectionSchema + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/camunda-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/camunda-connection-router.ts new file mode 100644 index 000000000..7da0b7e5f --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/camunda-connection-router.ts @@ -0,0 +1,51 @@ +import { z } from "zod"; + +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateCamundaConnectionSchema, + SanitizedCamundaConnectionSchema, + UpdateCamundaConnectionSchema +} from "@app/services/app-connection/camunda"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerCamundaConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.Camunda, + server, + sanitizedResponseSchema: SanitizedCamundaConnectionSchema, + createSchema: CreateCamundaConnectionSchema, + updateSchema: UpdateCamundaConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + + server.route({ + method: "GET", + url: `/:connectionId/clusters`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z.object({ + clusters: z.object({ uuid: z.string(), name: z.string() }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const clusters = await server.services.appConnection.camunda.listClusters(connectionId, req.permission); + + return { clusters }; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/databricks-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/databricks-connection-router.ts new file mode 100644 index 000000000..7fdb7f3a2 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/databricks-connection-router.ts @@ -0,0 +1,54 @@ +import { z } from "zod"; + +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateDatabricksConnectionSchema, + SanitizedDatabricksConnectionSchema, + UpdateDatabricksConnectionSchema +} from "@app/services/app-connection/databricks"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerDatabricksConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.Databricks, + server, + sanitizedResponseSchema: SanitizedDatabricksConnectionSchema, + createSchema: CreateDatabricksConnectionSchema, + updateSchema: UpdateDatabricksConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + + server.route({ + method: "GET", + url: `/:connectionId/secret-scopes`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z.object({ + secretScopes: z.object({ name: z.string() }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const secretScopes = await server.services.appConnection.databricks.listSecretScopes( + connectionId, + req.permission + ); + + return { secretScopes }; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/gcp-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/gcp-connection-router.ts new file mode 100644 index 000000000..f92d5e668 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/gcp-connection-router.ts @@ -0,0 +1,48 @@ +import z from "zod"; + +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateGcpConnectionSchema, + SanitizedGcpConnectionSchema, + UpdateGcpConnectionSchema +} from "@app/services/app-connection/gcp"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerGcpConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.GCP, + server, + sanitizedResponseSchema: SanitizedGcpConnectionSchema, + createSchema: CreateGcpConnectionSchema, + updateSchema: UpdateGcpConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + server.route({ + method: "GET", + url: `/:connectionId/secret-manager-projects`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z.object({ id: z.string(), name: z.string() }).array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const projects = await server.services.appConnection.gcp.listSecretManagerProjects(connectionId, req.permission); + + return projects; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/github-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/github-connection-router.ts new file mode 100644 index 000000000..8444b0cd6 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/github-connection-router.ts @@ -0,0 +1,117 @@ +import { z } from "zod"; + +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateGitHubConnectionSchema, + SanitizedGitHubConnectionSchema, + UpdateGitHubConnectionSchema +} from "@app/services/app-connection/github"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerGitHubConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.GitHub, + server, + sanitizedResponseSchema: SanitizedGitHubConnectionSchema, + createSchema: CreateGitHubConnectionSchema, + updateSchema: UpdateGitHubConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + + server.route({ + method: "GET", + url: `/:connectionId/repositories`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z.object({ + repositories: z + .object({ id: z.number(), name: z.string(), owner: z.object({ login: z.string(), id: z.number() }) }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const repositories = await server.services.appConnection.github.listRepositories(connectionId, req.permission); + + return { repositories }; + } + }); + + server.route({ + method: "GET", + url: `/:connectionId/organizations`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z.object({ + organizations: z.object({ id: z.number(), login: z.string() }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const organizations = await server.services.appConnection.github.listOrganizations(connectionId, req.permission); + + return { organizations }; + } + }); + + server.route({ + method: "GET", + url: `/:connectionId/environments`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + querystring: z.object({ + repo: z.string().min(1, "Repository name is required"), + owner: z.string().min(1, "Repository owner name is required") + }), + response: { + 200: z.object({ + environments: z.object({ id: z.number(), name: z.string() }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + const { repo, owner } = req.query; + + const environments = await server.services.appConnection.github.listEnvironments( + { + connectionId, + repo, + owner + }, + req.permission + ); + + return { environments }; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/humanitec-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/humanitec-connection-router.ts new file mode 100644 index 000000000..2d462c4ef --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/humanitec-connection-router.ts @@ -0,0 +1,69 @@ +import z from "zod"; + +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateHumanitecConnectionSchema, + HumanitecOrgWithApps, + SanitizedHumanitecConnectionSchema, + UpdateHumanitecConnectionSchema +} from "@app/services/app-connection/humanitec"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerHumanitecConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.Humanitec, + server, + sanitizedResponseSchema: SanitizedHumanitecConnectionSchema, + createSchema: CreateHumanitecConnectionSchema, + updateSchema: UpdateHumanitecConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + server.route({ + method: "GET", + url: `/:connectionId/organizations`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z + .object({ + id: z.string(), + name: z.string(), + apps: z + .object({ + id: z.string(), + name: z.string(), + envs: z + .object({ + id: z.string(), + name: z.string() + }) + .array() + }) + .array() + }) + .array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const organizations: HumanitecOrgWithApps[] = await server.services.appConnection.humanitec.listOrganizations( + connectionId, + req.permission + ); + + return organizations; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts new file mode 100644 index 000000000..a833b6882 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -0,0 +1,36 @@ +import { registerAuth0ConnectionRouter } from "@app/server/routes/v1/app-connection-routers/auth0-connection-router"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +import { registerAwsConnectionRouter } from "./aws-connection-router"; +import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router"; +import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router"; +import { registerCamundaConnectionRouter } from "./camunda-connection-router"; +import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; +import { registerGcpConnectionRouter } from "./gcp-connection-router"; +import { registerGitHubConnectionRouter } from "./github-connection-router"; +import { registerHumanitecConnectionRouter } from "./humanitec-connection-router"; +import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; +import { registerPostgresConnectionRouter } from "./postgres-connection-router"; +import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router"; +import { registerVercelConnectionRouter } from "./vercel-connection-router"; +import { registerWindmillConnectionRouter } from "./windmill-connection-router"; + +export * from "./app-connection-router"; + +export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record Promise> = + { + [AppConnection.AWS]: registerAwsConnectionRouter, + [AppConnection.GitHub]: registerGitHubConnectionRouter, + [AppConnection.GCP]: registerGcpConnectionRouter, + [AppConnection.AzureKeyVault]: registerAzureKeyVaultConnectionRouter, + [AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter, + [AppConnection.Databricks]: registerDatabricksConnectionRouter, + [AppConnection.Humanitec]: registerHumanitecConnectionRouter, + [AppConnection.TerraformCloud]: registerTerraformCloudConnectionRouter, + [AppConnection.Vercel]: registerVercelConnectionRouter, + [AppConnection.Postgres]: registerPostgresConnectionRouter, + [AppConnection.MsSql]: registerMsSqlConnectionRouter, + [AppConnection.Camunda]: registerCamundaConnectionRouter, + [AppConnection.Windmill]: registerWindmillConnectionRouter, + [AppConnection.Auth0]: registerAuth0ConnectionRouter + }; diff --git a/backend/src/server/routes/v1/app-connection-routers/mssql-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/mssql-connection-router.ts new file mode 100644 index 000000000..355630718 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/mssql-connection-router.ts @@ -0,0 +1,18 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateMsSqlConnectionSchema, + SanitizedMsSqlConnectionSchema, + UpdateMsSqlConnectionSchema +} from "@app/services/app-connection/mssql"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerMsSqlConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.MsSql, + server, + sanitizedResponseSchema: SanitizedMsSqlConnectionSchema, + createSchema: CreateMsSqlConnectionSchema, + updateSchema: UpdateMsSqlConnectionSchema + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/postgres-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/postgres-connection-router.ts new file mode 100644 index 000000000..8662f2e52 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/postgres-connection-router.ts @@ -0,0 +1,18 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreatePostgresConnectionSchema, + SanitizedPostgresConnectionSchema, + UpdatePostgresConnectionSchema +} from "@app/services/app-connection/postgres"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerPostgresConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.Postgres, + server, + sanitizedResponseSchema: SanitizedPostgresConnectionSchema, + createSchema: CreatePostgresConnectionSchema, + updateSchema: UpdatePostgresConnectionSchema + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/terraform-cloud-router.ts b/backend/src/server/routes/v1/app-connection-routers/terraform-cloud-router.ts new file mode 100644 index 000000000..6e0cab34a --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/terraform-cloud-router.ts @@ -0,0 +1,69 @@ +import z from "zod"; + +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateTerraformCloudConnectionSchema, + SanitizedTerraformCloudConnectionSchema, + TTerraformCloudOrganization, + UpdateTerraformCloudConnectionSchema +} from "@app/services/app-connection/terraform-cloud"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerTerraformCloudConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.TerraformCloud, + server, + sanitizedResponseSchema: SanitizedTerraformCloudConnectionSchema, + createSchema: CreateTerraformCloudConnectionSchema, + updateSchema: UpdateTerraformCloudConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + server.route({ + method: "GET", + url: `/:connectionId/organizations`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z + .object({ + id: z.string(), + name: z.string(), + variableSets: z + .object({ + id: z.string(), + name: z.string(), + description: z.string().optional(), + global: z.boolean().optional() + }) + .array(), + workspaces: z + .object({ + id: z.string(), + name: z.string() + }) + .array() + }) + .array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const organizations: TTerraformCloudOrganization[] = + await server.services.appConnection.terraformCloud.listOrganizations(connectionId, req.permission); + + return organizations; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/vercel-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/vercel-connection-router.ts new file mode 100644 index 000000000..079870305 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/vercel-connection-router.ts @@ -0,0 +1,77 @@ +import z from "zod"; + +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateVercelConnectionSchema, + SanitizedVercelConnectionSchema, + UpdateVercelConnectionSchema, + VercelOrgWithApps +} from "@app/services/app-connection/vercel"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerVercelConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.Vercel, + server, + sanitizedResponseSchema: SanitizedVercelConnectionSchema, + createSchema: CreateVercelConnectionSchema, + updateSchema: UpdateVercelConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + server.route({ + method: "GET", + url: `/:connectionId/projects`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z + .object({ + id: z.string(), + name: z.string(), + slug: z.string(), + apps: z + .object({ + id: z.string(), + name: z.string(), + envs: z + .object({ + id: z.string(), + slug: z.string(), + type: z.string(), + target: z.array(z.string()).optional(), + description: z.string().optional(), + createdAt: z.number().optional(), + updatedAt: z.number().optional() + }) + .array() + .optional(), + previewBranches: z.array(z.string()).optional() + }) + .array() + }) + .array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const projects: VercelOrgWithApps[] = await server.services.appConnection.vercel.listProjects( + connectionId, + req.permission + ); + + return projects; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/windmill-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/windmill-connection-router.ts new file mode 100644 index 000000000..455105deb --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/windmill-connection-router.ts @@ -0,0 +1,53 @@ +import z from "zod"; + +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateWindmillConnectionSchema, + SanitizedWindmillConnectionSchema, + UpdateWindmillConnectionSchema +} from "@app/services/app-connection/windmill"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerWindmillConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.Windmill, + server, + sanitizedResponseSchema: SanitizedWindmillConnectionSchema, + createSchema: CreateWindmillConnectionSchema, + updateSchema: UpdateWindmillConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + server.route({ + method: "GET", + url: `/:connectionId/workspaces`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z + .object({ + id: z.string(), + name: z.string() + }) + .array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const workspaces = await server.services.appConnection.windmill.listWorkspaces(connectionId, req.permission); + + return workspaces; + } + }); +}; diff --git a/backend/src/server/routes/v1/auth-router.ts b/backend/src/server/routes/v1/auth-router.ts index d67e7b562..04ca958c6 100644 --- a/backend/src/server/routes/v1/auth-router.ts +++ b/backend/src/server/routes/v1/auth-router.ts @@ -2,10 +2,9 @@ import jwt from "jsonwebtoken"; import { z } from "zod"; import { getConfig } from "@app/lib/config/env"; -import { NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { authRateLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { AuthMode, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type"; +import { AuthMode, AuthTokenType } from "@app/services/auth/auth-type"; export const registerAuthRoutes = async (server: FastifyZodProvider) => { server.route({ @@ -21,18 +20,19 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }), handler: async (req, res) => { + const { decodedToken } = await server.services.authToken.validateRefreshToken(req.cookies.jid); const appCfg = getConfig(); - if (req.auth.authMode === AuthMode.JWT) { - await server.services.login.logout(req.permission.id, req.auth.tokenVersionId); - } + + await server.services.login.logout(decodedToken.userId, decodedToken.tokenVersionId); + void res.cookie("jid", "", { httpOnly: true, path: "/", sameSite: "strict", secure: appCfg.HTTPS_ENABLED }); + return { message: "Successfully logged out" }; } }); @@ -63,42 +63,14 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => { schema: { response: { 200: z.object({ - token: z.string() + token: z.string(), + organizationId: z.string().optional() }) } }, handler: async (req) => { - const refreshToken = req.cookies.jid; + const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid); const appCfg = getConfig(); - if (!refreshToken) - throw new NotFoundError({ - name: "AuthTokenNotFound", - message: "Failed to find refresh token" - }); - - const decodedToken = jwt.verify(refreshToken, appCfg.AUTH_SECRET) as AuthModeRefreshJwtTokenPayload; - if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN) - throw new UnauthorizedError({ - message: "The token provided is not a refresh token", - name: "InvalidToken" - }); - - const tokenVersion = await server.services.authToken.getUserTokenSessionById( - decodedToken.tokenVersionId, - decodedToken.userId - ); - if (!tokenVersion) - throw new UnauthorizedError({ - message: "Valid token version not found", - name: "InvalidToken" - }); - - if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) { - throw new UnauthorizedError({ - message: "Token version mismatch", - name: "InvalidToken" - }); - } const token = jwt.sign( { @@ -108,13 +80,14 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => { tokenVersionId: tokenVersion.id, accessVersion: tokenVersion.accessVersion, organizationId: decodedToken.organizationId, - isMfaVerified: decodedToken.isMfaVerified + isMfaVerified: decodedToken.isMfaVerified, + mfaMethod: decodedToken.mfaMethod }, appCfg.AUTH_SECRET, { expiresIn: appCfg.JWT_AUTH_LIFETIME } ); - return { token }; + return { token, organizationId: decodedToken.organizationId }; } }); }; diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts index 88ec8500e..f6538b797 100644 --- a/backend/src/server/routes/v1/certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-router.ts @@ -1,11 +1,12 @@ /* eslint-disable @typescript-eslint/no-floating-promises */ -import ms from "ms"; import { z } from "zod"; import { CertificateAuthoritiesSchema, CertificateTemplatesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs"; +import { ApiDocsTags, CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs"; +import { ms } from "@app/lib/ms"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; @@ -14,6 +15,7 @@ import { validateAltNamesField, validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators"; +import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; export const registerCaRouter = async (server: FastifyZodProvider) => { server.route({ @@ -24,6 +26,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Create CA", body: z .object({ @@ -103,6 +107,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Get CA", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET.caId) @@ -149,6 +155,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Get DER-encoded certificate of CA", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT_BY_ID.caId), @@ -175,6 +183,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Update CA", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.UPDATE.caId) @@ -229,6 +239,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Delete CA", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.DELETE.caId) @@ -274,6 +286,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Get CA CSR", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CSR.caId) @@ -319,6 +333,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Perform CA certificate renewal", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.caId) @@ -374,6 +390,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Get list of past and current CA certificates for a CA", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.caId) @@ -422,6 +440,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Get current CA cert and cert chain of a CA", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT.caId) @@ -471,6 +491,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Create intermediate CA certificate from parent CA", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.caId) @@ -534,6 +556,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Import certificate and chain to CA", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.IMPORT_CERT.caId) @@ -586,6 +610,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Issue certificate from CA", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.caId) @@ -649,6 +675,16 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { } }); + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.IssueCert, + distinctId: getTelemetryDistinctId(req), + properties: { + caId: ca.id, + commonName: req.body.commonName, + ...req.auditLogInfo + } + }); + return { certificate, certificateChain, @@ -667,6 +703,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Sign certificate from CA", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.caId) @@ -707,7 +745,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } = + const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca, commonName } = await server.services.certificateAuthority.signCertFromCa({ isInternal: false, caId: req.params.caId, @@ -731,6 +769,16 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { } }); + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SignCert, + distinctId: getTelemetryDistinctId(req), + properties: { + caId: ca.id, + commonName, + ...req.auditLogInfo + } + }); + return { certificate: certificate.toString("pem"), certificateChain, @@ -748,6 +796,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Get list of certificate templates for the CA", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.caId) @@ -793,6 +843,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Get list of CRLs of the CA", params: z.object({ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CRLS.caId) diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts index 99d57e802..ea33e948f 100644 --- a/backend/src/server/routes/v1/certificate-router.ts +++ b/backend/src/server/routes/v1/certificate-router.ts @@ -1,10 +1,11 @@ -import ms from "ms"; import { z } from "zod"; import { CertificatesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs"; +import { ApiDocsTags, CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs"; +import { ms } from "@app/lib/ms"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { CertExtendedKeyUsage, CertKeyUsage, CrlReason } from "@app/services/certificate/certificate-types"; @@ -12,6 +13,7 @@ import { validateAltNamesField, validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators"; +import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; export const registerCertRouter = async (server: FastifyZodProvider) => { server.route({ @@ -22,6 +24,8 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], description: "Get certificate", params: z.object({ serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber) @@ -68,6 +72,8 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], description: "Issue certificate", body: z .object({ @@ -150,6 +156,17 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { } }); + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.IssueCert, + distinctId: getTelemetryDistinctId(req), + properties: { + caId: req.body.caId, + certificateTemplateId: req.body.certificateTemplateId, + commonName: req.body.commonName, + ...req.auditLogInfo + } + }); + return { certificate, certificateChain, @@ -168,6 +185,8 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], description: "Sign certificate", body: z .object({ @@ -228,7 +247,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } = + const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca, commonName } = await server.services.certificateAuthority.signCertFromCa({ isInternal: false, actor: req.permission.type, @@ -251,6 +270,17 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { } }); + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SignCert, + distinctId: getTelemetryDistinctId(req), + properties: { + caId: req.body.caId, + certificateTemplateId: req.body.certificateTemplateId, + commonName, + ...req.auditLogInfo + } + }); + return { certificate: certificate.toString("pem"), certificateChain, @@ -268,6 +298,8 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], description: "Revoke", params: z.object({ serialNumber: z.string().trim().describe(CERTIFICATES.REVOKE.serialNumber) @@ -322,6 +354,8 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], description: "Delete certificate", params: z.object({ serialNumber: z.string().trim().describe(CERTIFICATES.DELETE.serialNumber) @@ -368,6 +402,8 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], description: "Get certificate body of certificate", params: z.object({ serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumber) diff --git a/backend/src/server/routes/v1/certificate-template-router.ts b/backend/src/server/routes/v1/certificate-template-router.ts index 54ce571a2..b0c186206 100644 --- a/backend/src/server/routes/v1/certificate-template-router.ts +++ b/backend/src/server/routes/v1/certificate-template-router.ts @@ -1,9 +1,9 @@ -import ms from "ms"; import { z } from "zod"; import { CertificateTemplateEstConfigsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { CERTIFICATE_TEMPLATES } from "@app/lib/api-docs"; +import { ApiDocsTags, CERTIFICATE_TEMPLATES } from "@app/lib/api-docs"; +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"; @@ -14,7 +14,8 @@ import { validateTemplateRegexField } from "@app/services/certificate-template/c const sanitizedEstConfig = CertificateTemplateEstConfigsSchema.pick({ id: true, certificateTemplateId: true, - isEnabled: true + isEnabled: true, + disableBootstrapCertValidation: true }); export const registerCertificateTemplateRouter = async (server: FastifyZodProvider) => { @@ -25,6 +26,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateTemplates], params: z.object({ certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.GET.certificateTemplateId) }), @@ -64,6 +67,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateTemplates], body: z.object({ caId: z.string().describe(CERTIFICATE_TEMPLATES.CREATE.caId), pkiCollectionId: z.string().optional().describe(CERTIFICATE_TEMPLATES.CREATE.pkiCollectionId), @@ -131,6 +136,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateTemplates], body: z.object({ caId: z.string().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.caId), pkiCollectionId: z.string().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.pkiCollectionId), @@ -197,6 +204,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateTemplates], params: z.object({ certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.DELETE.certificateTemplateId) }), @@ -237,15 +246,24 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateTemplates], description: "Create Certificate Template EST configuration", params: z.object({ certificateTemplateId: z.string().trim() }), - body: z.object({ - caChain: z.string().trim().min(1), - passphrase: z.string().min(1), - isEnabled: z.boolean().default(true) - }), + body: z + .object({ + caChain: z.string().trim().optional(), + passphrase: z.string().min(1), + isEnabled: z.boolean().default(true), + disableBootstrapCertValidation: z.boolean().default(false) + }) + .refine( + ({ caChain, disableBootstrapCertValidation }) => + disableBootstrapCertValidation || (!disableBootstrapCertValidation && caChain), + "CA chain is required" + ), response: { 200: sanitizedEstConfig } @@ -284,13 +302,16 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateTemplates], description: "Update Certificate Template EST configuration", params: z.object({ certificateTemplateId: z.string().trim() }), body: z.object({ - caChain: z.string().trim().min(1).optional(), + caChain: z.string().trim().optional(), passphrase: z.string().min(1).optional(), + disableBootstrapCertValidation: z.boolean().optional(), isEnabled: z.boolean().optional() }), response: { @@ -331,6 +352,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateTemplates], description: "Get Certificate Template EST configuration", params: z.object({ certificateTemplateId: z.string().trim() diff --git a/backend/src/server/routes/v1/cmek-router.ts b/backend/src/server/routes/v1/cmek-router.ts index 18d13e67f..c8fd485a7 100644 --- a/backend/src/server/routes/v1/cmek-router.ts +++ b/backend/src/server/routes/v1/cmek-router.ts @@ -1,28 +1,26 @@ -import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { InternalKmsSchema, KmsKeysSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { KMS } from "@app/lib/api-docs"; +import { ApiDocsTags, KMS } from "@app/lib/api-docs"; import { getBase64SizeInBytes, isBase64 } from "@app/lib/base64"; -import { SymmetricEncryption } from "@app/lib/crypto/cipher"; +import { AllowedEncryptionKeyAlgorithms, SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher"; +import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign"; import { OrderByDirection } from "@app/lib/types"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; -import { CmekOrderBy } from "@app/services/cmek/cmek-types"; +import { CmekOrderBy, TCmekKeyEncryptionAlgorithm } from "@app/services/cmek/cmek-types"; +import { KmsKeyUsage } from "@app/services/kms/kms-types"; -const keyNameSchema = z - .string() - .trim() - .min(1) - .max(32) - .toLowerCase() - .refine((v) => slugify(v) === v, { - message: "Name must be slug friendly" - }); +const keyNameSchema = slugSchema({ min: 1, max: 32, field: "Name" }); const keyDescriptionSchema = z.string().trim().max(500).optional(); +const CmekSchema = KmsKeysSchema.merge(InternalKmsSchema.pick({ version: true, encryptionAlgorithm: true })).omit({ + isReserved: true +}); + const base64Schema = z.string().superRefine((val, ctx) => { if (!isBase64(val)) { ctx.addIssue({ @@ -48,32 +46,71 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.KmsKeys], description: "Create KMS key", - body: z.object({ - projectId: z.string().describe(KMS.CREATE_KEY.projectId), - name: keyNameSchema.describe(KMS.CREATE_KEY.name), - description: keyDescriptionSchema.describe(KMS.CREATE_KEY.description), - encryptionAlgorithm: z - .nativeEnum(SymmetricEncryption) - .optional() - .default(SymmetricEncryption.AES_GCM_256) - .describe(KMS.CREATE_KEY.encryptionAlgorithm) // eventually will support others - }), + body: z + .object({ + projectId: z.string().describe(KMS.CREATE_KEY.projectId), + name: keyNameSchema.describe(KMS.CREATE_KEY.name), + description: keyDescriptionSchema.describe(KMS.CREATE_KEY.description), + keyUsage: z + .nativeEnum(KmsKeyUsage) + .optional() + .default(KmsKeyUsage.ENCRYPT_DECRYPT) + .describe(KMS.CREATE_KEY.type), + encryptionAlgorithm: z + .enum(AllowedEncryptionKeyAlgorithms) + .optional() + .default(SymmetricKeyAlgorithm.AES_GCM_256) + .describe(KMS.CREATE_KEY.encryptionAlgorithm) + }) + .superRefine((data, ctx) => { + if ( + data.keyUsage === KmsKeyUsage.ENCRYPT_DECRYPT && + !Object.values(SymmetricKeyAlgorithm).includes(data.encryptionAlgorithm as SymmetricKeyAlgorithm) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `encryptionAlgorithm must be a valid symmetric encryption algorithm. Valid options are: ${Object.values( + SymmetricKeyAlgorithm + ).join(", ")}` + }); + } + if ( + data.keyUsage === KmsKeyUsage.SIGN_VERIFY && + !Object.values(AsymmetricKeyAlgorithm).includes(data.encryptionAlgorithm as AsymmetricKeyAlgorithm) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `encryptionAlgorithm must be a valid asymmetric sign-verify algorithm. Valid options are: ${Object.values( + AsymmetricKeyAlgorithm + ).join(", ")}` + }); + } + }), response: { 200: z.object({ - key: KmsKeysSchema + key: CmekSchema }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const { - body: { projectId, name, description, encryptionAlgorithm }, + body: { projectId, name, description, encryptionAlgorithm, keyUsage }, permission } = req; const cmek = await server.services.cmek.createCmek( - { orgId: permission.orgId, projectId, name, description, encryptionAlgorithm }, + { + orgId: permission.orgId, + projectId, + name, + description, + encryptionAlgorithm: encryptionAlgorithm as TCmekKeyEncryptionAlgorithm, + keyUsage + }, permission ); @@ -86,7 +123,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { keyId: cmek.id, name, description, - encryptionAlgorithm + encryptionAlgorithm: encryptionAlgorithm as TCmekKeyEncryptionAlgorithm } } }); @@ -103,6 +140,8 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.KmsKeys], description: "Update KMS key", params: z.object({ keyId: z.string().uuid().describe(KMS.UPDATE_KEY.keyId) @@ -114,7 +153,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - key: KmsKeysSchema + key: CmekSchema }) } }, @@ -130,7 +169,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: permission.orgId, + projectId: cmek.projectId!, event: { type: EventType.UPDATE_CMEK, metadata: { @@ -152,13 +191,15 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.KmsKeys], description: "Delete KMS key", params: z.object({ keyId: z.string().uuid().describe(KMS.DELETE_KEY.keyId) }), response: { 200: z.object({ - key: KmsKeysSchema + key: CmekSchema }) } }, @@ -173,7 +214,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: permission.orgId, + projectId: cmek.projectId!, event: { type: EventType.DELETE_CMEK, metadata: { @@ -194,6 +235,8 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.KmsKeys], description: "List KMS keys", querystring: z.object({ projectId: z.string().describe(KMS.LIST_KEYS.projectId), @@ -209,7 +252,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - keys: KmsKeysSchema.merge(InternalKmsSchema.pick({ version: true, encryptionAlgorithm: true })).array(), + keys: CmekSchema.array(), totalCount: z.number() }) } @@ -238,6 +281,96 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/keys/:keyId", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.KmsKeys], + description: "Get KMS key by ID", + params: z.object({ + keyId: z.string().uuid().describe(KMS.GET_KEY_BY_ID.keyId) + }), + response: { + 200: z.object({ + key: CmekSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { + params: { keyId }, + permission + } = req; + + const key = await server.services.cmek.findCmekById(keyId, permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: key.projectId!, + event: { + type: EventType.GET_CMEK, + metadata: { + keyId: key.id + } + } + }); + + return { key }; + } + }); + + server.route({ + method: "GET", + url: "/keys/key-name/:keyName", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.KmsKeys], + description: "Get KMS key by name", + params: z.object({ + keyName: slugSchema({ field: "Key name" }).describe(KMS.GET_KEY_BY_NAME.keyName) + }), + querystring: z.object({ + projectId: z.string().min(1, "Project ID is required").describe(KMS.GET_KEY_BY_NAME.projectId) + }), + response: { + 200: z.object({ + key: CmekSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { + params: { keyName }, + query: { projectId }, + permission + } = req; + + const key = await server.services.cmek.findCmekByName(keyName, projectId, permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: key.projectId!, + event: { + type: EventType.GET_CMEK, + metadata: { + keyId: key.id + } + } + }); + + return { key }; + } + }); + // encrypt data server.route({ method: "POST", @@ -246,6 +379,8 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.KmsEncryption], description: "Encrypt data with KMS key", params: z.object({ keyId: z.string().uuid().describe(KMS.ENCRYPT.keyId) @@ -267,11 +402,11 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { permission } = req; - const ciphertext = await server.services.cmek.cmekEncrypt({ keyId, plaintext }, permission); + const { ciphertext, projectId } = await server.services.cmek.cmekEncrypt({ keyId, plaintext }, permission); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: permission.orgId, + projectId, event: { type: EventType.CMEK_ENCRYPT, metadata: { @@ -284,6 +419,206 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/keys/:keyId/public-key", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.KmsSigning], + description: + "Get the public key for a KMS key that is used for signing and verifying data. This endpoint is only available for asymmetric keys.", + params: z.object({ + keyId: z.string().uuid().describe(KMS.GET_PUBLIC_KEY.keyId) + }), + response: { + 200: z.object({ + publicKey: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { + params: { keyId }, + permission + } = req; + + const { publicKey, projectId } = await server.services.cmek.getPublicKey({ keyId }, permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.CMEK_GET_PUBLIC_KEY, + metadata: { + keyId + } + } + }); + + return { publicKey }; + } + }); + + server.route({ + method: "GET", + url: "/keys/:keyId/signing-algorithms", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.KmsSigning], + description: "List all available signing algorithms for a KMS key", + params: z.object({ + keyId: z.string().uuid().describe(KMS.LIST_SIGNING_ALGORITHMS.keyId) + }), + response: { + 200: z.object({ + signingAlgorithms: z.array(z.nativeEnum(SigningAlgorithm)) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { keyId } = req.params; + + const { signingAlgorithms, projectId } = await server.services.cmek.listSigningAlgorithms( + { keyId }, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.CMEK_LIST_SIGNING_ALGORITHMS, + metadata: { + keyId + } + } + }); + + return { signingAlgorithms }; + } + }); + + server.route({ + method: "POST", + url: "/keys/:keyId/sign", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.KmsSigning], + description: "Sign data with a KMS key.", + params: z.object({ + keyId: z.string().uuid().describe(KMS.SIGN.keyId) + }), + body: z.object({ + signingAlgorithm: z.nativeEnum(SigningAlgorithm), + isDigest: z.boolean().optional().default(false).describe(KMS.SIGN.isDigest), + data: base64Schema.describe(KMS.SIGN.data) + }), + response: { + 200: z.object({ + signature: z.string(), + keyId: z.string().uuid(), + signingAlgorithm: z.nativeEnum(SigningAlgorithm) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { + params: { keyId: inputKeyId }, + body: { data, signingAlgorithm, isDigest }, + permission + } = req; + + const { projectId, ...result } = await server.services.cmek.cmekSign( + { keyId: inputKeyId, data, signingAlgorithm, isDigest }, + permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.CMEK_SIGN, + metadata: { + keyId: inputKeyId, + signingAlgorithm, + signature: result.signature + } + } + }); + return result; + } + }); + + server.route({ + method: "POST", + url: "/keys/:keyId/verify", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.KmsSigning], + description: "Verify data signatures with a KMS key.", + params: z.object({ + keyId: z.string().uuid().describe(KMS.VERIFY.keyId) + }), + body: z.object({ + isDigest: z.boolean().optional().default(false).describe(KMS.VERIFY.isDigest), + data: base64Schema.describe(KMS.VERIFY.data), + signature: base64Schema.describe(KMS.VERIFY.signature), + signingAlgorithm: z.nativeEnum(SigningAlgorithm) + }), + response: { + 200: z.object({ + signatureValid: z.boolean(), + keyId: z.string().uuid(), + signingAlgorithm: z.nativeEnum(SigningAlgorithm) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { + params: { keyId }, + body: { data, signature, signingAlgorithm, isDigest }, + permission + } = req; + + const { projectId, ...result } = await server.services.cmek.cmekVerify( + { keyId, data, signature, signingAlgorithm, isDigest }, + permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.CMEK_VERIFY, + metadata: { + keyId, + signatureValid: result.signatureValid, + signingAlgorithm, + signature + } + } + }); + + return result; + } + }); + server.route({ method: "POST", url: "/keys/:keyId/decrypt", @@ -291,6 +626,8 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.KmsEncryption], description: "Decrypt data with KMS key", params: z.object({ keyId: z.string().uuid().describe(KMS.DECRYPT.keyId) @@ -312,11 +649,11 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => { permission } = req; - const plaintext = await server.services.cmek.cmekDecrypt({ keyId, ciphertext }, permission); + const { plaintext, projectId } = await server.services.cmek.cmekDecrypt({ keyId, ciphertext }, permission); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: permission.orgId, + projectId, event: { type: EventType.CMEK_DECRYPT, metadata: { diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index 8213cf666..5a52d4748 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -1,12 +1,10 @@ -import { ForbiddenError, subject } from "@casl/ability"; +import { ForbiddenError } from "@casl/ability"; import { z } from "zod"; -import { SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas"; +import { SecretFoldersSchema, SecretImportsSchema } from "@app/db/schemas"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; -import { - ProjectPermissionDynamicSecretActions, - ProjectPermissionSub -} from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; +import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema"; import { DASHBOARD } from "@app/lib/api-docs"; import { BadRequestError } from "@app/lib/errors"; import { removeTrailingSlash } from "@app/lib/fn"; @@ -15,27 +13,19 @@ import { secretsLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getUserAgentType } from "@app/server/plugins/audit-log"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { SanitizedDynamicSecretSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; +import { + booleanSchema, + SanitizedDynamicSecretSchema, + SanitizedTagSchema, + secretRawSchema +} from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; +import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; const MAX_DEEP_SEARCH_LIMIT = 500; // arbitrary limit to prevent excessive results -// handle querystring boolean values -const booleanSchema = z - .union([z.boolean(), z.string().trim()]) - .transform((value) => { - if (typeof value === "string") { - // ie if not empty, 0 or false, return true - return Boolean(value) && Number(value) !== 0 && value.toLowerCase() !== "false"; - } - - return value; - }) - .optional() - .default(true); - const parseSecretPathSearch = (search?: string) => { if (!search) return { @@ -107,21 +97,70 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { search: z.string().trim().describe(DASHBOARD.SECRET_OVERVIEW_LIST.search).optional(), includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeSecrets), includeFolders: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeFolders), + includeImports: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeImports), + includeSecretRotations: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeSecretRotations), includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeDynamicSecrets) }), response: { 200: z.object({ folders: SecretFoldersSchema.extend({ environment: z.string() }).array().optional(), dynamicSecrets: SanitizedDynamicSecretSchema.extend({ environment: z.string() }).array().optional(), + secretRotations: z + .intersection( + SecretRotationV2Schema, + z.object({ + secrets: secretRawSchema + .extend({ + secretValueHidden: z.boolean(), + secretPath: z.string().optional(), + secretMetadata: ResourceMetadataSchema.optional(), + tags: SanitizedTagSchema.array().optional() + }) + .nullable() + .array() + }) + ) + .array() + .optional(), secrets: secretRawSchema .extend({ + secretValueHidden: z.boolean(), secretPath: z.string().optional(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) + secretMetadata: ResourceMetadataSchema.optional(), + tags: SanitizedTagSchema.array().optional() + }) + .array() + .optional(), + imports: SecretImportsSchema.omit({ importEnv: true }) + .extend({ + importEnv: z.object({ name: z.string(), slug: z.string(), id: z.string() }), + environment: z.string() + }) + .array() + .optional(), + importedByEnvs: z + .object({ + environment: z.string(), + importedBy: z + .object({ + environment: z.object({ + name: z.string(), + slug: z.string() + }), + folders: z + .object({ + name: z.string(), + isImported: z.boolean(), + secrets: z + .object({ + secretId: z.string(), + referencedSecretKey: z.string() + }) + .array() + .optional() + }) + .array() + }) .array() .optional() }) @@ -130,6 +169,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { totalFolderCount: z.number().optional(), totalDynamicSecretCount: z.number().optional(), totalSecretCount: z.number().optional(), + totalImportCount: z.number().optional(), + totalSecretRotationCount: z.number().optional(), totalCount: z.number() }) } @@ -146,7 +187,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { orderDirection, includeFolders, includeSecrets, - includeDynamicSecrets + includeImports, + includeDynamicSecrets, + includeSecretRotations } = req.query; const environments = req.query.environments.split(","); @@ -162,15 +205,67 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { let remainingLimit = limit; let adjustedOffset = offset; + let imports: Awaited> | undefined; let folders: Awaited> | undefined; let secrets: Awaited> | undefined; let dynamicSecrets: | Awaited> | undefined; + let secretRotations: + | Awaited> + | undefined; let totalFolderCount: number | undefined; let totalDynamicSecretCount: number | undefined; let totalSecretCount: number | undefined; + let totalImportCount: number | undefined; + let totalSecretRotationCount: number | undefined; + + if (includeImports) { + totalImportCount = await server.services.secretImport.getProjectImportMultiEnvCount({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId, + environments, + path: secretPath, + search + }); + + if (remainingLimit > 0 && totalImportCount > adjustedOffset) { + imports = await server.services.secretImport.getImportsMultiEnv({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId, + environments, + path: secretPath, + search, + limit: remainingLimit, + offset: adjustedOffset + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.query.projectId, + event: { + type: EventType.GET_SECRET_IMPORTS, + metadata: { + environment: environments.join(","), + folderId: imports?.[0]?.folderId, + numberOfImports: imports.length + } + } + }); + + remainingLimit -= imports.length; + adjustedOffset = 0; + } else { + adjustedOffset = Math.max(0, adjustedOffset - totalImportCount); + } + } if (includeFolders) { // this is the unique count, ie duplicate folders across envs only count as 1 @@ -218,23 +313,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { totalCount: totalFolderCount ?? 0 }; - const { permission } = await server.services.permission.getProjectPermission( - req.permission.type, - req.permission.id, - projectId, - req.permission.authMethod, - req.permission.orgId - ); - - const allowedDynamicSecretEnvironments = // filter envs user has access to - environments.filter((environment) => - permission.can( - ProjectPermissionDynamicSecretActions.Lease, - subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath }) - ) - ); - - if (includeDynamicSecrets && allowedDynamicSecretEnvironments.length) { + if (includeDynamicSecrets) { // this is the unique count, ie duplicate secrets across envs only count as 1 totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({ actor: req.permission.type, @@ -243,7 +322,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { actorOrgId: req.permission.orgId, projectId, search, - environmentSlugs: allowedDynamicSecretEnvironments, + environmentSlugs: environments, path: secretPath, isInternal: true }); @@ -258,7 +337,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { search, orderBy, orderDirection, - environmentSlugs: allowedDynamicSecretEnvironments, + environmentSlugs: environments, path: secretPath, limit: remainingLimit, offset: adjustedOffset, @@ -275,6 +354,56 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { } } + if (includeSecretRotations) { + totalSecretRotationCount = await server.services.secretRotationV2.getDashboardSecretRotationCount( + { + projectId, + search, + environments, + secretPath + }, + req.permission + ); + + if (remainingLimit > 0 && totalSecretRotationCount > adjustedOffset) { + secretRotations = await server.services.secretRotationV2.getDashboardSecretRotations( + { + projectId, + search, + orderBy, + orderDirection, + environments, + secretPath, + limit: remainingLimit, + offset: adjustedOffset + }, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + projectId, + ...req.auditLogInfo, + event: { + type: EventType.GET_SECRET_ROTATIONS, + metadata: { + count: secretRotations.length, + rotationIds: secretRotations.map((rotation) => rotation.id), + secretPath, + environment: environments.join(",") + } + } + }); + + // get the count of unique secret rotation names to properly adjust remaining limit + const uniqueSecretRotationCount = new Set(secretRotations.map((rotation) => rotation.name)).size; + + remainingLimit -= uniqueSecretRotationCount; + adjustedOffset = 0; + } else { + adjustedOffset = Math.max(0, adjustedOffset - totalSecretRotationCount); + } + } + if (includeSecrets) { // this is the unique count, ie duplicate secrets across envs only count as 1 totalSecretCount = await server.services.secret.getSecretsCountMultiEnv({ @@ -291,6 +420,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { secrets = await server.services.secret.getSecretsRawMultiEnv({ + viewSecretValue: true, actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, @@ -305,51 +435,89 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { offset: adjustedOffset, isInternal: true }); + } + } - for await (const environment of environments) { - const secretCountFromEnv = secrets.filter((secret) => secret.environment === environment).length; + if (secrets?.length || secretRotations?.length) { + for await (const environment of environments) { + const secretCountFromEnv = + (secrets?.filter((secret) => secret.environment === environment).length ?? 0) + + (secretRotations + ?.filter((rotation) => rotation.environment.slug === environment) + .flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))).length ?? 0); - if (secretCountFromEnv) { - await server.services.auditLog.createAuditLog({ - projectId, - ...req.auditLogInfo, - event: { - type: EventType.GET_SECRETS, - metadata: { - environment, - secretPath, - numberOfSecrets: secretCountFromEnv - } + if (secretCountFromEnv) { + await server.services.auditLog.createAuditLog({ + projectId, + ...req.auditLogInfo, + event: { + type: EventType.GET_SECRETS, + metadata: { + environment, + secretPath, + numberOfSecrets: secretCountFromEnv + } + } + }); + + if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) { + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SecretPulled, + distinctId: getTelemetryDistinctId(req), + properties: { + numberOfSecrets: secretCountFromEnv, + workspaceId: projectId, + environment, + secretPath, + channel: getUserAgentType(req.headers["user-agent"]), + ...req.auditLogInfo } }); - - if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) { - await server.services.telemetry.sendPostHogEvents({ - event: PostHogEventTypes.SecretPulled, - distinctId: getTelemetryDistinctId(req), - properties: { - numberOfSecrets: secretCountFromEnv, - workspaceId: projectId, - environment, - secretPath, - channel: getUserAgentType(req.headers["user-agent"]), - ...req.auditLogInfo - } - }); - } } } } } + const importedByEnvs = []; + + for await (const environment of environments) { + const importedBy = await server.services.secretImport.getFolderIsImportedBy({ + path: secretPath, + environment, + projectId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + secrets: secrets?.filter((s) => s.environment === environment) + }); + + if (importedBy) { + importedByEnvs.push({ + environment, + importedBy + }); + } + } + return { folders, dynamicSecrets, secrets, + imports, + secretRotations, totalFolderCount, totalDynamicSecretCount, + totalImportCount, totalSecretCount, - totalCount: (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0) + totalSecretRotationCount, + importedByEnvs, + totalCount: + (totalFolderCount ?? 0) + + (totalDynamicSecretCount ?? 0) + + (totalSecretCount ?? 0) + + (totalImportCount ?? 0) + + (totalSecretRotationCount ?? 0) }; } }); @@ -390,10 +558,12 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { .optional(), search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(), tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(), + viewSecretValue: booleanSchema.default(true), includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets), includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders), includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets), - includeImports: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeImports) + includeImports: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeImports), + includeSecretRotations: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecretRotations) }), response: { 200: z.object({ @@ -405,17 +575,29 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { .optional(), folders: SecretFoldersSchema.array().optional(), dynamicSecrets: SanitizedDynamicSecretSchema.array().optional(), + secretRotations: z + .intersection( + SecretRotationV2Schema, + z.object({ + secrets: secretRawSchema + .extend({ + secretValueHidden: z.boolean(), + secretPath: z.string().optional(), + secretMetadata: ResourceMetadataSchema.optional(), + tags: SanitizedTagSchema.array().optional() + }) + .nullable() + .array() + }) + ) + .array() + .optional(), secrets: secretRawSchema .extend({ + secretValueHidden: z.boolean(), secretPath: z.string().optional(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional() + secretMetadata: ResourceMetadataSchema.optional(), + tags: SanitizedTagSchema.array().optional() }) .array() .optional(), @@ -423,6 +605,29 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { totalFolderCount: z.number().optional(), totalDynamicSecretCount: z.number().optional(), totalSecretCount: z.number().optional(), + importedBy: z + .object({ + environment: z.object({ + name: z.string(), + slug: z.string() + }), + folders: z + .object({ + name: z.string(), + isImported: z.boolean(), + secrets: z + .object({ + secretId: z.string(), + referencedSecretKey: z.string() + }) + .array() + .optional() + }) + .array() + }) + .array() + .optional(), + totalSecretRotationCount: z.number().optional(), totalCount: z.number() }) } @@ -441,7 +646,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { includeFolders, includeSecrets, includeDynamicSecrets, - includeImports + includeImports, + includeSecretRotations } = req.query; if (!projectId || !environment) throw new BadRequestError({ message: "Missing workspace id or environment" }); @@ -460,11 +666,15 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { let folders: Awaited> | undefined; let secrets: Awaited>["secrets"] | undefined; let dynamicSecrets: Awaited> | undefined; + let secretRotations: + | Awaited> + | undefined; let totalImportCount: number | undefined; let totalFolderCount: number | undefined; let totalDynamicSecretCount: number | undefined; let totalSecretCount: number | undefined; + let totalSecretRotationCount: number | undefined; if (includeImports) { totalImportCount = await server.services.secretImport.getProjectImportCount({ @@ -547,6 +757,53 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { } } + if (includeSecretRotations) { + totalSecretRotationCount = await server.services.secretRotationV2.getDashboardSecretRotationCount( + { + projectId, + search, + environments: [environment], + secretPath + }, + req.permission + ); + + if (remainingLimit > 0 && totalSecretRotationCount > adjustedOffset) { + secretRotations = await server.services.secretRotationV2.getDashboardSecretRotations( + { + projectId, + search, + orderBy, + orderDirection, + environments: [environment], + secretPath, + limit: remainingLimit, + offset: adjustedOffset + }, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + projectId, + ...req.auditLogInfo, + event: { + type: EventType.GET_SECRET_ROTATIONS, + metadata: { + count: secretRotations.length, + rotationIds: secretRotations.map((rotation) => rotation.id), + secretPath, + environment + } + } + }); + + remainingLimit -= secretRotations.length; + adjustedOffset = 0; + } else { + adjustedOffset = Math.max(0, adjustedOffset - totalSecretRotationCount); + } + } + try { if (includeDynamicSecrets) { totalDynamicSecretCount = await server.services.dynamicSecret.getDynamicSecretCount({ @@ -582,7 +839,13 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { adjustedOffset = Math.max(0, adjustedOffset - totalDynamicSecretCount); } } + } catch (error) { + if (!(error instanceof ForbiddenError)) { + throw error; + } + } + try { if (includeSecrets) { totalSecretCount = await server.services.secret.getSecretsCount({ actorId: req.permission.id, @@ -597,51 +860,25 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { }); if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { - const secretsRaw = await server.services.secret.getSecretsRaw({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - environment, - actorAuthMethod: req.permission.authMethod, - projectId, - path: secretPath, - orderBy, - orderDirection, - search, - limit: remainingLimit, - offset: adjustedOffset, - tagSlugs: tags - }); - - secrets = secretsRaw.secrets; - - await server.services.auditLog.createAuditLog({ - projectId, - ...req.auditLogInfo, - event: { - type: EventType.GET_SECRETS, - metadata: { - environment, - secretPath, - numberOfSecrets: secrets.length - } - } - }); - - if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) { - await server.services.telemetry.sendPostHogEvents({ - event: PostHogEventTypes.SecretPulled, - distinctId: getTelemetryDistinctId(req), - properties: { - numberOfSecrets: secrets.length, - workspaceId: projectId, - environment, - secretPath, - channel: getUserAgentType(req.headers["user-agent"]), - ...req.auditLogInfo - } - }); - } + secrets = ( + await server.services.secret.getSecretsRaw({ + actorId: req.permission.id, + actor: req.permission.type, + viewSecretValue: req.query.viewSecretValue, + throwOnMissingReadValuePermission: false, + actorOrgId: req.permission.orgId, + environment, + actorAuthMethod: req.permission.authMethod, + projectId, + path: secretPath, + orderBy, + orderDirection, + search, + limit: remainingLimit, + offset: adjustedOffset, + tagSlugs: tags + }) + ).secrets; } } } catch (error) { @@ -650,17 +887,69 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { } } + const importedBy = await server.services.secretImport.getFolderIsImportedBy({ + path: secretPath, + environment, + projectId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + secrets + }); + + if (secrets?.length || secretRotations?.length) { + const secretCount = + (secrets?.length ?? 0) + + (secretRotations?.flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))).length ?? 0); + + await server.services.auditLog.createAuditLog({ + projectId, + ...req.auditLogInfo, + event: { + type: EventType.GET_SECRETS, + metadata: { + environment, + secretPath, + numberOfSecrets: secretCount + } + } + }); + + if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) { + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SecretPulled, + distinctId: getTelemetryDistinctId(req), + properties: { + numberOfSecrets: secretCount, + workspaceId: projectId, + environment, + secretPath, + channel: getUserAgentType(req.headers["user-agent"]), + ...req.auditLogInfo + } + }); + } + } + return { imports, folders, dynamicSecrets, secrets, + secretRotations, totalImportCount, totalFolderCount, totalDynamicSecretCount, totalSecretCount, + totalSecretRotationCount, + importedBy, totalCount: - (totalImportCount ?? 0) + (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0) + (totalImportCount ?? 0) + + (totalFolderCount ?? 0) + + (totalDynamicSecretCount ?? 0) + + (totalSecretCount ?? 0) + + (totalSecretRotationCount ?? 0) }; } }); @@ -692,18 +981,14 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { .optional(), secrets: secretRawSchema .extend({ + secretValueHidden: z.boolean(), secretPath: z.string().optional(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional() + secretMetadata: ResourceMetadataSchema.optional(), + tags: SanitizedTagSchema.array().optional() }) .array() - .optional() + .optional(), + secretRotations: SecretRotationV2Schema.array().optional() }) } }, @@ -744,6 +1029,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { const secrets = await server.services.secret.getSecretsRawByFolderMappings( { + filterByAction: ProjectPermissionSecretActions.DescribeSecret, projectId, folderMappings, filters: { @@ -766,6 +1052,17 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { req.permission ); + const secretRotations = searchHasTags + ? [] + : await server.services.secretRotationV2.getQuickSearchSecretRotations( + { + projectId, + folderMappings, + filters: sharedFilters + }, + req.permission + ); + for await (const environment of environments) { const secretCountForEnv = secrets.filter((secret) => secret.environment === environment).length; @@ -798,6 +1095,24 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { }); } } + + const secretRotationsFromEnv = secretRotations.filter((rotation) => rotation.environment.slug === environment); + + if (secretRotationsFromEnv.length) { + await server.services.auditLog.createAuditLog({ + projectId, + ...req.auditLogInfo, + event: { + type: EventType.GET_SECRET_ROTATIONS, + metadata: { + count: secretRotationsFromEnv.length, + rotationIds: secretRotationsFromEnv.map((rotation) => rotation.id), + secretPath, + environment + } + } + }); + } } const sliceQuickSearch = (array: T[]) => array.slice(0, 25); @@ -811,6 +1126,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { ? dynamicSecrets.filter((dynamicSecret) => dynamicSecret.path.endsWith(searchPath)) : dynamicSecrets ), + secretRotations: sliceQuickSearch( + searchPath ? secretRotations.filter((rotation) => rotation.folder.path.endsWith(searchPath)) : secretRotations + ), folders: searchHasTags ? [] : sliceQuickSearch( @@ -840,4 +1158,136 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { }; } }); + + server.route({ + method: "GET", + url: "/accessible-secrets", + config: { + rateLimit: secretsLimit + }, + schema: { + querystring: z.object({ + projectId: z.string().trim(), + environment: z.string().trim(), + secretPath: z.string().trim().default("/").transform(removeTrailingSlash), + recursive: booleanSchema.default(false), + filterByAction: z + .enum([ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue]) + .default(ProjectPermissionSecretActions.ReadValue) + }), + response: { + 200: z.object({ + secrets: secretRawSchema + .extend({ + secretPath: z.string().optional(), + secretValueHidden: z.boolean() + }) + .array() + .optional() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { projectId, environment, secretPath, filterByAction, recursive } = req.query; + + const { secrets } = await server.services.secret.getAccessibleSecrets({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + environment, + secretPath, + projectId, + filterByAction, + recursive + }); + + return { secrets }; + } + }); + + server.route({ + method: "GET", + url: "/secrets-by-keys", + config: { + rateLimit: secretsLimit + }, + schema: { + security: [ + { + bearerAuth: [] + } + ], + querystring: z.object({ + projectId: z.string().trim(), + environment: z.string().trim(), + secretPath: z.string().trim().default("/").transform(removeTrailingSlash), + keys: z.string().trim().transform(decodeURIComponent), + viewSecretValue: booleanSchema.default(false) + }), + response: { + 200: z.object({ + secrets: secretRawSchema + .extend({ + secretValueHidden: z.boolean(), + secretPath: z.string().optional(), + secretMetadata: ResourceMetadataSchema.optional(), + tags: SanitizedTagSchema.array().optional() + }) + .array() + .optional() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { secretPath, projectId, environment, viewSecretValue } = req.query; + + const keys = req.query.keys?.split(",").filter((key) => Boolean(key.trim())) ?? []; + if (!keys.length) throw new BadRequestError({ message: "One or more keys required" }); + + const { secrets } = await server.services.secret.getSecretsRaw({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + viewSecretValue, + environment, + actorAuthMethod: req.permission.authMethod, + projectId, + path: secretPath, + keys + }); + + await server.services.auditLog.createAuditLog({ + projectId, + ...req.auditLogInfo, + event: { + type: EventType.GET_SECRETS, + metadata: { + environment, + secretPath, + numberOfSecrets: secrets.length + } + } + }); + + if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) { + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SecretPulled, + distinctId: getTelemetryDistinctId(req), + properties: { + numberOfSecrets: secrets.length, + workspaceId: projectId, + environment, + secretPath, + channel: getUserAgentType(req.headers["user-agent"]), + ...req.auditLogInfo + } + }); + } + + return { secrets }; + } + }); }; diff --git a/backend/src/server/routes/v1/external-group-org-role-mapping-router.ts b/backend/src/server/routes/v1/external-group-org-role-mapping-router.ts index 032deda7d..67db5de6f 100644 --- a/backend/src/server/routes/v1/external-group-org-role-mapping-router.ts +++ b/backend/src/server/routes/v1/external-group-org-role-mapping-router.ts @@ -1,9 +1,9 @@ -import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { ExternalGroupOrgRoleMappingsSchema } from "@app/db/schemas/external-group-org-role-mappings"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -48,13 +48,7 @@ export const registerExternalGroupOrgRoleMappingRouter = async (server: FastifyZ mappings: z .object({ groupName: z.string().trim().min(1), - roleSlug: z - .string() - .min(1) - .toLowerCase() - .refine((v) => slugify(v) === v, { - message: "Role must be a valid slug" - }) + roleSlug: slugSchema({ max: 64 }) }) .array() }), diff --git a/backend/src/server/routes/v1/identity-access-token-router.ts b/backend/src/server/routes/v1/identity-access-token-router.ts index 7ed62e679..20ed8d150 100644 --- a/backend/src/server/routes/v1/identity-access-token-router.ts +++ b/backend/src/server/routes/v1/identity-access-token-router.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { UNIVERSAL_AUTH } from "@app/lib/api-docs"; +import { ApiDocsTags, UNIVERSAL_AUTH } from "@app/lib/api-docs"; import { writeLimit } from "@app/server/config/rateLimiter"; export const registerIdentityAccessTokenRouter = async (server: FastifyZodProvider) => { @@ -11,6 +11,8 @@ export const registerIdentityAccessTokenRouter = async (server: FastifyZodProvid rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.UniversalAuth], description: "Renew access token", body: z.object({ accessToken: z.string().trim().describe(UNIVERSAL_AUTH.RENEW_ACCESS_TOKEN.accessToken) @@ -44,6 +46,8 @@ export const registerIdentityAccessTokenRouter = async (server: FastifyZodProvid rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.UniversalAuth], description: "Revoke access token", body: z.object({ accessToken: z.string().trim().describe(UNIVERSAL_AUTH.REVOKE_ACCESS_TOKEN.accessToken) diff --git a/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts index 9199c21f1..effd66a68 100644 --- a/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts +++ b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { IdentityAwsAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { AWS_AUTH } from "@app/lib/api-docs"; +import { ApiDocsTags, AWS_AUTH } from "@app/lib/api-docs"; 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"; @@ -11,6 +11,7 @@ import { validateAccountIds, validatePrincipalArns } from "@app/services/identity-aws-auth/identity-aws-auth-validators"; +import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) => { server.route({ @@ -20,6 +21,8 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.AwsAuth], description: "Login with AWS Auth", body: z.object({ identityId: z.string().trim().describe(AWS_AUTH.LOGIN.identityId), @@ -70,6 +73,8 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.AwsAuth], description: "Attach AWS Auth configuration onto identity", security: [ { @@ -79,44 +84,44 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) params: z.object({ identityId: z.string().trim().describe(AWS_AUTH.ATTACH.identityId) }), - body: z.object({ - stsEndpoint: z - .string() - .trim() - .min(1) - .default("https://sts.amazonaws.com/") - .describe(AWS_AUTH.ATTACH.stsEndpoint), - allowedPrincipalArns: validatePrincipalArns.describe(AWS_AUTH.ATTACH.allowedPrincipalArns), - allowedAccountIds: validateAccountIds.describe(AWS_AUTH.ATTACH.allowedAccountIds), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) - .describe(AWS_AUTH.ATTACH.accessTokenTrustedIps), - accessTokenTTL: z - .number() - .int() - .min(1) - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenTTL must have a non zero number" - }) - .default(2592000) - .describe(AWS_AUTH.ATTACH.accessTokenTTL), - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .default(2592000) - .describe(AWS_AUTH.ATTACH.accessTokenMaxTTL), - accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(AWS_AUTH.ATTACH.accessTokenNumUsesLimit) - }), + body: z + .object({ + stsEndpoint: z + .string() + .trim() + .min(1) + .default("https://sts.amazonaws.com/") + .describe(AWS_AUTH.ATTACH.stsEndpoint), + allowedPrincipalArns: validatePrincipalArns.describe(AWS_AUTH.ATTACH.allowedPrincipalArns), + allowedAccountIds: validateAccountIds.describe(AWS_AUTH.ATTACH.allowedAccountIds), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(AWS_AUTH.ATTACH.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(AWS_AUTH.ATTACH.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .min(1) + .max(315360000) + .default(2592000) + .describe(AWS_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(AWS_AUTH.ATTACH.accessTokenNumUsesLimit) + }) + .refine( + (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityAwsAuth: IdentityAwsAuthsSchema @@ -130,7 +135,8 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, - identityId: req.params.identityId + identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth) }); await server.services.auditLog.createAuditLog({ @@ -163,6 +169,8 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.AwsAuth], description: "Update AWS Auth configuration on identity", security: [ { @@ -172,30 +180,33 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) params: z.object({ identityId: z.string().describe(AWS_AUTH.UPDATE.identityId) }), - body: z.object({ - stsEndpoint: z.string().trim().min(1).optional().describe(AWS_AUTH.UPDATE.stsEndpoint), - allowedPrincipalArns: validatePrincipalArns.describe(AWS_AUTH.UPDATE.allowedPrincipalArns), - allowedAccountIds: validateAccountIds.describe(AWS_AUTH.UPDATE.allowedAccountIds), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .optional() - .describe(AWS_AUTH.UPDATE.accessTokenTrustedIps), - accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(AWS_AUTH.UPDATE.accessTokenTTL), - accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(AWS_AUTH.UPDATE.accessTokenNumUsesLimit), - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .optional() - .describe(AWS_AUTH.UPDATE.accessTokenMaxTTL) - }), + body: z + .object({ + stsEndpoint: z.string().trim().min(1).optional().describe(AWS_AUTH.UPDATE.stsEndpoint), + allowedPrincipalArns: validatePrincipalArns.describe(AWS_AUTH.UPDATE.allowedPrincipalArns), + allowedAccountIds: validateAccountIds.describe(AWS_AUTH.UPDATE.allowedAccountIds), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .optional() + .describe(AWS_AUTH.UPDATE.accessTokenTrustedIps), + accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(AWS_AUTH.UPDATE.accessTokenTTL), + accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(AWS_AUTH.UPDATE.accessTokenNumUsesLimit), + accessTokenMaxTTL: z + .number() + .int() + .max(315360000) + .min(0) + .optional() + .describe(AWS_AUTH.UPDATE.accessTokenMaxTTL) + }) + .refine( + (val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true), + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityAwsAuth: IdentityAwsAuthsSchema @@ -242,6 +253,8 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.AwsAuth], description: "Retrieve AWS Auth configuration on identity", security: [ { @@ -288,6 +301,8 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.AwsAuth], description: "Delete AWS Auth configuration on identity", security: [ { diff --git a/backend/src/server/routes/v1/identity-azure-auth-router.ts b/backend/src/server/routes/v1/identity-azure-auth-router.ts index 6aee4504f..9053bc2e3 100644 --- a/backend/src/server/routes/v1/identity-azure-auth-router.ts +++ b/backend/src/server/routes/v1/identity-azure-auth-router.ts @@ -2,14 +2,13 @@ import { z } from "zod"; import { IdentityAzureAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { AZURE_AUTH } from "@app/lib/api-docs"; +import { ApiDocsTags, AZURE_AUTH } from "@app/lib/api-docs"; 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"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; import { validateAzureAuthField } from "@app/services/identity-azure-auth/identity-azure-auth-validators"; - -import {} from "../sanitizedSchemas"; +import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider) => { server.route({ @@ -19,6 +18,8 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.AzureAuth], description: "Login with Azure Auth", body: z.object({ identityId: z.string().trim().describe(AZURE_AUTH.LOGIN.identityId), @@ -67,6 +68,8 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.AzureAuth], description: "Attach Azure Auth configuration onto identity", security: [ { @@ -76,39 +79,44 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider params: z.object({ identityId: z.string().trim().describe(AZURE_AUTH.LOGIN.identityId) }), - body: z.object({ - tenantId: z.string().trim().describe(AZURE_AUTH.ATTACH.tenantId), - resource: z.string().trim().describe(AZURE_AUTH.ATTACH.resource), - allowedServicePrincipalIds: validateAzureAuthField.describe(AZURE_AUTH.ATTACH.allowedServicePrincipalIds), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) - .describe(AZURE_AUTH.ATTACH.accessTokenTrustedIps), - accessTokenTTL: z - .number() - .int() - .min(1) - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenTTL must have a non zero number" - }) - .default(2592000) - .describe(AZURE_AUTH.ATTACH.accessTokenTTL), - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .default(2592000) - .describe(AZURE_AUTH.ATTACH.accessTokenMaxTTL), - accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(AZURE_AUTH.ATTACH.accessTokenNumUsesLimit) - }), + body: z + .object({ + tenantId: z.string().trim().describe(AZURE_AUTH.ATTACH.tenantId), + resource: z.string().trim().describe(AZURE_AUTH.ATTACH.resource), + allowedServicePrincipalIds: validateAzureAuthField.describe(AZURE_AUTH.ATTACH.allowedServicePrincipalIds), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(AZURE_AUTH.ATTACH.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(AZURE_AUTH.ATTACH.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(AZURE_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .default(0) + .describe(AZURE_AUTH.ATTACH.accessTokenNumUsesLimit) + }) + .refine( + (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityAzureAuth: IdentityAzureAuthsSchema @@ -122,7 +130,8 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, - identityId: req.params.identityId + identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth) }); await server.services.auditLog.createAuditLog({ @@ -154,6 +163,8 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.AzureAuth], description: "Update Azure Auth configuration on identity", security: [ { @@ -163,32 +174,40 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider params: z.object({ identityId: z.string().trim().describe(AZURE_AUTH.UPDATE.identityId) }), - body: z.object({ - tenantId: z.string().trim().optional().describe(AZURE_AUTH.UPDATE.tenantId), - resource: z.string().trim().optional().describe(AZURE_AUTH.UPDATE.resource), - allowedServicePrincipalIds: validateAzureAuthField - .optional() - .describe(AZURE_AUTH.UPDATE.allowedServicePrincipalIds), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .optional() - .describe(AZURE_AUTH.UPDATE.accessTokenTrustedIps), - accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(AZURE_AUTH.UPDATE.accessTokenTTL), - accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(AZURE_AUTH.UPDATE.accessTokenNumUsesLimit), - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .optional() - .describe(AZURE_AUTH.UPDATE.accessTokenMaxTTL) - }), + body: z + .object({ + tenantId: z.string().trim().optional().describe(AZURE_AUTH.UPDATE.tenantId), + resource: z.string().trim().optional().describe(AZURE_AUTH.UPDATE.resource), + allowedServicePrincipalIds: validateAzureAuthField + .optional() + .describe(AZURE_AUTH.UPDATE.allowedServicePrincipalIds), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .optional() + .describe(AZURE_AUTH.UPDATE.accessTokenTrustedIps), + accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(AZURE_AUTH.UPDATE.accessTokenTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .optional() + .describe(AZURE_AUTH.UPDATE.accessTokenNumUsesLimit), + accessTokenMaxTTL: z + .number() + .int() + .max(315360000) + .min(0) + .optional() + .describe(AZURE_AUTH.UPDATE.accessTokenMaxTTL) + }) + .refine( + (val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true), + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityAzureAuth: IdentityAzureAuthsSchema @@ -234,6 +253,8 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.AzureAuth], description: "Retrieve Azure Auth configuration on identity", security: [ { @@ -281,6 +302,8 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.AzureAuth], description: "Delete Azure Auth configuration on identity", security: [ { diff --git a/backend/src/server/routes/v1/identity-gcp-auth-router.ts b/backend/src/server/routes/v1/identity-gcp-auth-router.ts index 88c5af45f..b83faf9d9 100644 --- a/backend/src/server/routes/v1/identity-gcp-auth-router.ts +++ b/backend/src/server/routes/v1/identity-gcp-auth-router.ts @@ -2,12 +2,13 @@ import { z } from "zod"; import { IdentityGcpAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { GCP_AUTH } from "@app/lib/api-docs"; +import { ApiDocsTags, GCP_AUTH } from "@app/lib/api-docs"; 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"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; import { validateGcpAuthField } from "@app/services/identity-gcp-auth/identity-gcp-auth-validators"; +import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) => { server.route({ @@ -17,9 +18,11 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.GcpAuth], description: "Login with GCP Auth", body: z.object({ - identityId: z.string().trim().describe(GCP_AUTH.LOGIN.identityId).trim(), + identityId: z.string().trim().describe(GCP_AUTH.LOGIN.identityId), jwt: z.string() }), response: { @@ -65,6 +68,8 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.GcpAuth], description: "Attach GCP Auth configuration onto identity", security: [ { @@ -74,40 +79,40 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) params: z.object({ identityId: z.string().trim().describe(GCP_AUTH.ATTACH.identityId) }), - body: z.object({ - type: z.enum(["iam", "gce"]), - allowedServiceAccounts: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedServiceAccounts), - allowedProjects: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedProjects), - allowedZones: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedZones), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) - .describe(GCP_AUTH.ATTACH.accessTokenTrustedIps), - accessTokenTTL: z - .number() - .int() - .min(1) - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenTTL must have a non zero number" - }) - .default(2592000) - .describe(GCP_AUTH.ATTACH.accessTokenTTL), - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .default(2592000) - .describe(GCP_AUTH.ATTACH.accessTokenMaxTTL), - accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(GCP_AUTH.ATTACH.accessTokenNumUsesLimit) - }), + body: z + .object({ + type: z.enum(["iam", "gce"]), + allowedServiceAccounts: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedServiceAccounts), + allowedProjects: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedProjects), + allowedZones: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedZones), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(GCP_AUTH.ATTACH.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(GCP_AUTH.ATTACH.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(GCP_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(GCP_AUTH.ATTACH.accessTokenNumUsesLimit) + }) + .refine( + (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityGcpAuth: IdentityGcpAuthsSchema @@ -121,7 +126,8 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, - identityId: req.params.identityId + identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth) }); await server.services.auditLog.createAuditLog({ @@ -155,6 +161,8 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.GcpAuth], description: "Update GCP Auth configuration on identity", security: [ { @@ -164,31 +172,34 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) params: z.object({ identityId: z.string().trim().describe(GCP_AUTH.UPDATE.identityId) }), - body: z.object({ - type: z.enum(["iam", "gce"]).optional(), - allowedServiceAccounts: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedServiceAccounts), - allowedProjects: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedProjects), - allowedZones: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedZones), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .optional() - .describe(GCP_AUTH.UPDATE.accessTokenTrustedIps), - accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(GCP_AUTH.UPDATE.accessTokenTTL), - accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(GCP_AUTH.UPDATE.accessTokenNumUsesLimit), - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .optional() - .describe(GCP_AUTH.UPDATE.accessTokenMaxTTL) - }), + body: z + .object({ + type: z.enum(["iam", "gce"]).optional(), + allowedServiceAccounts: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedServiceAccounts), + allowedProjects: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedProjects), + allowedZones: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedZones), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .optional() + .describe(GCP_AUTH.UPDATE.accessTokenTrustedIps), + accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(GCP_AUTH.UPDATE.accessTokenTTL), + accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(GCP_AUTH.UPDATE.accessTokenNumUsesLimit), + accessTokenMaxTTL: z + .number() + .int() + .min(0) + .max(315360000) + .optional() + .describe(GCP_AUTH.UPDATE.accessTokenMaxTTL) + }) + .refine( + (val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true), + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityGcpAuth: IdentityGcpAuthsSchema @@ -236,6 +247,8 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.GcpAuth], description: "Retrieve GCP Auth configuration on identity", security: [ { @@ -283,6 +296,8 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.GcpAuth], description: "Delete GCP Auth configuration on identity", security: [ { diff --git a/backend/src/server/routes/v1/identity-jwt-auth-router.ts b/backend/src/server/routes/v1/identity-jwt-auth-router.ts new file mode 100644 index 000000000..373a8b927 --- /dev/null +++ b/backend/src/server/routes/v1/identity-jwt-auth-router.ts @@ -0,0 +1,376 @@ +import { z } from "zod"; + +import { IdentityJwtAuthsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags, JWT_AUTH } from "@app/lib/api-docs"; +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"; +import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; +import { JwtConfigurationType } from "@app/services/identity-jwt-auth/identity-jwt-auth-types"; +import { + validateJwtAuthAudiencesField, + validateJwtBoundClaimsField +} from "@app/services/identity-jwt-auth/identity-jwt-auth-validators"; +import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; + +const IdentityJwtAuthResponseSchema = IdentityJwtAuthsSchema.omit({ + encryptedJwksCaCert: true, + encryptedPublicKeys: true +}).extend({ + jwksCaCert: z.string(), + publicKeys: z.string().array() +}); + +const CreateBaseSchema = z.object({ + boundIssuer: z.string().trim().default("").describe(JWT_AUTH.ATTACH.boundIssuer), + boundAudiences: validateJwtAuthAudiencesField.describe(JWT_AUTH.ATTACH.boundAudiences), + boundClaims: validateJwtBoundClaimsField.describe(JWT_AUTH.ATTACH.boundClaims), + boundSubject: z.string().trim().default("").describe(JWT_AUTH.ATTACH.boundSubject), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(JWT_AUTH.ATTACH.accessTokenTrustedIps), + accessTokenTTL: z.number().int().min(0).max(315360000).default(2592000).describe(JWT_AUTH.ATTACH.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(JWT_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(JWT_AUTH.ATTACH.accessTokenNumUsesLimit) +}); + +const UpdateBaseSchema = z + .object({ + boundIssuer: z.string().trim().default("").describe(JWT_AUTH.UPDATE.boundIssuer), + boundAudiences: validateJwtAuthAudiencesField.describe(JWT_AUTH.UPDATE.boundAudiences), + boundClaims: validateJwtBoundClaimsField.describe(JWT_AUTH.UPDATE.boundClaims), + boundSubject: z.string().trim().default("").describe(JWT_AUTH.UPDATE.boundSubject), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(JWT_AUTH.UPDATE.accessTokenTrustedIps), + accessTokenTTL: z.number().int().min(0).max(315360000).default(2592000).describe(JWT_AUTH.UPDATE.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(JWT_AUTH.UPDATE.accessTokenMaxTTL), + accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(JWT_AUTH.UPDATE.accessTokenNumUsesLimit) + }) + .partial(); + +const JwksConfigurationSchema = z.object({ + configurationType: z.literal(JwtConfigurationType.JWKS).describe(JWT_AUTH.ATTACH.configurationType), + jwksUrl: z.string().trim().url().describe(JWT_AUTH.ATTACH.jwksUrl), + jwksCaCert: z.string().trim().default("").describe(JWT_AUTH.ATTACH.jwksCaCert), + publicKeys: z.string().array().optional().default([]).describe(JWT_AUTH.ATTACH.publicKeys) +}); + +const StaticConfigurationSchema = z.object({ + configurationType: z.literal(JwtConfigurationType.STATIC).describe(JWT_AUTH.ATTACH.configurationType), + jwksUrl: z.string().trim().optional().default("").describe(JWT_AUTH.ATTACH.jwksUrl), + jwksCaCert: z.string().trim().optional().default("").describe(JWT_AUTH.ATTACH.jwksCaCert), + publicKeys: z.string().min(1).array().min(1).describe(JWT_AUTH.ATTACH.publicKeys) +}); + +export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/jwt-auth/login", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.JwtAuth], + description: "Login with JWT Auth", + body: z.object({ + identityId: z.string().trim().describe(JWT_AUTH.LOGIN.identityId), + jwt: z.string().trim() + }), + response: { + 200: z.object({ + accessToken: z.string(), + expiresIn: z.coerce.number(), + accessTokenMaxTTL: z.coerce.number(), + tokenType: z.literal("Bearer") + }) + } + }, + handler: async (req) => { + const { identityJwtAuth, accessToken, identityAccessToken, identityMembershipOrg } = + await server.services.identityJwtAuth.login({ + identityId: req.body.identityId, + jwt: req.body.jwt + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityMembershipOrg?.orgId, + event: { + type: EventType.LOGIN_IDENTITY_JWT_AUTH, + metadata: { + identityId: identityJwtAuth.identityId, + identityAccessTokenId: identityAccessToken.id, + identityJwtAuthId: identityJwtAuth.id + } + } + }); + return { + accessToken, + tokenType: "Bearer" as const, + expiresIn: identityJwtAuth.accessTokenTTL, + accessTokenMaxTTL: identityJwtAuth.accessTokenMaxTTL + }; + } + }); + + server.route({ + method: "POST", + url: "/jwt-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.JwtAuth], + description: "Attach JWT Auth configuration onto identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().trim().describe(JWT_AUTH.ATTACH.identityId) + }), + body: z.discriminatedUnion("configurationType", [ + JwksConfigurationSchema.merge(CreateBaseSchema), + StaticConfigurationSchema.merge(CreateBaseSchema) + ]), + response: { + 200: z.object({ + identityJwtAuth: IdentityJwtAuthResponseSchema + }) + } + }, + handler: async (req) => { + const identityJwtAuth = await server.services.identityJwtAuth.attachJwtAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body, + identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth) + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityJwtAuth.orgId, + event: { + type: EventType.ADD_IDENTITY_JWT_AUTH, + metadata: { + identityId: identityJwtAuth.identityId, + configurationType: identityJwtAuth.configurationType, + jwksUrl: identityJwtAuth.jwksUrl, + jwksCaCert: identityJwtAuth.jwksCaCert, + publicKeys: identityJwtAuth.publicKeys, + boundIssuer: identityJwtAuth.boundIssuer, + boundAudiences: identityJwtAuth.boundAudiences, + boundClaims: identityJwtAuth.boundClaims as Record, + boundSubject: identityJwtAuth.boundSubject, + accessTokenTTL: identityJwtAuth.accessTokenTTL, + accessTokenMaxTTL: identityJwtAuth.accessTokenMaxTTL, + accessTokenTrustedIps: identityJwtAuth.accessTokenTrustedIps as TIdentityTrustedIp[], + accessTokenNumUsesLimit: identityJwtAuth.accessTokenNumUsesLimit + } + } + }); + + return { + identityJwtAuth + }; + } + }); + + server.route({ + method: "PATCH", + url: "/jwt-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.JwtAuth], + description: "Update JWT Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().trim().describe(JWT_AUTH.UPDATE.identityId) + }), + body: z.discriminatedUnion("configurationType", [ + JwksConfigurationSchema.merge(UpdateBaseSchema), + StaticConfigurationSchema.merge(UpdateBaseSchema) + ]), + response: { + 200: z.object({ + identityJwtAuth: IdentityJwtAuthResponseSchema + }) + } + }, + handler: async (req) => { + const identityJwtAuth = await server.services.identityJwtAuth.updateJwtAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + ...req.body, + identityId: req.params.identityId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityJwtAuth.orgId, + event: { + type: EventType.UPDATE_IDENTITY_JWT_AUTH, + metadata: { + identityId: identityJwtAuth.identityId, + configurationType: identityJwtAuth.configurationType, + jwksUrl: identityJwtAuth.jwksUrl, + jwksCaCert: identityJwtAuth.jwksCaCert, + publicKeys: identityJwtAuth.publicKeys, + boundIssuer: identityJwtAuth.boundIssuer, + boundAudiences: identityJwtAuth.boundAudiences, + boundClaims: identityJwtAuth.boundClaims as Record, + boundSubject: identityJwtAuth.boundSubject, + accessTokenTTL: identityJwtAuth.accessTokenTTL, + accessTokenMaxTTL: identityJwtAuth.accessTokenMaxTTL, + accessTokenTrustedIps: identityJwtAuth.accessTokenTrustedIps as TIdentityTrustedIp[], + accessTokenNumUsesLimit: identityJwtAuth.accessTokenNumUsesLimit + } + } + }); + + return { identityJwtAuth }; + } + }); + + server.route({ + method: "GET", + url: "/jwt-auth/identities/:identityId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.JwtAuth], + description: "Retrieve JWT Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(JWT_AUTH.RETRIEVE.identityId) + }), + response: { + 200: z.object({ + identityJwtAuth: IdentityJwtAuthResponseSchema + }) + } + }, + handler: async (req) => { + const identityJwtAuth = await server.services.identityJwtAuth.getJwtAuth({ + identityId: req.params.identityId, + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityJwtAuth.orgId, + event: { + type: EventType.GET_IDENTITY_JWT_AUTH, + metadata: { + identityId: identityJwtAuth.identityId + } + } + }); + + return { identityJwtAuth }; + } + }); + + server.route({ + method: "DELETE", + url: "/jwt-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.JwtAuth], + description: "Delete JWT Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(JWT_AUTH.REVOKE.identityId) + }), + response: { + 200: z.object({ + identityJwtAuth: IdentityJwtAuthResponseSchema.omit({ + publicKeys: true, + jwksCaCert: true + }) + }) + } + }, + handler: async (req) => { + const identityJwtAuth = await server.services.identityJwtAuth.revokeJwtAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityJwtAuth.orgId, + event: { + type: EventType.REVOKE_IDENTITY_JWT_AUTH, + metadata: { + identityId: identityJwtAuth.identityId + } + } + }); + + return { identityJwtAuth }; + } + }); +}; diff --git a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts index 3a71ba7a2..21759e0cd 100644 --- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts +++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts @@ -2,22 +2,29 @@ import { z } from "zod"; import { IdentityKubernetesAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { KUBERNETES_AUTH } from "@app/lib/api-docs"; +import { ApiDocsTags, KUBERNETES_AUTH } from "@app/lib/api-docs"; 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"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; +import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; -const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.omit({ - encryptedCaCert: true, - caCertIV: true, - caCertTag: true, - encryptedTokenReviewerJwt: true, - tokenReviewerJwtIV: true, - tokenReviewerJwtTag: true +const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.pick({ + id: true, + accessTokenTTL: true, + accessTokenMaxTTL: true, + accessTokenNumUsesLimit: true, + accessTokenTrustedIps: true, + createdAt: true, + updatedAt: true, + identityId: true, + kubernetesHost: true, + allowedNamespaces: true, + allowedNames: true, + allowedAudience: true }).extend({ caCert: z.string(), - tokenReviewerJwt: z.string() + tokenReviewerJwt: z.string().optional().nullable() }); export const registerIdentityKubernetesRouter = async (server: FastifyZodProvider) => { @@ -28,6 +35,8 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.KubernetesAuth], description: "Login with Kubernetes Auth", body: z.object({ identityId: z.string().trim().describe(KUBERNETES_AUTH.LOGIN.identityId), @@ -78,6 +87,8 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.KubernetesAuth], description: "Attach Kubernetes Auth configuration onto identity", security: [ { @@ -87,47 +98,47 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide params: z.object({ identityId: z.string().trim().describe(KUBERNETES_AUTH.ATTACH.identityId) }), - body: z.object({ - kubernetesHost: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.kubernetesHost), - caCert: z.string().trim().default("").describe(KUBERNETES_AUTH.ATTACH.caCert), - tokenReviewerJwt: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.tokenReviewerJwt), - allowedNamespaces: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNamespaces), // TODO: validation - allowedNames: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNames), - allowedAudience: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedAudience), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) - .describe(KUBERNETES_AUTH.ATTACH.accessTokenTrustedIps), - accessTokenTTL: z - .number() - .int() - .min(1) - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenTTL must have a non zero number" - }) - .default(2592000) - .describe(KUBERNETES_AUTH.ATTACH.accessTokenTTL), - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .default(2592000) - .describe(KUBERNETES_AUTH.ATTACH.accessTokenMaxTTL), - accessTokenNumUsesLimit: z - .number() - .int() - .min(0) - .default(0) - .describe(KUBERNETES_AUTH.ATTACH.accessTokenNumUsesLimit) - }), + body: z + .object({ + kubernetesHost: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.kubernetesHost), + caCert: z.string().trim().default("").describe(KUBERNETES_AUTH.ATTACH.caCert), + tokenReviewerJwt: z.string().trim().optional().describe(KUBERNETES_AUTH.ATTACH.tokenReviewerJwt), + allowedNamespaces: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNamespaces), // TODO: validation + allowedNames: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNames), + allowedAudience: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedAudience), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(KUBERNETES_AUTH.ATTACH.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(KUBERNETES_AUTH.ATTACH.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(KUBERNETES_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .default(0) + .describe(KUBERNETES_AUTH.ATTACH.accessTokenNumUsesLimit) + }) + .refine( + (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityKubernetesAuth: IdentityKubernetesAuthResponseSchema @@ -141,7 +152,8 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, - identityId: req.params.identityId + identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth) }); await server.services.auditLog.createAuditLog({ @@ -174,6 +186,8 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.KubernetesAuth], description: "Update Kubernetes Auth configuration on identity", security: [ { @@ -183,44 +197,47 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide params: z.object({ identityId: z.string().describe(KUBERNETES_AUTH.UPDATE.identityId) }), - body: z.object({ - kubernetesHost: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.kubernetesHost), - caCert: z.string().trim().optional().describe(KUBERNETES_AUTH.UPDATE.caCert), - tokenReviewerJwt: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.tokenReviewerJwt), - allowedNamespaces: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNamespaces), // TODO: validation - allowedNames: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNames), - allowedAudience: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedAudience), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .optional() - .describe(KUBERNETES_AUTH.UPDATE.accessTokenTrustedIps), - accessTokenTTL: z - .number() - .int() - .min(0) - .max(315360000) - .optional() - .describe(KUBERNETES_AUTH.UPDATE.accessTokenTTL), - accessTokenNumUsesLimit: z - .number() - .int() - .min(0) - .optional() - .describe(KUBERNETES_AUTH.UPDATE.accessTokenNumUsesLimit), - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .optional() - .describe(KUBERNETES_AUTH.UPDATE.accessTokenMaxTTL) - }), + body: z + .object({ + kubernetesHost: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.kubernetesHost), + caCert: z.string().trim().optional().describe(KUBERNETES_AUTH.UPDATE.caCert), + tokenReviewerJwt: z.string().trim().nullable().optional().describe(KUBERNETES_AUTH.UPDATE.tokenReviewerJwt), + allowedNamespaces: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNamespaces), // TODO: validation + allowedNames: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNames), + allowedAudience: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedAudience), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .optional() + .describe(KUBERNETES_AUTH.UPDATE.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .optional() + .describe(KUBERNETES_AUTH.UPDATE.accessTokenTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .optional() + .describe(KUBERNETES_AUTH.UPDATE.accessTokenNumUsesLimit), + accessTokenMaxTTL: z + .number() + .int() + .min(0) + .max(315360000) + .optional() + .describe(KUBERNETES_AUTH.UPDATE.accessTokenMaxTTL) + }) + .refine( + (val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true), + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityKubernetesAuth: IdentityKubernetesAuthResponseSchema @@ -267,6 +284,8 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.KubernetesAuth], description: "Retrieve Kubernetes Auth configuration on identity", security: [ { @@ -314,6 +333,8 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.KubernetesAuth], description: "Delete Kubernetes Auth configuration on identity", security: [ { diff --git a/backend/src/server/routes/v1/identity-oidc-auth-router.ts b/backend/src/server/routes/v1/identity-oidc-auth-router.ts index 280dbc5d5..74cd94eb5 100644 --- a/backend/src/server/routes/v1/identity-oidc-auth-router.ts +++ b/backend/src/server/routes/v1/identity-oidc-auth-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { IdentityOidcAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { OIDC_AUTH } from "@app/lib/api-docs"; +import { ApiDocsTags, OIDC_AUTH } from "@app/lib/api-docs"; 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"; @@ -11,15 +11,29 @@ import { validateOidcAuthAudiencesField, validateOidcBoundClaimsField } from "@app/services/identity-oidc-auth/identity-oidc-auth-validators"; +import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; -const IdentityOidcAuthResponseSchema = IdentityOidcAuthsSchema.omit({ - encryptedCaCert: true, - caCertIV: true, - caCertTag: true +const IdentityOidcAuthResponseSchema = IdentityOidcAuthsSchema.pick({ + id: true, + accessTokenTTL: true, + accessTokenMaxTTL: true, + accessTokenNumUsesLimit: true, + accessTokenTrustedIps: true, + identityId: true, + oidcDiscoveryUrl: true, + boundIssuer: true, + boundAudiences: true, + boundClaims: true, + claimMetadataMapping: true, + boundSubject: true, + createdAt: true, + updatedAt: true }).extend({ caCert: z.string() }); +const MAX_OIDC_CLAIM_SIZE = 32_768; + export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", @@ -28,6 +42,8 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.OidcAuth], description: "Login with OIDC Auth", body: z.object({ identityId: z.string().trim().describe(OIDC_AUTH.LOGIN.identityId), @@ -43,7 +59,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) } }, handler: async (req) => { - const { identityOidcAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityOidcAuth, accessToken, identityAccessToken, identityMembershipOrg, oidcTokenData } = await server.services.identityOidcAuth.login({ identityId: req.body.identityId, jwt: req.body.jwt @@ -57,7 +73,11 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) metadata: { identityId: identityOidcAuth.identityId, identityAccessTokenId: identityAccessToken.id, - identityOidcAuthId: identityOidcAuth.id + identityOidcAuthId: identityOidcAuth.id, + oidcClaimsReceived: + Buffer.from(JSON.stringify(oidcTokenData), "utf8").byteLength < MAX_OIDC_CLAIM_SIZE + ? oidcTokenData + : { payload: "Error: Payload exceeds 32KB, provided oidc claim not recorded in audit log." } } } }); @@ -78,6 +98,8 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.OidcAuth], description: "Attach OIDC Auth configuration onto identity", security: [ { @@ -87,42 +109,43 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) params: z.object({ identityId: z.string().trim().describe(OIDC_AUTH.ATTACH.identityId) }), - body: z.object({ - oidcDiscoveryUrl: z.string().url().min(1).describe(OIDC_AUTH.ATTACH.oidcDiscoveryUrl), - caCert: z.string().trim().default("").describe(OIDC_AUTH.ATTACH.caCert), - boundIssuer: z.string().min(1).describe(OIDC_AUTH.ATTACH.boundIssuer), - boundAudiences: validateOidcAuthAudiencesField.describe(OIDC_AUTH.ATTACH.boundAudiences), - boundClaims: validateOidcBoundClaimsField.describe(OIDC_AUTH.ATTACH.boundClaims), - boundSubject: z.string().optional().default("").describe(OIDC_AUTH.ATTACH.boundSubject), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) - .describe(OIDC_AUTH.ATTACH.accessTokenTrustedIps), - accessTokenTTL: z - .number() - .int() - .min(1) - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenTTL must have a non zero number" - }) - .default(2592000) - .describe(OIDC_AUTH.ATTACH.accessTokenTTL), - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .default(2592000) - .describe(OIDC_AUTH.ATTACH.accessTokenMaxTTL), - accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(OIDC_AUTH.ATTACH.accessTokenNumUsesLimit) - }), + body: z + .object({ + oidcDiscoveryUrl: z.string().url().min(1).describe(OIDC_AUTH.ATTACH.oidcDiscoveryUrl), + caCert: z.string().trim().default("").describe(OIDC_AUTH.ATTACH.caCert), + boundIssuer: z.string().min(1).describe(OIDC_AUTH.ATTACH.boundIssuer), + boundAudiences: validateOidcAuthAudiencesField.describe(OIDC_AUTH.ATTACH.boundAudiences), + boundClaims: validateOidcBoundClaimsField.describe(OIDC_AUTH.ATTACH.boundClaims), + claimMetadataMapping: validateOidcBoundClaimsField.describe(OIDC_AUTH.ATTACH.claimMetadataMapping).optional(), + boundSubject: z.string().optional().default("").describe(OIDC_AUTH.ATTACH.boundSubject), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(OIDC_AUTH.ATTACH.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(OIDC_AUTH.ATTACH.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(OIDC_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(OIDC_AUTH.ATTACH.accessTokenNumUsesLimit) + }) + .refine( + (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityOidcAuth: IdentityOidcAuthResponseSchema @@ -136,7 +159,8 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, - identityId: req.params.identityId + identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth) }); await server.services.auditLog.createAuditLog({ @@ -151,6 +175,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) boundIssuer: identityOidcAuth.boundIssuer, boundAudiences: identityOidcAuth.boundAudiences, boundClaims: identityOidcAuth.boundClaims as Record, + claimMetadataMapping: identityOidcAuth.claimMetadataMapping as Record, boundSubject: identityOidcAuth.boundSubject as string, accessTokenTTL: identityOidcAuth.accessTokenTTL, accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL, @@ -174,6 +199,8 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.OidcAuth], description: "Update OIDC Auth configuration on identity", security: [ { @@ -190,6 +217,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) boundIssuer: z.string().min(1).describe(OIDC_AUTH.UPDATE.boundIssuer), boundAudiences: validateOidcAuthAudiencesField.describe(OIDC_AUTH.UPDATE.boundAudiences), boundClaims: validateOidcBoundClaimsField.describe(OIDC_AUTH.UPDATE.boundClaims), + claimMetadataMapping: validateOidcBoundClaimsField.describe(OIDC_AUTH.UPDATE.claimMetadataMapping).optional(), boundSubject: z.string().optional().default("").describe(OIDC_AUTH.UPDATE.boundSubject), accessTokenTrustedIps: z .object({ @@ -202,26 +230,24 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) accessTokenTTL: z .number() .int() - .min(1) + .min(0) .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenTTL must have a non zero number" - }) .default(2592000) .describe(OIDC_AUTH.UPDATE.accessTokenTTL), accessTokenMaxTTL: z .number() .int() + .min(0) .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) .default(2592000) .describe(OIDC_AUTH.UPDATE.accessTokenMaxTTL), - accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(OIDC_AUTH.UPDATE.accessTokenNumUsesLimit) }) - .partial(), + .partial() + .refine( + (val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true), + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityOidcAuth: IdentityOidcAuthResponseSchema @@ -250,6 +276,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) boundIssuer: identityOidcAuth.boundIssuer, boundAudiences: identityOidcAuth.boundAudiences, boundClaims: identityOidcAuth.boundClaims as Record, + claimMetadataMapping: identityOidcAuth.claimMetadataMapping as Record, boundSubject: identityOidcAuth.boundSubject as string, accessTokenTTL: identityOidcAuth.accessTokenTTL, accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL, @@ -271,6 +298,8 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.OidcAuth], description: "Retrieve OIDC Auth configuration on identity", security: [ { @@ -318,6 +347,8 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.OidcAuth], description: "Delete OIDC Auth configuration on identity", security: [ { diff --git a/backend/src/server/routes/v1/identity-router.ts b/backend/src/server/routes/v1/identity-router.ts index 15e6eabef..7731aad98 100644 --- a/backend/src/server/routes/v1/identity-router.ts +++ b/backend/src/server/routes/v1/identity-router.ts @@ -2,15 +2,27 @@ import { z } from "zod"; import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { IDENTITIES } from "@app/lib/api-docs"; +import { ApiDocsTags, IDENTITIES } from "@app/lib/api-docs"; +import { buildSearchZodSchema, SearchResourceOperators } from "@app/lib/search-resource/search"; +import { OrderByDirection } from "@app/lib/types"; +import { CharacterType, zodValidateCharacters } from "@app/lib/validator/validate-string"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { OrgIdentityOrderBy } from "@app/services/identity/identity-types"; +import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; import { SanitizedProjectSchema } from "../sanitizedSchemas"; +const searchResourceZodValidate = zodValidateCharacters([ + CharacterType.AlphaNumeric, + CharacterType.Spaces, + CharacterType.Underscore, + CharacterType.Hyphen +]); + export const registerIdentityRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", @@ -20,6 +32,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Identities], description: "Create identity", security: [ { @@ -88,6 +102,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Identities], description: "Update identity", security: [ { @@ -118,6 +134,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth), ...req.body }); @@ -145,6 +162,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Identities], description: "Delete identity", security: [ { @@ -166,7 +185,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - id: req.params.identityId + id: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth) }); await server.services.auditLog.createAuditLog({ @@ -191,6 +211,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Identities], description: "Get an identity by id", security: [ { @@ -242,10 +264,12 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { method: "GET", url: "/", config: { - rateLimit: writeLimit + rateLimit: readLimit }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Identities], description: "List identities", security: [ { @@ -286,6 +310,105 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "POST", + url: "/search", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.Identities], + description: "Search identities", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + orderBy: z + .nativeEnum(OrgIdentityOrderBy) + .default(OrgIdentityOrderBy.Name) + .describe(IDENTITIES.SEARCH.orderBy) + .optional(), + orderDirection: z + .nativeEnum(OrderByDirection) + .default(OrderByDirection.ASC) + .describe(IDENTITIES.SEARCH.orderDirection) + .optional(), + limit: z.number().max(100).default(50).describe(IDENTITIES.SEARCH.limit), + offset: z.number().default(0).describe(IDENTITIES.SEARCH.offset), + search: buildSearchZodSchema( + z + .object({ + name: z + .union([ + searchResourceZodValidate(z.string().max(255), "Name"), + z + .object({ + [SearchResourceOperators.$eq]: searchResourceZodValidate(z.string().max(255), "Name $eq"), + [SearchResourceOperators.$contains]: searchResourceZodValidate( + z.string().max(255), + "Name $contains" + ), + [SearchResourceOperators.$in]: searchResourceZodValidate(z.string().max(255), "Name $in").array() + }) + .partial() + ]) + .describe(IDENTITIES.SEARCH.search.name), + role: z + .union([ + searchResourceZodValidate(z.string().max(255), "Role"), + z + .object({ + [SearchResourceOperators.$eq]: searchResourceZodValidate(z.string().max(255), "Role $eq"), + [SearchResourceOperators.$in]: searchResourceZodValidate(z.string().max(255), "Role $in").array() + }) + .partial() + ]) + .describe(IDENTITIES.SEARCH.search.role) + }) + .describe(IDENTITIES.SEARCH.search.desc) + .partial() + ) + }), + response: { + 200: z.object({ + identities: IdentityOrgMembershipsSchema.extend({ + customRole: OrgRolesSchema.pick({ + id: true, + name: true, + slug: true, + permissions: true, + description: true + }).optional(), + identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ + authMethods: z.array(z.string()) + }) + }).array(), + totalCount: z.number() + }) + } + }, + handler: async (req) => { + const { identityMemberships, totalCount } = await server.services.identity.searchOrgIdentities({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + searchFilter: req.body.search, + orgId: req.permission.orgId, + limit: req.body.limit, + offset: req.body.offset, + orderBy: req.body.orderBy, + orderDirection: req.body.orderDirection + }); + + return { identities: identityMemberships, totalCount }; + } + }); + server.route({ method: "GET", url: "/:identityId/identity-memberships", @@ -328,7 +451,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ authMethods: z.array(z.string()) }), - project: SanitizedProjectSchema.pick({ name: true, id: true }) + project: SanitizedProjectSchema.pick({ name: true, id: true, type: true }) }) ) }) diff --git a/backend/src/server/routes/v1/identity-token-auth-router.ts b/backend/src/server/routes/v1/identity-token-auth-router.ts index f367e6033..e22c41889 100644 --- a/backend/src/server/routes/v1/identity-token-auth-router.ts +++ b/backend/src/server/routes/v1/identity-token-auth-router.ts @@ -2,11 +2,12 @@ import { z } from "zod"; import { IdentityAccessTokensSchema, IdentityTokenAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { TOKEN_AUTH } from "@app/lib/api-docs"; +import { ApiDocsTags, TOKEN_AUTH } from "@app/lib/api-docs"; 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"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; +import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider) => { server.route({ @@ -17,6 +18,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.TokenAuth], description: "Attach Token Auth configuration onto identity", security: [ { @@ -26,36 +29,41 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider params: z.object({ identityId: z.string().trim().describe(TOKEN_AUTH.ATTACH.identityId) }), - body: z.object({ - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) - .describe(TOKEN_AUTH.ATTACH.accessTokenTrustedIps), - accessTokenTTL: z - .number() - .int() - .min(1) - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenTTL must have a non zero number" - }) - .default(2592000) - .describe(TOKEN_AUTH.ATTACH.accessTokenTTL), - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .default(2592000) - .describe(TOKEN_AUTH.ATTACH.accessTokenMaxTTL), - accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(TOKEN_AUTH.ATTACH.accessTokenNumUsesLimit) - }), + body: z + .object({ + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(TOKEN_AUTH.ATTACH.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(TOKEN_AUTH.ATTACH.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(TOKEN_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .default(0) + .describe(TOKEN_AUTH.ATTACH.accessTokenNumUsesLimit) + }) + .refine( + (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityTokenAuth: IdentityTokenAuthsSchema @@ -69,7 +77,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, - identityId: req.params.identityId + identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth) }); await server.services.auditLog.createAuditLog({ @@ -101,6 +110,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.TokenAuth], description: "Update Token Auth configuration on identity", security: [ { @@ -110,27 +121,35 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider params: z.object({ identityId: z.string().trim().describe(TOKEN_AUTH.UPDATE.identityId) }), - body: z.object({ - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .optional() - .describe(TOKEN_AUTH.UPDATE.accessTokenTrustedIps), - accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(TOKEN_AUTH.UPDATE.accessTokenTTL), - accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(TOKEN_AUTH.UPDATE.accessTokenNumUsesLimit), - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .optional() - .describe(TOKEN_AUTH.UPDATE.accessTokenMaxTTL) - }), + body: z + .object({ + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .optional() + .describe(TOKEN_AUTH.UPDATE.accessTokenTrustedIps), + accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(TOKEN_AUTH.UPDATE.accessTokenTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .optional() + .describe(TOKEN_AUTH.UPDATE.accessTokenNumUsesLimit), + accessTokenMaxTTL: z + .number() + .int() + .min(0) + .max(315360000) + .optional() + .describe(TOKEN_AUTH.UPDATE.accessTokenMaxTTL) + }) + .refine( + (val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true), + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityTokenAuth: IdentityTokenAuthsSchema @@ -144,7 +163,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, ...req.body, - identityId: req.params.identityId + identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth) }); await server.services.auditLog.createAuditLog({ @@ -176,6 +196,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.TokenAuth], description: "Retrieve Token Auth configuration on identity", security: [ { @@ -223,6 +245,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.TokenAuth], description: "Delete Token Auth configuration on identity", security: [ { @@ -244,7 +268,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - identityId: req.params.identityId + identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth) }); await server.services.auditLog.createAuditLog({ @@ -270,6 +295,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.TokenAuth], description: "Create token for identity with Token Auth", security: [ { @@ -299,6 +326,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth), ...req.body }); @@ -331,6 +359,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.TokenAuth], description: "Get tokens for identity with Token Auth", security: [ { @@ -357,6 +387,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth), ...req.query }); @@ -383,6 +414,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.TokenAuth], description: "Update token for identity with Token Auth", security: [ { @@ -408,6 +441,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, tokenId: req.params.tokenId, + isActorSuperAdmin: isSuperAdmin(req.auth), ...req.body }); @@ -436,6 +470,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.TokenAuth], description: "Revoke token for identity with Token Auth", security: [ { @@ -457,7 +493,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - tokenId: req.params.tokenId + tokenId: req.params.tokenId, + isActorSuperAdmin: isSuperAdmin(req.auth) }); return { diff --git a/backend/src/server/routes/v1/identity-universal-auth-router.ts b/backend/src/server/routes/v1/identity-universal-auth-router.ts index f103a39e0..6fe4c7a85 100644 --- a/backend/src/server/routes/v1/identity-universal-auth-router.ts +++ b/backend/src/server/routes/v1/identity-universal-auth-router.ts @@ -2,11 +2,12 @@ import { z } from "zod"; import { IdentityUaClientSecretsSchema, IdentityUniversalAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { UNIVERSAL_AUTH } from "@app/lib/api-docs"; +import { ApiDocsTags, UNIVERSAL_AUTH } from "@app/lib/api-docs"; 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"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; +import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; export const sanitizedClientSecretSchema = IdentityUaClientSecretsSchema.pick({ id: true, @@ -29,6 +30,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.UniversalAuth], description: "Login with Universal Auth", body: z.object({ clientId: z.string().trim().describe(UNIVERSAL_AUTH.LOGIN.clientId), @@ -77,6 +80,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.UniversalAuth], description: "Attach Universal Auth configuration onto identity", security: [ { @@ -86,49 +91,49 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { params: z.object({ identityId: z.string().trim().describe(UNIVERSAL_AUTH.ATTACH.identityId) }), - body: z.object({ - clientSecretTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) - .describe(UNIVERSAL_AUTH.ATTACH.clientSecretTrustedIps), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) - .describe(UNIVERSAL_AUTH.ATTACH.accessTokenTrustedIps), - accessTokenTTL: z - .number() - .int() - .min(1) - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenTTL must have a non zero number" - }) - .default(2592000) - .describe(UNIVERSAL_AUTH.ATTACH.accessTokenTTL), // 30 days - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .default(2592000) - .describe(UNIVERSAL_AUTH.ATTACH.accessTokenMaxTTL), // 30 days - accessTokenNumUsesLimit: z - .number() - .int() - .min(0) - .default(0) - .describe(UNIVERSAL_AUTH.ATTACH.accessTokenNumUsesLimit) - }), + body: z + .object({ + clientSecretTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(UNIVERSAL_AUTH.ATTACH.clientSecretTrustedIps), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(UNIVERSAL_AUTH.ATTACH.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(UNIVERSAL_AUTH.ATTACH.accessTokenTTL), // 30 days + accessTokenMaxTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(UNIVERSAL_AUTH.ATTACH.accessTokenMaxTTL), // 30 days + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .default(0) + .describe(UNIVERSAL_AUTH.ATTACH.accessTokenNumUsesLimit) + }) + .refine( + (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityUniversalAuth: IdentityUniversalAuthsSchema @@ -142,8 +147,10 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, ...req.body, - identityId: req.params.identityId + identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth) }); + await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: identityUniversalAuth.orgId, @@ -172,6 +179,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.UniversalAuth], description: "Update Universal Auth configuration on identity", security: [ { @@ -181,46 +190,49 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { params: z.object({ identityId: z.string().describe(UNIVERSAL_AUTH.UPDATE.identityId) }), - body: z.object({ - clientSecretTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .optional() - .describe(UNIVERSAL_AUTH.UPDATE.clientSecretTrustedIps), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .optional() - .describe(UNIVERSAL_AUTH.UPDATE.accessTokenTrustedIps), - accessTokenTTL: z - .number() - .int() - .min(0) - .max(315360000) - .optional() - .describe(UNIVERSAL_AUTH.UPDATE.accessTokenTTL), - accessTokenNumUsesLimit: z - .number() - .int() - .min(0) - .optional() - .describe(UNIVERSAL_AUTH.UPDATE.accessTokenNumUsesLimit), - accessTokenMaxTTL: z - .number() - .int() - .max(315360000) - .refine((value) => value !== 0, { - message: "accessTokenMaxTTL must have a non zero number" - }) - .optional() - .describe(UNIVERSAL_AUTH.UPDATE.accessTokenMaxTTL) - }), + body: z + .object({ + clientSecretTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .optional() + .describe(UNIVERSAL_AUTH.UPDATE.clientSecretTrustedIps), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .optional() + .describe(UNIVERSAL_AUTH.UPDATE.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .optional() + .describe(UNIVERSAL_AUTH.UPDATE.accessTokenTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .optional() + .describe(UNIVERSAL_AUTH.UPDATE.accessTokenNumUsesLimit), + accessTokenMaxTTL: z + .number() + .int() + .min(0) + .max(315360000) + .optional() + .describe(UNIVERSAL_AUTH.UPDATE.accessTokenMaxTTL) + }) + .refine( + (val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true), + "Access Token TTL cannot be greater than Access Token Max TTL." + ), response: { 200: z.object({ identityUniversalAuth: IdentityUniversalAuthsSchema @@ -265,6 +277,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.UniversalAuth], description: "Retrieve Universal Auth configuration on identity", security: [ { @@ -312,6 +326,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.UniversalAuth], description: "Delete Universal Auth configuration on identity", security: [ { @@ -359,6 +375,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.UniversalAuth], description: "Create Universal Auth Client Secret for identity", security: [ { @@ -415,6 +433,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.UniversalAuth], description: "List Universal Auth Client Secrets for identity", security: [ { @@ -463,6 +483,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.UniversalAuth], description: "Get Universal Auth Client Secret for identity", security: [ { @@ -513,6 +535,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.UniversalAuth], description: "Revoke Universal Auth Client Secrets for identity", security: [ { diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index f9edfc18c..50fd33840 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -1,5 +1,10 @@ +import { + APP_CONNECTION_REGISTER_ROUTER_MAP, + registerAppConnectionRouter +} from "@app/server/routes/v1/app-connection-routers"; import { registerCmekRouter } from "@app/server/routes/v1/cmek-router"; import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router"; +import { registerSecretSyncRouter, SECRET_SYNC_REGISTER_ROUTER_MAP } from "@app/server/routes/v1/secret-sync-routers"; import { registerAdminRouter } from "./admin-router"; import { registerAuthRoutes } from "./auth-router"; @@ -12,6 +17,7 @@ import { registerIdentityAccessTokenRouter } from "./identity-access-token-route import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router"; import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router"; import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router"; +import { registerIdentityJwtAuthRouter } from "./identity-jwt-auth-router"; import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router"; import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router"; import { registerIdentityRouter } from "./identity-router"; @@ -31,6 +37,7 @@ import { registerProjectMembershipRouter } from "./project-membership-router"; import { registerProjectRouter } from "./project-router"; import { registerSecretFolderRouter } from "./secret-folder-router"; import { registerSecretImportRouter } from "./secret-import-router"; +import { registerSecretRequestsRouter } from "./secret-requests-router"; import { registerSecretSharingRouter } from "./secret-sharing-router"; import { registerSecretTagRouter } from "./secret-tag-router"; import { registerSlackRouter } from "./slack-router"; @@ -54,6 +61,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await authRouter.register(registerIdentityAwsAuthRouter); await authRouter.register(registerIdentityAzureAuthRouter); await authRouter.register(registerIdentityOidcAuthRouter); + await authRouter.register(registerIdentityJwtAuthRouter); }, { prefix: "/auth" } ); @@ -83,7 +91,6 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await projectRouter.register(registerProjectMembershipRouter); await projectRouter.register(registerSecretTagRouter); }, - { prefix: "/workspace" } ); @@ -103,9 +110,43 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" }); await server.register(registerWebhookRouter, { prefix: "/webhooks" }); await server.register(registerIdentityRouter, { prefix: "/identities" }); - await server.register(registerSecretSharingRouter, { prefix: "/secret-sharing" }); + + await server.register( + async (secretSharingRouter) => { + await secretSharingRouter.register(registerSecretSharingRouter, { prefix: "/shared" }); + await secretSharingRouter.register(registerSecretRequestsRouter, { prefix: "/requests" }); + }, + { prefix: "/secret-sharing" } + ); + await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" }); await server.register(registerDashboardRouter, { prefix: "/dashboard" }); await server.register(registerCmekRouter, { prefix: "/kms" }); await server.register(registerExternalGroupOrgRoleMappingRouter, { prefix: "/external-group-mappings" }); + + await server.register( + async (appConnectionRouter) => { + // register generic app connection endpoints + await appConnectionRouter.register(registerAppConnectionRouter); + + // register service specific endpoints (app-connections/aws, app-connections/github, etc.) + for await (const [app, router] of Object.entries(APP_CONNECTION_REGISTER_ROUTER_MAP)) { + await appConnectionRouter.register(router, { prefix: `/${app}` }); + } + }, + { prefix: "/app-connections" } + ); + + await server.register( + async (secretSyncRouter) => { + // register generic secret sync endpoints + await secretSyncRouter.register(registerSecretSyncRouter); + + // register service specific secret sync endpoints (secret-syncs/aws-parameter-store, secret-syncs/github, etc.) + for await (const [destination, router] of Object.entries(SECRET_SYNC_REGISTER_ROUTER_MAP)) { + await secretSyncRouter.register(router, { prefix: `/${destination}` }); + } + }, + { prefix: "/secret-syncs" } + ); }; diff --git a/backend/src/server/routes/v1/integration-auth-router.ts b/backend/src/server/routes/v1/integration-auth-router.ts index 1d2959f5b..e5156724d 100644 --- a/backend/src/server/routes/v1/integration-auth-router.ts +++ b/backend/src/server/routes/v1/integration-auth-router.ts @@ -1,10 +1,12 @@ import { z } from "zod"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { INTEGRATION_AUTH } from "@app/lib/api-docs"; +import { ApiDocsTags, INTEGRATION_AUTH } from "@app/lib/api-docs"; 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"; +import { OctopusDeployScope } from "@app/services/integration-auth/integration-auth-types"; +import { Integrations } from "@app/services/integration-auth/integration-list"; import { integrationAuthPubSchema } from "../sanitizedSchemas"; @@ -17,6 +19,8 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Integrations], description: "List of integrations available.", security: [ { @@ -29,6 +33,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) .object({ name: z.string(), slug: z.string(), + syncSlug: z.string().optional(), clientSlug: z.string().optional(), image: z.string(), isAvailable: z.boolean().optional(), @@ -54,6 +59,8 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Integrations], description: "Get details of an integration authorization by auth object id.", security: [ { @@ -81,6 +88,69 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) } }); + server.route({ + method: "PATCH", + url: "/:integrationAuthId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.Integrations], + description: "Update the integration authentication object required for syncing secrets.", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + integrationAuthId: z.string().trim().describe(INTEGRATION_AUTH.UPDATE_BY_ID.integrationAuthId) + }), + body: z.object({ + integration: z.nativeEnum(Integrations).optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.integration), + accessId: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.accessId), + accessToken: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.accessToken), + awsAssumeIamRoleArn: z + .string() + .url() + .trim() + .optional() + .describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.awsAssumeIamRoleArn), + url: z.string().url().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.url), + namespace: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.namespace), + refreshToken: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.refreshToken) + }), + response: { + 200: z.object({ + integrationAuth: integrationAuthPubSchema + }) + } + }, + handler: async (req) => { + const integrationAuth = await server.services.integrationAuth.updateIntegrationAuth({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + integrationAuthId: req.params.integrationAuthId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: integrationAuth.projectId, + event: { + type: EventType.UPDATE_INTEGRATION_AUTH, + metadata: { + integration: integrationAuth.integration + } + } + }); + return { integrationAuth }; + } + }); + server.route({ method: "DELETE", url: "/", @@ -89,6 +159,8 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Integrations], description: "Remove all integration's auth object from the project.", security: [ { @@ -138,6 +210,8 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Integrations], description: "Remove an integration auth object by object id.", security: [ { @@ -230,6 +304,8 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Integrations], description: "Create the integration authentication object required for syncing secrets.", security: [ { @@ -891,6 +967,48 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) } }); + server.route({ + method: "GET", + url: "/:integrationAuthId/bitbucket/environments", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + params: z.object({ + integrationAuthId: z.string().trim() + }), + querystring: z.object({ + workspaceSlug: z.string().trim().min(1, { message: "Workspace slug required" }), + repoSlug: z.string().trim().min(1, { message: "Repo slug required" }) + }), + response: { + 200: z.object({ + environments: z + .object({ + name: z.string(), + slug: z.string(), + uuid: z.string(), + type: z.string() + }) + .array() + }) + } + }, + handler: async (req) => { + const environments = await server.services.integrationAuth.getBitbucketEnvironments({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.integrationAuthId, + workspaceSlug: req.query.workspaceSlug, + repoSlug: req.query.repoSlug + }); + return { environments }; + } + }); + server.route({ method: "GET", url: "/:integrationAuthId/northflank/secret-groups", @@ -966,4 +1084,208 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) return { buildConfigs }; } }); + + server.route({ + method: "GET", + url: "/:integrationAuthId/octopus-deploy/scope-values", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + params: z.object({ + integrationAuthId: z.string().trim() + }), + querystring: z.object({ + scope: z.nativeEnum(OctopusDeployScope), + spaceId: z.string().trim(), + resourceId: z.string().trim() + }), + response: { + 200: z.object({ + Environments: z + .object({ + Name: z.string(), + Id: z.string() + }) + .array(), + Machines: z + .object({ + Name: z.string(), + Id: z.string() + }) + .array(), + Actions: z + .object({ + Name: z.string(), + Id: z.string() + }) + .array(), + Roles: z + .object({ + Name: z.string(), + Id: z.string() + }) + .array(), + Channels: z + .object({ + Name: z.string(), + Id: z.string() + }) + .array(), + TenantTags: z + .object({ + Name: z.string(), + Id: z.string() + }) + .array(), + Processes: z + .object({ + ProcessType: z.string(), + Name: z.string(), + Id: z.string() + }) + .array() + }) + } + }, + handler: async (req) => { + const scopeValues = await server.services.integrationAuth.getOctopusDeployScopeValues({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.integrationAuthId, + scope: req.query.scope, + spaceId: req.query.spaceId, + resourceId: req.query.resourceId + }); + return scopeValues; + } + }); + + server.route({ + method: "GET", + url: "/:integrationAuthId/vercel/custom-environments", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + querystring: z.object({ + teamId: z.string().trim() + }), + params: z.object({ + integrationAuthId: z.string().trim() + }), + response: { + 200: z.object({ + environments: z + .object({ + appId: z.string(), + customEnvironments: z + .object({ + id: z.string(), + slug: z.string() + }) + .array() + }) + .array() + }) + } + }, + handler: async (req) => { + const environments = await server.services.integrationAuth.getVercelCustomEnvironments({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.integrationAuthId, + teamId: req.query.teamId + }); + + return { environments }; + } + }); + + server.route({ + method: "GET", + url: "/:integrationAuthId/octopus-deploy/spaces", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + params: z.object({ + integrationAuthId: z.string().trim() + }), + response: { + 200: z.object({ + spaces: z + .object({ + Name: z.string(), + Id: z.string(), + IsDefault: z.boolean() + }) + .array() + }) + } + }, + handler: async (req) => { + const spaces = await server.services.integrationAuth.getOctopusDeploySpaces({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.integrationAuthId + }); + return { spaces }; + } + }); + + server.route({ + method: "GET", + url: "/:integrationAuthId/circleci/organizations", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + params: z.object({ + integrationAuthId: z.string().trim() + }), + response: { + 200: z.object({ + organizations: z + .object({ + name: z.string(), + slug: z.string(), + projects: z + .object({ + name: z.string(), + id: z.string() + }) + .array(), + contexts: z + .object({ + name: z.string(), + id: z.string() + }) + .array() + }) + .array() + }) + } + }, + handler: async (req) => { + const organizations = await server.services.integrationAuth.getCircleCIOrganizations({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.integrationAuthId + }); + return { organizations }; + } + }); }; diff --git a/backend/src/server/routes/v1/integration-router.ts b/backend/src/server/routes/v1/integration-router.ts index 86d321852..f3964e7b7 100644 --- a/backend/src/server/routes/v1/integration-router.ts +++ b/backend/src/server/routes/v1/integration-router.ts @@ -2,13 +2,14 @@ import { z } from "zod"; import { IntegrationsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { INTEGRATION } from "@app/lib/api-docs"; +import { ApiDocsTags, INTEGRATION } from "@app/lib/api-docs"; import { removeTrailingSlash, shake } from "@app/lib/fn"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { IntegrationMetadataSchema } from "@app/services/integration/integration-schema"; +import { Integrations } from "@app/services/integration-auth/integration-list"; import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types"; import {} from "../sanitizedSchemas"; @@ -21,6 +22,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Integrations], description: "Create an integration to sync secrets.", security: [ { @@ -118,6 +121,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Integrations], description: "Update an integration by integration id", security: [ { @@ -130,7 +135,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { body: z.object({ app: z.string().trim().optional().describe(INTEGRATION.UPDATE.app), appId: z.string().trim().optional().describe(INTEGRATION.UPDATE.appId), - isActive: z.boolean().describe(INTEGRATION.UPDATE.isActive), + isActive: z.boolean().optional().describe(INTEGRATION.UPDATE.isActive), secretPath: z .string() .trim() @@ -140,7 +145,9 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { targetEnvironment: z.string().trim().optional().describe(INTEGRATION.UPDATE.targetEnvironment), owner: z.string().trim().optional().describe(INTEGRATION.UPDATE.owner), environment: z.string().trim().optional().describe(INTEGRATION.UPDATE.environment), - metadata: IntegrationMetadataSchema.optional() + path: z.string().trim().optional().describe(INTEGRATION.UPDATE.path), + metadata: IntegrationMetadataSchema.optional(), + region: z.string().trim().optional().describe(INTEGRATION.UPDATE.region) }), response: { 200: z.object({ @@ -175,6 +182,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Integrations], description: "Get an integration by integration id", security: [ { @@ -206,6 +215,33 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { id: req.params.integrationId }); + if (integration.region) { + integration.metadata = { + ...(integration.metadata || {}), + region: integration.region + }; + } + + if ( + integration.integration === Integrations.AWS_SECRET_MANAGER || + integration.integration === Integrations.AWS_PARAMETER_STORE + ) { + const awsRoleDetails = await server.services.integration.getIntegrationAWSIamRole({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.integrationId + }); + + if (awsRoleDetails) { + integration.metadata = { + ...(integration.metadata || {}), + awsIamRole: awsRoleDetails.role + }; + } + } + return { integration }; } }); @@ -217,6 +253,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Integrations], description: "Remove an integration using the integration object ID", security: [ { @@ -285,6 +323,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Integrations], description: "Manually trigger sync of an integration by integration id", security: [ { diff --git a/backend/src/server/routes/v1/invite-org-router.ts b/backend/src/server/routes/v1/invite-org-router.ts index 9991f6032..501bebdab 100644 --- a/backend/src/server/routes/v1/invite-org-router.ts +++ b/backend/src/server/routes/v1/invite-org-router.ts @@ -73,6 +73,40 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + url: "/signup-resend", + config: { + rateLimit: inviteUserRateLimit + }, + method: "POST", + schema: { + body: z.object({ + membershipId: z.string() + }), + response: { + 200: z.object({ + signupToken: z + .object({ + email: z.string(), + link: z.string() + }) + .optional() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + return server.services.org.resendOrgMemberInvitation({ + orgId: req.permission.orgId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + membershipId: req.body.membershipId + }); + } + }); + server.route({ url: "/verify", method: "POST", diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index 30d032e13..22314b54b 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -1,21 +1,21 @@ -import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { AuditLogsSchema, GroupsSchema, IncidentContactsSchema, - OrganizationsSchema, OrgMembershipsSchema, OrgRolesSchema, UsersSchema } from "@app/db/schemas"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; -import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs"; -import { getLastMidnightDateISO } from "@app/lib/fn"; +import { ApiDocsTags, AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs"; +import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { GenericResourceNameSchema, slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { ActorType, AuthMode } from "@app/services/auth/auth-type"; +import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type"; +import { sanitizedOrganizationSchema } from "@app/services/org/org-schema"; import { integrationAuthPubSchema } from "../sanitizedSchemas"; @@ -29,9 +29,12 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { schema: { response: { 200: z.object({ - organizations: OrganizationsSchema.extend({ - orgAuthMethod: z.string() - }).array() + organizations: sanitizedOrganizationSchema + .extend({ + orgAuthMethod: z.string(), + userRole: z.string() + }) + .array() }) } }, @@ -54,7 +57,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - organization: OrganizationsSchema + organization: sanitizedOrganizationSchema }) } }, @@ -106,10 +109,20 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.AuditLogs], description: "Get all audit logs for an organization", querystring: z.object({ projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId), + environment: z.string().optional().describe(AUDIT_LOGS.EXPORT.environment), actorType: z.nativeEnum(ActorType).optional(), + secretPath: z + .string() + .optional() + .transform((val) => (!val ? val : removeTrailingSlash(val))) + .describe(AUDIT_LOGS.EXPORT.secretPath), + secretKey: z.string().optional().describe(AUDIT_LOGS.EXPORT.secretKey), + // eventType is split with , for multiple values, we need to transform it to array eventType: z .string() @@ -242,29 +255,20 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { schema: { params: z.object({ organizationId: z.string().trim() }), body: z.object({ - name: z.string().trim().max(64, { message: "Name must be 64 or fewer characters" }).optional(), - slug: z - .string() - .trim() - .max(64, { message: "Slug must be 64 or fewer characters" }) - .regex(/^[a-zA-Z0-9-]+$/, "Slug must only contain alphanumeric characters or hyphens") - .optional(), + name: GenericResourceNameSchema.optional(), + slug: slugSchema({ max: 64 }).optional(), authEnforced: z.boolean().optional(), scimEnabled: z.boolean().optional(), - defaultMembershipRoleSlug: z - .string() - .min(1) - .trim() - .refine((v) => slugify(v) === v, { - message: "Membership role must be a valid slug" - }) - .optional(), - enforceMfa: z.boolean().optional() + defaultMembershipRoleSlug: slugSchema({ max: 64, field: "Default Membership Role" }).optional(), + enforceMfa: z.boolean().optional(), + selectedMfaMethod: z.nativeEnum(MfaMethod).optional(), + allowSecretSharingOutsideOrganization: z.boolean().optional(), + bypassOrgAuthEnabled: z.boolean().optional() }), response: { 200: z.object({ message: z.string(), - organization: OrganizationsSchema + organization: sanitizedOrganizationSchema }) } }, diff --git a/backend/src/server/routes/v1/password-router.ts b/backend/src/server/routes/v1/password-router.ts index 316ddcb53..724468e02 100644 --- a/backend/src/server/routes/v1/password-router.ts +++ b/backend/src/server/routes/v1/password-router.ts @@ -6,6 +6,7 @@ import { authRateLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { validateSignUpAuthorization } from "@app/services/auth/auth-fns"; import { AuthMode } from "@app/services/auth/auth-type"; +import { UserEncryption } from "@app/services/user/user-types"; export const registerPasswordRouter = async (server: FastifyZodProvider) => { server.route({ @@ -113,20 +114,16 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - message: z.string(), user: UsersSchema, - token: z.string() + token: z.string(), + userEncryptionVersion: z.nativeEnum(UserEncryption) }) } }, handler: async (req) => { - const { token, user } = await server.services.password.verifyPasswordResetEmail(req.body.email, req.body.code); + const passwordReset = await server.services.password.verifyPasswordResetEmail(req.body.email, req.body.code); - return { - message: "Successfully verified email", - user, - token - }; + return passwordReset; } }); @@ -203,7 +200,8 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { encryptedPrivateKeyIV: z.string().trim(), encryptedPrivateKeyTag: z.string().trim(), salt: z.string().trim(), - verifier: z.string().trim() + verifier: z.string().trim(), + password: z.string().trim() }), response: { 200: z.object({ @@ -218,7 +216,69 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { userId: token.userId }); - return { message: "Successfully updated backup private key" }; + return { message: "Successfully reset password" }; + } + }); + + server.route({ + method: "POST", + url: "/email/password-setup", + config: { + rateLimit: authRateLimit + }, + schema: { + response: { + 200: z.object({ + message: z.string() + }) + } + }, + handler: async (req) => { + await server.services.password.sendPasswordSetupEmail(req.permission); + + return { + message: "A password setup link has been sent" + }; + } + }); + + server.route({ + method: "POST", + url: "/password-setup", + config: { + rateLimit: authRateLimit + }, + schema: { + body: z.object({ + protectedKey: z.string().trim(), + protectedKeyIV: z.string().trim(), + protectedKeyTag: z.string().trim(), + encryptedPrivateKey: z.string().trim(), + encryptedPrivateKeyIV: z.string().trim(), + encryptedPrivateKeyTag: z.string().trim(), + salt: z.string().trim(), + verifier: z.string().trim(), + password: z.string().trim(), + token: z.string().trim() + }), + response: { + 200: z.object({ + message: z.string() + }) + } + }, + handler: async (req, res) => { + await server.services.password.setupPassword(req.body, req.permission); + + const appCfg = getConfig(); + void res.cookie("jid", "", { + httpOnly: true, + path: "/", + sameSite: "strict", + secure: appCfg.HTTPS_ENABLED + }); + + return { message: "Successfully setup password" }; } }); }; diff --git a/backend/src/server/routes/v1/pki-alert-router.ts b/backend/src/server/routes/v1/pki-alert-router.ts index f64ec9e47..43ce91e88 100644 --- a/backend/src/server/routes/v1/pki-alert-router.ts +++ b/backend/src/server/routes/v1/pki-alert-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { PkiAlertsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { ALERTS } from "@app/lib/api-docs"; +import { ALERTS, ApiDocsTags } from "@app/lib/api-docs"; 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"; @@ -16,6 +16,8 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiAlerting], description: "Create PKI alert", body: z.object({ projectId: z.string().trim().describe(ALERTS.CREATE.projectId), @@ -68,6 +70,8 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiAlerting], description: "Get PKI alert", params: z.object({ alertId: z.string().trim().describe(ALERTS.GET.alertId) @@ -108,6 +112,8 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiAlerting], description: "Update PKI alert", params: z.object({ alertId: z.string().trim().describe(ALERTS.UPDATE.alertId) @@ -164,6 +170,8 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiAlerting], description: "Delete PKI alert", params: z.object({ alertId: z.string().trim().describe(ALERTS.DELETE.alertId) diff --git a/backend/src/server/routes/v1/pki-collection-router.ts b/backend/src/server/routes/v1/pki-collection-router.ts index 2f2add5c1..92f883b38 100644 --- a/backend/src/server/routes/v1/pki-collection-router.ts +++ b/backend/src/server/routes/v1/pki-collection-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { PkiCollectionItemsSchema, PkiCollectionsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { PKI_COLLECTIONS } from "@app/lib/api-docs"; +import { ApiDocsTags, PKI_COLLECTIONS } from "@app/lib/api-docs"; 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"; @@ -17,6 +17,8 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateCollections], description: "Create PKI collection", body: z.object({ projectId: z.string().trim().describe(PKI_COLLECTIONS.CREATE.projectId), @@ -60,6 +62,8 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateCollections], description: "Get PKI collection", params: z.object({ collectionId: z.string().trim().describe(PKI_COLLECTIONS.GET.collectionId) @@ -100,6 +104,8 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateCollections], description: "Update PKI collection", params: z.object({ collectionId: z.string().trim().describe(PKI_COLLECTIONS.UPDATE.collectionId) @@ -146,6 +152,8 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateCollections], description: "Delete PKI collection", params: z.object({ collectionId: z.string().trim().describe(PKI_COLLECTIONS.DELETE.collectionId) @@ -186,6 +194,8 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateCollections], description: "Get items in PKI collection", params: z.object({ collectionId: z.string().trim().describe(PKI_COLLECTIONS.LIST_ITEMS.collectionId) @@ -247,6 +257,8 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateCollections], description: "Add item to PKI collection", params: z.object({ collectionId: z.string().trim().describe(PKI_COLLECTIONS.ADD_ITEM.collectionId) @@ -298,6 +310,8 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateCollections], description: "Remove item from PKI collection", params: z.object({ collectionId: z.string().trim().describe(PKI_COLLECTIONS.DELETE_ITEM.collectionId), diff --git a/backend/src/server/routes/v1/project-env-router.ts b/backend/src/server/routes/v1/project-env-router.ts index c5ded83e4..9a136e160 100644 --- a/backend/src/server/routes/v1/project-env-router.ts +++ b/backend/src/server/routes/v1/project-env-router.ts @@ -1,10 +1,10 @@ -import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { ProjectEnvironmentsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { ENVIRONMENTS } from "@app/lib/api-docs"; +import { ApiDocsTags, ENVIRONMENTS } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -13,9 +13,11 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { method: "GET", url: "/:workspaceId/environments/:envId", config: { - rateLimit: writeLimit + rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Environments], description: "Get Environment", security: [ { @@ -65,6 +67,8 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Environments], description: "Get Environment by ID", security: [ { @@ -112,6 +116,8 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Environments], description: "Create environment", security: [ { @@ -124,13 +130,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { body: z.object({ name: z.string().trim().describe(ENVIRONMENTS.CREATE.name), position: z.number().min(1).optional().describe(ENVIRONMENTS.CREATE.position), - slug: z - .string() - .trim() - .refine((v) => slugify(v) === v, { - message: "Slug must be a valid slug" - }) - .describe(ENVIRONMENTS.CREATE.slug) + slug: slugSchema({ max: 64 }).describe(ENVIRONMENTS.CREATE.slug) }), response: { 200: z.object({ @@ -177,6 +177,8 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Environments], description: "Update environment", security: [ { @@ -188,14 +190,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { id: z.string().trim().describe(ENVIRONMENTS.UPDATE.id) }), body: z.object({ - slug: z - .string() - .trim() - .optional() - .refine((v) => !v || slugify(v) === v, { - message: "Slug must be a valid slug" - }) - .describe(ENVIRONMENTS.UPDATE.slug), + slug: slugSchema({ max: 64 }).optional().describe(ENVIRONMENTS.UPDATE.slug), name: z.string().trim().optional().describe(ENVIRONMENTS.UPDATE.name), position: z.number().optional().describe(ENVIRONMENTS.UPDATE.position) }), @@ -250,6 +245,8 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Environments], description: "Delete environment", security: [ { diff --git a/backend/src/server/routes/v1/project-membership-router.ts b/backend/src/server/routes/v1/project-membership-router.ts index 3c3a1ada4..cd3734efc 100644 --- a/backend/src/server/routes/v1/project-membership-router.ts +++ b/backend/src/server/routes/v1/project-membership-router.ts @@ -1,4 +1,3 @@ -import ms from "ms"; import { z } from "zod"; import { @@ -9,7 +8,8 @@ import { UsersSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { PROJECT_USERS } from "@app/lib/api-docs"; +import { ApiDocsTags, PROJECT_USERS } from "@app/lib/api-docs"; +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"; @@ -23,6 +23,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectUsers], description: "Return project user memberships", security: [ { @@ -141,6 +143,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectUsers], description: "Return project user memberships", security: [ { @@ -255,6 +259,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectUsers], description: "Update project user membership", security: [ { diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index e5e2f636c..19423901b 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -2,18 +2,24 @@ import { z } from "zod"; import { IntegrationsSchema, + ProjectEnvironmentsSchema, ProjectMembershipsSchema, ProjectRolesSchema, ProjectSlackConfigsSchema, + ProjectType, + SecretFoldersSchema, + SortDirection, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { PROJECTS } from "@app/lib/api-docs"; +import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs"; +import { CharacterType, characterValidator } from "@app/lib/validator/validate-string"; +import { re2Validator } from "@app/lib/zod"; 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"; -import { ProjectFilterType } from "@app/services/project/project-types"; +import { ActorType, AuthMode } from "@app/services/auth/auth-type"; +import { ProjectFilterType, SearchProjectSortBy } from "@app/services/project/project-types"; import { validateSlackChannelsField } from "@app/services/slack/slack-auth-validators"; import { integrationAuthPubSchema, SanitizedProjectSchema } from "../sanitizedSchemas"; @@ -135,7 +141,10 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { includeRoles: z .enum(["true", "false"]) .default("false") - .transform((value) => value === "true") + .transform((value) => value === "true"), + type: z + .enum([ProjectType.SecretManager, ProjectType.KMS, ProjectType.CertificateManager, ProjectType.SSH, "all"]) + .optional() }), response: { 200: z.object({ @@ -154,7 +163,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actor: req.permission.type, - actorOrgId: req.permission.orgId + actorOrgId: req.permission.orgId, + type: req.query.type }); return { workspaces }; } @@ -167,6 +177,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Projects], description: "Get project", security: [ { @@ -205,6 +217,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Projects], description: "Delete project", security: [ { @@ -280,6 +294,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Projects], description: "Update project", security: [ { @@ -296,7 +312,24 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { .max(64, { message: "Name must be 64 or fewer characters" }) .optional() .describe(PROJECTS.UPDATE.name), - autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization) + description: z + .string() + .trim() + .max(256, { message: "Description must be 256 or fewer characters" }) + .optional() + .describe(PROJECTS.UPDATE.projectDescription), + autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization), + hasDeleteProtection: z.boolean().optional().describe(PROJECTS.UPDATE.hasDeleteProtection), + slug: z + .string() + .trim() + .max(64, { message: "Slug must be 64 characters or fewer" }) + .refine(re2Validator(/^[a-z0-9]+(?:[_-][a-z0-9]+)*$/), { + message: + "Project slug can only contain lowercase letters and numbers, with optional single hyphens (-) or underscores (_) between words. Cannot start or end with a hyphen or underscore." + }) + .optional() + .describe(PROJECTS.UPDATE.slug) }), response: { 200: z.object({ @@ -313,7 +346,10 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }, update: { name: req.body.name, - autoCapitalization: req.body.autoCapitalization + description: req.body.description, + autoCapitalization: req.body.autoCapitalization, + hasDeleteProtection: req.body.hasDeleteProtection, + slug: req.body.slug }, actorAuthMethod: req.permission.authMethod, actorId: req.permission.id, @@ -363,6 +399,43 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "POST", + url: "/:workspaceId/delete-protection", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + hasDeleteProtection: z.boolean() + }), + response: { + 200: z.object({ + message: z.string(), + workspace: SanitizedProjectSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const workspace = await server.services.project.toggleDeleteProtection({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId, + hasDeleteProtection: req.body.hasDeleteProtection + }); + return { + message: "Successfully changed workspace settings", + workspace + }; + } + }); + server.route({ method: "PUT", url: "/:workspaceSlug/version-limit", @@ -446,6 +519,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Integrations], description: "List integrations for a project.", security: [ { @@ -489,6 +564,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Integrations], description: "List integration auth objects for a workspace.", security: [ { @@ -652,4 +729,134 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { return slackConfig; } }); + + server.route({ + method: "GET", + url: "/:workspaceId/environment-folder-tree", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + workspaceId: z.string().trim() + }), + response: { + 200: z.record( + ProjectEnvironmentsSchema.extend({ folders: SecretFoldersSchema.extend({ path: z.string() }).array() }) + ) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const environmentsFolders = await server.services.folder.getProjectEnvironmentsFolders( + req.params.workspaceId, + req.permission + ); + + return environmentsFolders; + } + }); + + server.route({ + method: "POST", + url: "/search", + config: { + rateLimit: readLimit + }, + schema: { + body: z.object({ + limit: z.number().default(100), + offset: z.number().default(0), + type: z.nativeEnum(ProjectType).optional(), + orderBy: z.nativeEnum(SearchProjectSortBy).optional().default(SearchProjectSortBy.NAME), + orderDirection: z.nativeEnum(SortDirection).optional().default(SortDirection.ASC), + name: z + .string() + .trim() + .refine((val) => characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen])(val), { + message: "Invalid pattern: only alphanumeric characters, - are allowed." + }) + .optional() + }), + response: { + 200: z.object({ + projects: SanitizedProjectSchema.extend({ isMember: z.boolean() }).array(), + totalCount: z.number() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { docs: projects, totalCount } = await server.services.project.searchProjects({ + permission: req.permission, + ...req.body + }); + + return { projects, totalCount }; + } + }); + + server.route({ + method: "POST", + url: "/:workspaceId/project-access", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + comment: z + .string() + .trim() + .max(2500) + .refine( + (val) => + characterValidator([ + CharacterType.AlphaNumeric, + CharacterType.Hyphen, + CharacterType.Comma, + CharacterType.Fullstop, + CharacterType.Spaces, + CharacterType.Exclamation + ])(val), + { + message: "Invalid pattern: only alphanumeric characters, spaces, -.!, are allowed." + } + ) + .optional() + }), + response: { + 200: z.object({ + message: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + await server.services.project.requestProjectAccess({ + permission: req.permission, + comment: req.body.comment, + projectId: req.params.workspaceId + }); + + if (req.auth.actor === ActorType.USER) { + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.params.workspaceId, + event: { + type: EventType.PROJECT_ACCESS_REQUEST, + metadata: { + projectId: req.params.workspaceId, + requesterEmail: req.auth.user.email || req.auth.user.username, + requesterId: req.auth.userId + } + } + }); + } + + return { message: "Project access request has been send to project admins" }; + } + }); }; diff --git a/backend/src/server/routes/v1/secret-folder-router.ts b/backend/src/server/routes/v1/secret-folder-router.ts index 21fc1bd27..b307347b8 100644 --- a/backend/src/server/routes/v1/secret-folder-router.ts +++ b/backend/src/server/routes/v1/secret-folder-router.ts @@ -2,12 +2,15 @@ import { z } from "zod"; import { SecretFoldersSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { FOLDERS } from "@app/lib/api-docs"; +import { ApiDocsTags, FOLDERS } from "@app/lib/api-docs"; import { prefixWithSlash, removeTrailingSlash } from "@app/lib/fn"; +import { isValidFolderName } from "@app/lib/validator"; import { readLimit, secretsLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { booleanSchema } from "../sanitizedSchemas"; + export const registerSecretFolderRouter = async (server: FastifyZodProvider) => { server.route({ url: "/", @@ -16,6 +19,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Folders], description: "Create folders", security: [ { @@ -25,22 +30,31 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => body: z.object({ workspaceId: z.string().trim().describe(FOLDERS.CREATE.workspaceId), environment: z.string().trim().describe(FOLDERS.CREATE.environment), - name: z.string().trim().describe(FOLDERS.CREATE.name), + name: z + .string() + .trim() + .describe(FOLDERS.CREATE.name) + .refine((name) => isValidFolderName(name), { + message: "Invalid folder name. Only alphanumeric characters, dashes, and underscores are allowed." + }), path: z .string() .trim() .default("/") - .transform(prefixWithSlash) + .transform(prefixWithSlash) // Transformations get skipped if path is undefined .transform(removeTrailingSlash) - .describe(FOLDERS.CREATE.path), + .describe(FOLDERS.CREATE.path) + .optional(), // backward compatiability with cli directory: z .string() .trim() .default("/") - .transform(prefixWithSlash) + .transform(prefixWithSlash) // Transformations get skipped if directory is undefined .transform(removeTrailingSlash) .describe(FOLDERS.CREATE.directory) + .optional(), + description: z.string().optional().nullable().describe(FOLDERS.CREATE.description) }), response: { 200: z.object({ @@ -50,7 +64,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const path = req.body.path || req.body.directory; + const path = req.body.path || req.body.directory || "/"; const folder = await server.services.folder.createFolder({ actorId: req.permission.id, actor: req.permission.type, @@ -58,7 +72,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => actorOrgId: req.permission.orgId, ...req.body, projectId: req.body.workspaceId, - path + path, + description: req.body.description }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, @@ -69,7 +84,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => environment: req.body.environment, folderId: folder.id, folderName: folder.name, - folderPath: path + folderPath: path, + ...(req.body.description ? { description: req.body.description } : {}) } } }); @@ -84,6 +100,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Folders], description: "Update folder", security: [ { @@ -97,22 +115,31 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => body: z.object({ workspaceId: z.string().trim().describe(FOLDERS.UPDATE.workspaceId), environment: z.string().trim().describe(FOLDERS.UPDATE.environment), - name: z.string().trim().describe(FOLDERS.UPDATE.name), + name: z + .string() + .trim() + .describe(FOLDERS.UPDATE.name) + .refine((name) => isValidFolderName(name), { + message: "Invalid folder name. Only alphanumeric characters, dashes, and underscores are allowed." + }), path: z .string() .trim() .default("/") - .transform(prefixWithSlash) + .transform(prefixWithSlash) // Transformations get skipped if path is undefined .transform(removeTrailingSlash) - .describe(FOLDERS.UPDATE.path), + .describe(FOLDERS.UPDATE.path) + .optional(), // backward compatiability with cli directory: z .string() .trim() .default("/") - .transform(prefixWithSlash) + .transform(prefixWithSlash) // Transformations get skipped if directory is undefined .transform(removeTrailingSlash) .describe(FOLDERS.UPDATE.directory) + .optional(), + description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description) }), response: { 200: z.object({ @@ -122,7 +149,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const path = req.body.path || req.body.directory; + const path = req.body.path || req.body.directory || "/"; const { folder, old } = await server.services.folder.updateFolder({ actorId: req.permission.id, actor: req.permission.type, @@ -158,6 +185,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Folders], description: "Update folders by batch", security: [ { @@ -170,14 +199,21 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => .object({ id: z.string().describe(FOLDERS.UPDATE.folderId), environment: z.string().trim().describe(FOLDERS.UPDATE.environment), - name: z.string().trim().describe(FOLDERS.UPDATE.name), + name: z + .string() + .trim() + .describe(FOLDERS.UPDATE.name) + .refine((name) => isValidFolderName(name), { + message: "Invalid folder name. Only alphanumeric characters, dashes, and underscores are allowed." + }), path: z .string() .trim() .default("/") .transform(prefixWithSlash) .transform(removeTrailingSlash) - .describe(FOLDERS.UPDATE.path) + .describe(FOLDERS.UPDATE.path), + description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description) }) .array() .min(1) @@ -229,6 +265,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Folders], description: "Delete a folder", security: [ { @@ -245,17 +283,19 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => .string() .trim() .default("/") - .transform(prefixWithSlash) + .transform(prefixWithSlash) // Transformations get skipped if path is undefined .transform(removeTrailingSlash) - .describe(FOLDERS.DELETE.path), + .describe(FOLDERS.DELETE.path) + .optional(), // keep this here as cli need directory directory: z .string() .trim() .default("/") - .transform(prefixWithSlash) + .transform(prefixWithSlash) // Transformations get skipped if directory is undefined .transform(removeTrailingSlash) .describe(FOLDERS.DELETE.directory) + .optional() }), response: { 200: z.object({ @@ -265,7 +305,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const path = req.body.path || req.body.directory; + const path = req.body.path || req.body.directory || "/"; const folder = await server.services.folder.deleteFolder({ actorId: req.permission.id, actor: req.permission.type, @@ -300,6 +340,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Folders], description: "Get folders", security: [ { @@ -309,31 +351,35 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => querystring: z.object({ workspaceId: z.string().trim().describe(FOLDERS.LIST.workspaceId), environment: z.string().trim().describe(FOLDERS.LIST.environment), + lastSecretModified: z.string().datetime().trim().optional().describe(FOLDERS.LIST.lastSecretModified), path: z .string() .trim() - .default("/") - .transform(prefixWithSlash) + .transform(prefixWithSlash) // Transformations get skipped if path is undefined .transform(removeTrailingSlash) - .describe(FOLDERS.LIST.path), + .describe(FOLDERS.LIST.path) + .optional(), // backward compatiability with cli directory: z .string() .trim() - .default("/") - .transform(prefixWithSlash) + .transform(prefixWithSlash) // Transformations get skipped if directory is undefined .transform(removeTrailingSlash) .describe(FOLDERS.LIST.directory) + .optional(), + recursive: booleanSchema.default(false).describe(FOLDERS.LIST.recursive) }), response: { 200: z.object({ - folders: SecretFoldersSchema.array() + folders: SecretFoldersSchema.extend({ + relativePath: z.string().optional() + }).array() }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const path = req.query.path || req.query.directory; + const path = req.query.path || req.query.directory || "/"; const folders = await server.services.folder.getFolders({ actorId: req.permission.id, actor: req.permission.type, @@ -354,6 +400,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Folders], description: "Get folder by id", security: [ { diff --git a/backend/src/server/routes/v1/secret-import-router.ts b/backend/src/server/routes/v1/secret-import-router.ts index aa6efdf36..fca11f8a0 100644 --- a/backend/src/server/routes/v1/secret-import-router.ts +++ b/backend/src/server/routes/v1/secret-import-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { SecretImportsSchema, SecretsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { SECRET_IMPORTS } from "@app/lib/api-docs"; +import { ApiDocsTags, SECRET_IMPORTS } from "@app/lib/api-docs"; import { removeTrailingSlash } from "@app/lib/fn"; import { readLimit, secretsLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -18,6 +18,8 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SecretImports], description: "Create secret imports", security: [ { @@ -83,6 +85,8 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SecretImports], description: "Update secret imports", security: [ { @@ -157,6 +161,8 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SecretImports], description: "Delete secret imports", security: [ { @@ -263,6 +269,8 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SecretImports], description: "Get secret imports", security: [ { @@ -319,6 +327,8 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SecretImports], description: "Get single secret import", security: [ { @@ -421,6 +431,8 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SecretImports], querystring: z.object({ workspaceId: z.string().trim(), environment: z.string().trim(), diff --git a/backend/src/server/routes/v1/secret-requests-router.ts b/backend/src/server/routes/v1/secret-requests-router.ts new file mode 100644 index 000000000..a1e4eafc2 --- /dev/null +++ b/backend/src/server/routes/v1/secret-requests-router.ts @@ -0,0 +1,270 @@ +import { z } from "zod"; + +import { SecretSharingSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { SecretSharingAccessType } from "@app/lib/types"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { SecretSharingType } from "@app/services/secret-sharing/secret-sharing-types"; +import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; + +export const registerSecretRequestsRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/:id", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + id: z.string() + }), + response: { + 200: z.object({ + secretRequest: SecretSharingSchema.omit({ + encryptedSecret: true, + tag: true, + iv: true, + encryptedValue: true + }).extend({ + isSecretValueSet: z.boolean(), + requester: z.object({ + organizationName: z.string(), + firstName: z.string().nullish(), + lastName: z.string().nullish(), + username: z.string() + }) + }) + }) + } + }, + handler: async (req) => { + const secretRequest = await req.server.services.secretSharing.getSecretRequestById({ + id: req.params.id, + actorOrgId: req.permission?.orgId, + actor: req.permission?.type, + actorId: req.permission?.id, + actorAuthMethod: req.permission?.authMethod + }); + + return { secretRequest }; + } + }); + + server.route({ + method: "POST", + url: "/:id/set-value", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + id: z.string() + }), + body: z.object({ + secretValue: z.string() + }), + response: { + 200: z.object({ + secretRequest: SecretSharingSchema.omit({ + encryptedSecret: true, + tag: true, + iv: true, + encryptedValue: true + }) + }) + } + }, + handler: async (req) => { + const secretRequest = await req.server.services.secretSharing.setSecretRequestValue({ + id: req.params.id, + actorOrgId: req.permission?.orgId, + actor: req.permission?.type, + actorId: req.permission?.id, + actorAuthMethod: req.permission?.authMethod, + secretValue: req.body.secretValue + }); + + return { secretRequest }; + } + }); + + server.route({ + method: "POST", + url: "/:id/reveal-value", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + id: z.string() + }), + response: { + 200: z.object({ + secretRequest: SecretSharingSchema.omit({ + encryptedSecret: true, + tag: true, + iv: true, + encryptedValue: true + }).extend({ + secretValue: z.string() + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const secretRequest = await req.server.services.secretSharing.revealSecretRequestValue({ + id: req.params.id, + actorOrgId: req.permission.orgId, + orgId: req.permission.orgId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod + }); + + return { secretRequest }; + } + }); + + server.route({ + method: "DELETE", + url: "/:id", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + id: z.string() + }), + response: { + 200: z.object({ + secretRequest: SecretSharingSchema.omit({ + encryptedSecret: true, + tag: true, + iv: true, + encryptedValue: true + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const secretRequest = await req.server.services.secretSharing.deleteSharedSecretById({ + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actorId: req.permission.id, + sharedSecretId: req.params.id, + orgId: req.permission.orgId, + actor: req.permission.type, + type: SecretSharingType.Request + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SecretRequestDeleted, + distinctId: getTelemetryDistinctId(req), + properties: { + secretRequestId: req.params.id, + organizationId: req.permission.orgId, + ...req.auditLogInfo + } + }); + return { secretRequest }; + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + querystring: z.object({ + offset: z.coerce.number().min(0).max(100).default(0), + limit: z.coerce.number().min(1).max(100).default(25) + }), + response: { + 200: z.object({ + secrets: z.array(SecretSharingSchema), + totalCount: z.number() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { secrets, totalCount } = await req.server.services.secretSharing.getSharedSecrets({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + type: SecretSharingType.Request, + ...req.query + }); + + return { + secrets, + totalCount + }; + } + }); + + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + name: z.string().max(50).optional(), + expiresAt: z.string(), + accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization) + }), + response: { + 200: z.object({ + id: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const shareRequest = await req.server.services.secretSharing.createSecretRequest({ + actor: req.permission.type, + actorId: req.permission.id, + orgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + orgId: req.permission.orgId, + ...req.auditLogInfo, + event: { + type: EventType.CREATE_SECRET_REQUEST, + metadata: { + accessType: req.body.accessType, + name: req.body.name, + id: shareRequest.id + } + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SecretRequestCreated, + distinctId: getTelemetryDistinctId(req), + properties: { + secretRequestId: shareRequest.id, + organizationId: req.permission.orgId, + secretRequestName: req.body.name, + ...req.auditLogInfo + } + }); + + return { id: shareRequest.id }; + } + }); +}; diff --git a/backend/src/server/routes/v1/secret-sharing-router.ts b/backend/src/server/routes/v1/secret-sharing-router.ts index 3363cc6c0..37c8a052f 100644 --- a/backend/src/server/routes/v1/secret-sharing-router.ts +++ b/backend/src/server/routes/v1/secret-sharing-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { SecretSharingSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { SecretSharingAccessType } from "@app/lib/types"; import { publicEndpointLimit, @@ -10,6 +11,7 @@ import { } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { SecretSharingType } from "@app/services/secret-sharing/secret-sharing-types"; export const registerSecretSharingRouter = async (server: FastifyZodProvider) => { server.route({ @@ -37,6 +39,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, + type: SecretSharingType.Share, ...req.query }); @@ -88,6 +91,21 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => orgId: req.permission?.orgId }); + if (sharedSecret.secret?.orgId) { + await server.services.auditLog.createAuditLog({ + orgId: sharedSecret.secret.orgId, + ...req.auditLogInfo, + event: { + type: EventType.READ_SHARED_SECRET, + metadata: { + id: req.params.id, + name: sharedSecret.secret.name || undefined, + accessType: sharedSecret.secret.accessType + } + } + }); + } + return sharedSecret; } }); @@ -151,6 +169,23 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => actorOrgId: req.permission.orgId, ...req.body }); + + await server.services.auditLog.createAuditLog({ + orgId: req.permission.orgId, + ...req.auditLogInfo, + event: { + type: EventType.CREATE_SHARED_SECRET, + metadata: { + accessType: req.body.accessType, + expiresAt: req.body.expiresAt, + expiresAfterViews: req.body.expiresAfterViews, + name: req.body.name, + id: sharedSecret.id, + usingPassword: !!req.body.password + } + } + }); + return { id: sharedSecret.id }; } }); @@ -178,7 +213,20 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => orgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - sharedSecretId + sharedSecretId, + type: SecretSharingType.Share + }); + + await server.services.auditLog.createAuditLog({ + orgId: req.permission.orgId, + ...req.auditLogInfo, + event: { + type: EventType.DELETE_SHARED_SECRET, + metadata: { + id: sharedSecretId, + name: deletedSharedSecret.name || undefined + } + } }); return { ...deletedSharedSecret }; diff --git a/backend/src/server/routes/v1/secret-sync-routers/aws-parameter-store-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/aws-parameter-store-sync-router.ts new file mode 100644 index 000000000..8f02b9e6e --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/aws-parameter-store-sync-router.ts @@ -0,0 +1,17 @@ +import { + AwsParameterStoreSyncSchema, + CreateAwsParameterStoreSyncSchema, + UpdateAwsParameterStoreSyncSchema +} from "@app/services/secret-sync/aws-parameter-store"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerAwsParameterStoreSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.AWSParameterStore, + server, + responseSchema: AwsParameterStoreSyncSchema, + createSchema: CreateAwsParameterStoreSyncSchema, + updateSchema: UpdateAwsParameterStoreSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/aws-secrets-manager-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/aws-secrets-manager-sync-router.ts new file mode 100644 index 000000000..ff2cc5a84 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/aws-secrets-manager-sync-router.ts @@ -0,0 +1,17 @@ +import { + AwsSecretsManagerSyncSchema, + CreateAwsSecretsManagerSyncSchema, + UpdateAwsSecretsManagerSyncSchema +} from "@app/services/secret-sync/aws-secrets-manager"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerAwsSecretsManagerSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.AWSSecretsManager, + server, + responseSchema: AwsSecretsManagerSyncSchema, + createSchema: CreateAwsSecretsManagerSyncSchema, + updateSchema: UpdateAwsSecretsManagerSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/azure-app-configuration-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/azure-app-configuration-sync-router.ts new file mode 100644 index 000000000..de9c0bd8b --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/azure-app-configuration-sync-router.ts @@ -0,0 +1,17 @@ +import { + AzureAppConfigurationSyncSchema, + CreateAzureAppConfigurationSyncSchema, + UpdateAzureAppConfigurationSyncSchema +} from "@app/services/secret-sync/azure-app-configuration"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerAzureAppConfigurationSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.AzureAppConfiguration, + server, + responseSchema: AzureAppConfigurationSyncSchema, + createSchema: CreateAzureAppConfigurationSyncSchema, + updateSchema: UpdateAzureAppConfigurationSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/azure-key-vault-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/azure-key-vault-sync-router.ts new file mode 100644 index 000000000..a33c513c8 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/azure-key-vault-sync-router.ts @@ -0,0 +1,17 @@ +import { + AzureKeyVaultSyncSchema, + CreateAzureKeyVaultSyncSchema, + UpdateAzureKeyVaultSyncSchema +} from "@app/services/secret-sync/azure-key-vault"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerAzureKeyVaultSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.AzureKeyVault, + server, + responseSchema: AzureKeyVaultSyncSchema, + createSchema: CreateAzureKeyVaultSyncSchema, + updateSchema: UpdateAzureKeyVaultSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/camunda-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/camunda-sync-router.ts new file mode 100644 index 000000000..13a8680c8 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/camunda-sync-router.ts @@ -0,0 +1,13 @@ +import { CamundaSyncSchema, CreateCamundaSyncSchema, UpdateCamundaSyncSchema } from "@app/services/secret-sync/camunda"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerCamundaSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.Camunda, + server, + responseSchema: CamundaSyncSchema, + createSchema: CreateCamundaSyncSchema, + updateSchema: UpdateCamundaSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/databricks-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/databricks-sync-router.ts new file mode 100644 index 000000000..b9c424a05 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/databricks-sync-router.ts @@ -0,0 +1,17 @@ +import { + CreateDatabricksSyncSchema, + DatabricksSyncSchema, + UpdateDatabricksSyncSchema +} from "@app/services/secret-sync/databricks"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerDatabricksSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.Databricks, + server, + responseSchema: DatabricksSyncSchema, + createSchema: CreateDatabricksSyncSchema, + updateSchema: UpdateDatabricksSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/gcp-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/gcp-sync-router.ts new file mode 100644 index 000000000..8e4266556 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/gcp-sync-router.ts @@ -0,0 +1,13 @@ +import { CreateGcpSyncSchema, GcpSyncSchema, UpdateGcpSyncSchema } from "@app/services/secret-sync/gcp"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerGcpSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.GCPSecretManager, + server, + responseSchema: GcpSyncSchema, + createSchema: CreateGcpSyncSchema, + updateSchema: UpdateGcpSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/github-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/github-sync-router.ts new file mode 100644 index 000000000..a84d70354 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/github-sync-router.ts @@ -0,0 +1,13 @@ +import { CreateGitHubSyncSchema, GitHubSyncSchema, UpdateGitHubSyncSchema } from "@app/services/secret-sync/github"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerGitHubSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.GitHub, + server, + responseSchema: GitHubSyncSchema, + createSchema: CreateGitHubSyncSchema, + updateSchema: UpdateGitHubSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/humanitec-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/humanitec-sync-router.ts new file mode 100644 index 000000000..4ae64bfc4 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/humanitec-sync-router.ts @@ -0,0 +1,17 @@ +import { + CreateHumanitecSyncSchema, + HumanitecSyncSchema, + UpdateHumanitecSyncSchema +} from "@app/services/secret-sync/humanitec"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerHumanitecSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.Humanitec, + server, + responseSchema: HumanitecSyncSchema, + createSchema: CreateHumanitecSyncSchema, + updateSchema: UpdateHumanitecSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts new file mode 100644 index 000000000..ee407cee3 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -0,0 +1,31 @@ +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-router"; +import { registerAwsSecretsManagerSyncRouter } from "./aws-secrets-manager-sync-router"; +import { registerAzureAppConfigurationSyncRouter } from "./azure-app-configuration-sync-router"; +import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router"; +import { registerCamundaSyncRouter } from "./camunda-sync-router"; +import { registerDatabricksSyncRouter } from "./databricks-sync-router"; +import { registerGcpSyncRouter } from "./gcp-sync-router"; +import { registerGitHubSyncRouter } from "./github-sync-router"; +import { registerHumanitecSyncRouter } from "./humanitec-sync-router"; +import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router"; +import { registerVercelSyncRouter } from "./vercel-sync-router"; +import { registerWindmillSyncRouter } from "./windmill-sync-router"; + +export * from "./secret-sync-router"; + +export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record Promise> = { + [SecretSync.AWSParameterStore]: registerAwsParameterStoreSyncRouter, + [SecretSync.AWSSecretsManager]: registerAwsSecretsManagerSyncRouter, + [SecretSync.GitHub]: registerGitHubSyncRouter, + [SecretSync.GCPSecretManager]: registerGcpSyncRouter, + [SecretSync.AzureKeyVault]: registerAzureKeyVaultSyncRouter, + [SecretSync.AzureAppConfiguration]: registerAzureAppConfigurationSyncRouter, + [SecretSync.Databricks]: registerDatabricksSyncRouter, + [SecretSync.Humanitec]: registerHumanitecSyncRouter, + [SecretSync.TerraformCloud]: registerTerraformCloudSyncRouter, + [SecretSync.Camunda]: registerCamundaSyncRouter, + [SecretSync.Vercel]: registerVercelSyncRouter, + [SecretSync.Windmill]: registerWindmillSyncRouter +}; 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 new file mode 100644 index 000000000..6ab9b5939 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-endpoints.ts @@ -0,0 +1,426 @@ +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags, SecretSyncs } from "@app/lib/api-docs"; +import { startsWithVowel } from "@app/lib/fn"; +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"; +import { SecretSync, SecretSyncImportBehavior } from "@app/services/secret-sync/secret-sync-enums"; +import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps"; +import { TSecretSync, TSecretSyncInput } from "@app/services/secret-sync/secret-sync-types"; + +export const registerSyncSecretsEndpoints = ({ + server, + destination, + createSchema, + updateSchema, + responseSchema +}: { + destination: SecretSync; + server: FastifyZodProvider; + createSchema: z.ZodType<{ + name: string; + environment: string; + secretPath: string; + projectId: string; + connectionId: string; + destinationConfig: I["destinationConfig"]; + syncOptions: I["syncOptions"]; + description?: string | null; + isAutoSyncEnabled?: boolean; + }>; + updateSchema: z.ZodType<{ + connectionId?: string; + name?: string; + environment?: string; + secretPath?: string; + destinationConfig?: I["destinationConfig"]; + syncOptions?: I["syncOptions"]; + description?: string | null; + isAutoSyncEnabled?: boolean; + }>; + responseSchema: z.ZodTypeAny; +}) => { + const destinationName = SECRET_SYNC_NAME_MAP[destination]; + + server.route({ + method: "GET", + url: `/`, + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretSyncs], + description: `List the ${destinationName} Syncs for the specified project.`, + querystring: z.object({ + projectId: z.string().trim().min(1, "Project ID required").describe(SecretSyncs.LIST(destination).projectId) + }), + response: { + 200: z.object({ secretSyncs: responseSchema.array() }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { + query: { projectId } + } = req; + + const secretSyncs = (await server.services.secretSync.listSecretSyncsByProjectId( + { projectId, destination }, + req.permission + )) as T[]; + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.GET_SECRET_SYNCS, + metadata: { + destination, + count: secretSyncs.length, + syncIds: secretSyncs.map((connection) => connection.id) + } + } + }); + + return { secretSyncs }; + } + }); + + server.route({ + method: "GET", + url: "/:syncId", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretSyncs], + description: `Get the specified ${destinationName} Sync by ID.`, + params: z.object({ + syncId: z.string().uuid().describe(SecretSyncs.GET_BY_ID(destination).syncId) + }), + response: { + 200: z.object({ secretSync: responseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { syncId } = req.params; + + const secretSync = (await server.services.secretSync.findSecretSyncById( + { syncId, destination }, + req.permission + )) as T; + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: secretSync.projectId, + event: { + type: EventType.GET_SECRET_SYNC, + metadata: { + syncId, + destination + } + } + }); + + return { secretSync }; + } + }); + + server.route({ + method: "GET", + url: `/sync-name/:syncName`, + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretSyncs], + description: `Get the specified ${destinationName} Sync by name and project ID.`, + params: z.object({ + syncName: z.string().trim().min(1, "Sync name required").describe(SecretSyncs.GET_BY_NAME(destination).syncName) + }), + querystring: z.object({ + projectId: z + .string() + .trim() + .min(1, "Project ID required") + .describe(SecretSyncs.GET_BY_NAME(destination).projectId) + }), + response: { + 200: z.object({ secretSync: responseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { syncName } = req.params; + const { projectId } = req.query; + + const secretSync = (await server.services.secretSync.findSecretSyncByName( + { syncName, projectId, destination }, + req.permission + )) as T; + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.GET_SECRET_SYNC, + metadata: { + syncId: secretSync.id, + destination + } + } + }); + + return { secretSync }; + } + }); + + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretSyncs], + description: `Create ${ + startsWithVowel(destinationName) ? "an" : "a" + } ${destinationName} Sync for the specified project environment.`, + body: createSchema, + response: { + 200: z.object({ secretSync: responseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const secretSync = (await server.services.secretSync.createSecretSync( + { ...req.body, destination }, + req.permission + )) as T; + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: secretSync.projectId, + event: { + type: EventType.CREATE_SECRET_SYNC, + metadata: { + syncId: secretSync.id, + destination, + ...req.body + } + } + }); + + return { secretSync }; + } + }); + + server.route({ + method: "PATCH", + url: "/:syncId", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretSyncs], + description: `Update the specified ${destinationName} Sync.`, + params: z.object({ + syncId: z.string().uuid().describe(SecretSyncs.UPDATE(destination).syncId) + }), + body: updateSchema, + response: { + 200: z.object({ secretSync: responseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { syncId } = req.params; + + const secretSync = (await server.services.secretSync.updateSecretSync( + { ...req.body, syncId, destination }, + req.permission + )) as T; + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: secretSync.projectId, + event: { + type: EventType.UPDATE_SECRET_SYNC, + metadata: { + syncId, + destination, + ...req.body + } + } + }); + + return { secretSync }; + } + }); + + server.route({ + method: "DELETE", + url: `/:syncId`, + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretSyncs], + description: `Delete the specified ${destinationName} Sync.`, + params: z.object({ + syncId: z.string().uuid().describe(SecretSyncs.DELETE(destination).syncId) + }), + querystring: z.object({ + removeSecrets: z + .enum(["true", "false"]) + .default("false") + .transform((value) => value === "true") + .describe(SecretSyncs.DELETE(destination).removeSecrets) + }), + response: { + 200: z.object({ secretSync: responseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { syncId } = req.params; + const { removeSecrets } = req.query; + + const secretSync = (await server.services.secretSync.deleteSecretSync( + { destination, syncId, removeSecrets }, + req.permission + )) as T; + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.DELETE_SECRET_SYNC, + metadata: { + destination, + syncId, + removeSecrets + } + } + }); + + return { secretSync }; + } + }); + + server.route({ + method: "POST", + url: "/:syncId/sync-secrets", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretSyncs], + description: `Trigger a sync for the specified ${destinationName} Sync.`, + params: z.object({ + syncId: z.string().uuid().describe(SecretSyncs.SYNC_SECRETS(destination).syncId) + }), + response: { + 200: z.object({ secretSync: responseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { syncId } = req.params; + + const secretSync = (await server.services.secretSync.triggerSecretSyncSyncSecretsById( + { + syncId, + destination, + auditLogInfo: req.auditLogInfo + }, + req.permission + )) as T; + + return { secretSync }; + } + }); + + server.route({ + method: "POST", + url: "/:syncId/import-secrets", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretSyncs], + description: `Import secrets from the specified ${destinationName} Sync destination.`, + params: z.object({ + syncId: z.string().uuid().describe(SecretSyncs.IMPORT_SECRETS(destination).syncId) + }), + querystring: z.object({ + importBehavior: z + .nativeEnum(SecretSyncImportBehavior) + .describe(SecretSyncs.IMPORT_SECRETS(destination).importBehavior) + }), + response: { + 200: z.object({ secretSync: responseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { syncId } = req.params; + const { importBehavior } = req.query; + + const secretSync = (await server.services.secretSync.triggerSecretSyncImportSecretsById( + { + syncId, + destination, + importBehavior + }, + req.permission + )) as T; + + return { secretSync }; + } + }); + + server.route({ + method: "POST", + url: "/:syncId/remove-secrets", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretSyncs], + description: `Remove previously synced secrets from the specified ${destinationName} Sync destination.`, + params: z.object({ + syncId: z.string().uuid().describe(SecretSyncs.REMOVE_SECRETS(destination).syncId) + }), + response: { + 200: z.object({ secretSync: responseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { syncId } = req.params; + + const secretSync = (await server.services.secretSync.triggerSecretSyncRemoveSecretsById( + { + syncId, + destination + }, + req.permission + )) as T; + + return { secretSync }; + } + }); +}; diff --git a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts new file mode 100644 index 000000000..e5a868b84 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts @@ -0,0 +1,125 @@ +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags, SecretSyncs } from "@app/lib/api-docs"; +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { + AwsParameterStoreSyncListItemSchema, + AwsParameterStoreSyncSchema +} from "@app/services/secret-sync/aws-parameter-store"; +import { + AwsSecretsManagerSyncListItemSchema, + AwsSecretsManagerSyncSchema +} from "@app/services/secret-sync/aws-secrets-manager"; +import { + AzureAppConfigurationSyncListItemSchema, + AzureAppConfigurationSyncSchema +} from "@app/services/secret-sync/azure-app-configuration"; +import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/services/secret-sync/azure-key-vault"; +import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda"; +import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/services/secret-sync/databricks"; +import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp"; +import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github"; +import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec"; +import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud"; +import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel"; +import { WindmillSyncListItemSchema, WindmillSyncSchema } from "@app/services/secret-sync/windmill"; + +const SecretSyncSchema = z.discriminatedUnion("destination", [ + AwsParameterStoreSyncSchema, + AwsSecretsManagerSyncSchema, + GitHubSyncSchema, + GcpSyncSchema, + AzureKeyVaultSyncSchema, + AzureAppConfigurationSyncSchema, + DatabricksSyncSchema, + HumanitecSyncSchema, + TerraformCloudSyncSchema, + CamundaSyncSchema, + VercelSyncSchema, + WindmillSyncSchema +]); + +const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [ + AwsParameterStoreSyncListItemSchema, + AwsSecretsManagerSyncListItemSchema, + GitHubSyncListItemSchema, + GcpSyncListItemSchema, + AzureKeyVaultSyncListItemSchema, + AzureAppConfigurationSyncListItemSchema, + DatabricksSyncListItemSchema, + HumanitecSyncListItemSchema, + TerraformCloudSyncListItemSchema, + CamundaSyncListItemSchema, + VercelSyncListItemSchema, + WindmillSyncListItemSchema +]); + +export const registerSecretSyncRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/options", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretSyncs], + description: "List the available Secret Sync Options.", + response: { + 200: z.object({ + secretSyncOptions: SecretSyncOptionsSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: () => { + const secretSyncOptions = server.services.secretSync.listSecretSyncOptions(); + return { secretSyncOptions }; + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SecretSyncs], + description: "List all the Secret Syncs for the specified project.", + querystring: z.object({ + projectId: z.string().trim().min(1, "Project ID required").describe(SecretSyncs.LIST().projectId) + }), + response: { + 200: z.object({ secretSyncs: SecretSyncSchema.array() }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { + query: { projectId }, + permission + } = req; + + const secretSyncs = await server.services.secretSync.listSecretSyncsByProjectId({ projectId }, permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.GET_SECRET_SYNCS, + metadata: { + syncIds: secretSyncs.map((sync) => sync.id), + count: secretSyncs.length + } + } + }); + + return { secretSyncs }; + } + }); +}; diff --git a/backend/src/server/routes/v1/secret-sync-routers/terraform-cloud-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/terraform-cloud-sync-router.ts new file mode 100644 index 000000000..d52666593 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/terraform-cloud-sync-router.ts @@ -0,0 +1,17 @@ +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + CreateTerraformCloudSyncSchema, + TerraformCloudSyncSchema, + UpdateTerraformCloudSyncSchema +} from "@app/services/secret-sync/terraform-cloud"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerTerraformCloudSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.TerraformCloud, + server, + responseSchema: TerraformCloudSyncSchema, + createSchema: CreateTerraformCloudSyncSchema, + updateSchema: UpdateTerraformCloudSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/vercel-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/vercel-sync-router.ts new file mode 100644 index 000000000..e6e2f40c6 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/vercel-sync-router.ts @@ -0,0 +1,13 @@ +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { CreateVercelSyncSchema, UpdateVercelSyncSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerVercelSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.Vercel, + server, + responseSchema: VercelSyncSchema, + createSchema: CreateVercelSyncSchema, + updateSchema: UpdateVercelSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/windmill-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/windmill-sync-router.ts new file mode 100644 index 000000000..d40a21ef3 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/windmill-sync-router.ts @@ -0,0 +1,17 @@ +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + CreateWindmillSyncSchema, + UpdateWindmillSyncSchema, + WindmillSyncSchema +} from "@app/services/secret-sync/windmill"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerWindmillSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.Windmill, + server, + responseSchema: WindmillSyncSchema, + createSchema: CreateWindmillSyncSchema, + updateSchema: UpdateWindmillSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-tag-router.ts b/backend/src/server/routes/v1/secret-tag-router.ts index 7d696999e..01ba783fe 100644 --- a/backend/src/server/routes/v1/secret-tag-router.ts +++ b/backend/src/server/routes/v1/secret-tag-router.ts @@ -1,9 +1,9 @@ -import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { SecretTagsSchema } from "@app/db/schemas"; -import { SECRET_TAGS } from "@app/lib/api-docs"; +import { ApiDocsTags, SECRET_TAGS } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -15,6 +15,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Folders], params: z.object({ projectId: z.string().trim().describe(SECRET_TAGS.LIST.projectId) }), @@ -44,6 +46,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Folders], params: z.object({ projectId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_ID.projectId), tagId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_ID.tagId) @@ -75,6 +79,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Folders], params: z.object({ projectId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_SLUG.projectId), tagSlug: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_SLUG.tagSlug) @@ -107,18 +113,13 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Folders], params: z.object({ projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId) }), body: z.object({ - slug: z - .string() - .toLowerCase() - .trim() - .describe(SECRET_TAGS.CREATE.slug) - .refine((v) => slugify(v) === v, { - message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens." - }), + slug: slugSchema({ max: 64 }).describe(SECRET_TAGS.CREATE.slug), color: z.string().trim().describe(SECRET_TAGS.CREATE.color) }), response: { @@ -148,19 +149,14 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Folders], params: z.object({ projectId: z.string().trim().describe(SECRET_TAGS.UPDATE.projectId), tagId: z.string().trim().describe(SECRET_TAGS.UPDATE.tagId) }), body: z.object({ - slug: z - .string() - .toLowerCase() - .trim() - .describe(SECRET_TAGS.UPDATE.slug) - .refine((v) => slugify(v) === v, { - message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens." - }), + slug: slugSchema({ max: 64 }).describe(SECRET_TAGS.UPDATE.slug), color: z.string().trim().describe(SECRET_TAGS.UPDATE.color) }), response: { @@ -190,6 +186,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Folders], params: z.object({ projectId: z.string().trim().describe(SECRET_TAGS.DELETE.projectId), tagId: z.string().trim().describe(SECRET_TAGS.DELETE.tagId) diff --git a/backend/src/server/routes/v1/slack-router.ts b/backend/src/server/routes/v1/slack-router.ts index 0601e2d1f..94276a13c 100644 --- a/backend/src/server/routes/v1/slack-router.ts +++ b/backend/src/server/routes/v1/slack-router.ts @@ -1,10 +1,10 @@ -import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { SlackIntegrationsSchema, WorkflowIntegrationsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { getConfig } from "@app/lib/config/env"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -35,12 +35,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => { } ], querystring: z.object({ - slug: z - .string() - .trim() - .refine((v) => slugify(v) === v, { - message: "Slug must be a valid slug" - }), + slug: slugSchema({ max: 64 }), description: z.string().optional() }), response: { @@ -288,13 +283,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => { id: z.string() }), body: z.object({ - slug: z - .string() - .trim() - .refine((v) => slugify(v) === v, { - message: "Slug must be a valid slug" - }) - .optional(), + slug: slugSchema({ max: 64 }).optional(), description: z.string().optional() }), response: { @@ -342,12 +331,8 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => { failureAsync: async () => { return res.redirect(appCfg.SITE_URL as string); }, - successAsync: async (installation) => { - const metadata = JSON.parse(installation.metadata || "") as { - orgId: string; - }; - - return res.redirect(`${appCfg.SITE_URL}/org/${metadata.orgId}/settings?selectedTab=workflow-integrations`); + successAsync: async () => { + return res.redirect(`${appCfg.SITE_URL}/organization/settings?selectedTab=workflow-integrations`); } }); } diff --git a/backend/src/server/routes/v1/sso-router.ts b/backend/src/server/routes/v1/sso-router.ts index 9007ca828..a222ab172 100644 --- a/backend/src/server/routes/v1/sso-router.ts +++ b/backend/src/server/routes/v1/sso-router.ts @@ -8,21 +8,38 @@ import { Authenticator } from "@fastify/passport"; import fastifySession from "@fastify/session"; +import RedisStore from "connect-redis"; import { Strategy as GitHubStrategy } from "passport-github"; import { Strategy as GitLabStrategy } from "passport-gitlab2"; import { Strategy as GoogleStrategy } from "passport-google-oauth20"; import { z } from "zod"; import { getConfig } from "@app/lib/config/env"; -import { NotFoundError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { fetchGithubEmails } from "@app/lib/requests/github"; +import { authRateLimit } from "@app/server/config/rateLimiter"; import { AuthMethod } from "@app/services/auth/auth-type"; +import { OrgAuthMethod } from "@app/services/org/org-types"; export const registerSsoRouter = async (server: FastifyZodProvider) => { const appCfg = getConfig(); + const passport = new Authenticator({ key: "sso", userProperty: "passportUser" }); - await server.register(fastifySession, { secret: appCfg.COOKIE_SECRET_SIGN_KEY }); + const redisStore = new RedisStore({ + client: server.redis, + prefix: "oauth-session:", + ttl: 600 // 10 minutes + }); + + await server.register(fastifySession, { + secret: appCfg.COOKIE_SECRET_SIGN_KEY, + store: redisStore, + cookie: { + secure: appCfg.HTTPS_ENABLED, + sameSite: "lax" // we want cookies to be sent to Infisical in redirects originating from IDP server + } + }); await server.register(passport.initialize()); await server.register(passport.secureSession()); // passport oauth strategy for Google @@ -35,11 +52,15 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { clientID: appCfg.CLIENT_ID_GOOGLE_LOGIN as string, clientSecret: appCfg.CLIENT_SECRET_GOOGLE_LOGIN as string, callbackURL: `${appCfg.SITE_URL}/api/v1/sso/google`, - scope: ["profile", " email"] + scope: ["profile", " email"], + state: true }, // eslint-disable-next-line async (req, _accessToken, _refreshToken, profile, cb) => { try { + // @ts-expect-error this is because this is express type and not fastify + const callbackPort = req.session.get("callbackPort"); + const email = profile?.emails?.[0]?.value; if (!email) throw new NotFoundError({ @@ -52,7 +73,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { firstName: profile?.name?.givenName || "", lastName: profile?.name?.familyName || "", authMethod: AuthMethod.GOOGLE, - callbackPort: req.query.state as string + callbackPort }); cb(null, { isUserCompleted, providerAuthToken }); } catch (error) { @@ -74,19 +95,23 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { clientID: appCfg.CLIENT_ID_GITHUB_LOGIN as string, clientSecret: appCfg.CLIENT_SECRET_GITHUB_LOGIN as string, callbackURL: `${appCfg.SITE_URL}/api/v1/sso/github`, - scope: ["user:email"] + scope: ["user:email"], + // akhilmhdh: because the ts type for this is outdated by the maintainer + state: true as unknown as string }, // eslint-disable-next-line async (req, accessToken, _refreshToken, profile, cb) => { + // @ts-expect-error this is because this is express type and not fastify + const callbackPort = req.session.get("callbackPort"); try { const ghEmails = await fetchGithubEmails(accessToken); const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0]; const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({ email, - firstName: profile.displayName, + firstName: profile.displayName || profile.username || "", lastName: "", authMethod: AuthMethod.GITHUB, - callbackPort: req.query.state as string + callbackPort }); return cb(null, { isUserCompleted, providerAuthToken }); } catch (error) { @@ -110,17 +135,20 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { clientID: appCfg.CLIENT_ID_GITLAB_LOGIN, clientSecret: appCfg.CLIENT_SECRET_GITLAB_LOGIN, callbackURL: `${appCfg.SITE_URL}/api/v1/sso/gitlab`, - baseURL: appCfg.CLIENT_GITLAB_LOGIN_URL + baseURL: appCfg.CLIENT_GITLAB_LOGIN_URL, + state: true }, async (req: any, _accessToken: string, _refreshToken: string, profile: any, cb: any) => { try { + const callbackPort = req.session.get("callbackPort"); + const email = profile.emails[0].value; const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({ email, - firstName: profile.displayName, + firstName: profile.displayName || profile.username || "", lastName: "", authMethod: AuthMethod.GITLAB, - callbackPort: req.query.state as string + callbackPort }); return cb(null, { isUserCompleted, providerAuthToken }); @@ -141,17 +169,24 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { callback_port: z.string().optional() }) }, - preValidation: (req, res) => - ( - passport.authenticate("google", { - scope: ["profile", "email"], - session: false, - state: req.query.callback_port, - authInfo: false - // this is due to zod type difference - // eslint-disable-next-line @typescript-eslint/no-explicit-any - }) as any - )(req, res), + preValidation: [ + async (req, res) => { + const { callback_port: callbackPort } = req.query; + // ensure fresh session state per login attempt + await req.session.regenerate(); + if (callbackPort) { + req.session.set("callbackPort", callbackPort); + } + return ( + passport.authenticate("google", { + scope: ["profile", "email"], + authInfo: false + // this is due to zod type difference + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) as any + )(req, res); + } + ], handler: () => {} }); @@ -164,7 +199,8 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { authInfo: false // this is due to zod type difference }) as never, - handler: (req, res) => { + handler: async (req, res) => { + await req.session.destroy(); if (req.passportUser.isUserCompleted) { return res.redirect( `${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}` @@ -184,18 +220,65 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { callback_port: z.string().optional() }) }, - preValidation: (req, res) => - ( - passport.authenticate("github", { - session: false, - state: req.query.callback_port, - authInfo: false - // this is due to zod type difference - }) as any - )(req, res), + preValidation: [ + async (req, res) => { + const { callback_port: callbackPort } = req.query; + // ensure fresh session state per login attempt + await req.session.regenerate(); + if (callbackPort) { + req.session.set("callbackPort", callbackPort); + } + + return ( + passport.authenticate("github", { + session: false, + authInfo: false + // this is due to zod type difference + }) as any + )(req, res); + } + ], handler: () => {} }); + server.route({ + url: "/redirect/organizations/:orgSlug", + method: "GET", + config: { + rateLimit: authRateLimit + }, + schema: { + params: z.object({ + orgSlug: z.string().trim() + }), + querystring: z.object({ + callback_port: z.string().optional() + }) + }, + handler: async (req, res) => { + const org = await server.services.org.findOrgBySlug(req.params.orgSlug); + if (org.orgAuthMethod === OrgAuthMethod.SAML) { + return res.redirect( + `${appCfg.SITE_URL}/api/v1/sso/redirect/saml2/organizations/${org.slug}?${ + req.query.callback_port ? `callback_port=${req.query.callback_port}` : "" + }` + ); + } + + if (org.orgAuthMethod === OrgAuthMethod.OIDC) { + return res.redirect( + `${appCfg.SITE_URL}/api/v1/sso/oidc/login?orgSlug=${org.slug}${ + req.query.callback_port ? `&callbackPort=${req.query.callback_port}` : "" + }` + ); + } + + throw new BadRequestError({ + message: "The organization does not have any SSO configured." + }); + } + }); + server.route({ url: "/github", method: "GET", @@ -205,7 +288,8 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { authInfo: false // this is due to zod type difference }) as any, - handler: (req, res) => { + handler: async (req, res) => { + await req.session.destroy(); if (req.passportUser.isUserCompleted) { return res.redirect( `${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}` @@ -225,16 +309,25 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { callback_port: z.string().optional() }) }, - preValidation: (req, res) => - ( - passport.authenticate("gitlab", { - session: false, - state: req.query.callback_port, - authInfo: false - // this is due to zod type difference - // eslint-disable-next-line @typescript-eslint/no-explicit-any - }) as any - )(req, res), + preValidation: [ + async (req, res) => { + const { callback_port: callbackPort } = req.query; + // ensure fresh session state per login attempt + await req.session.regenerate(); + if (callbackPort) { + req.session.set("callbackPort", callbackPort); + } + + return ( + passport.authenticate("gitlab", { + session: false, + authInfo: false + // this is due to zod type difference + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) as any + )(req, res); + } + ], handler: () => {} }); @@ -248,7 +341,8 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { // this is due to zod type difference // eslint-disable-next-line @typescript-eslint/no-explicit-any }) as any, - handler: (req, res) => { + handler: async (req, res) => { + await req.session.destroy(); if (req.passportUser.isUserCompleted) { return res.redirect( `${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}` diff --git a/backend/src/server/routes/v1/user-engagement-router.ts b/backend/src/server/routes/v1/user-engagement-router.ts index e3ce6532e..1a13dbc6e 100644 --- a/backend/src/server/routes/v1/user-engagement-router.ts +++ b/backend/src/server/routes/v1/user-engagement-router.ts @@ -21,7 +21,7 @@ export const registerUserEngagementRouter = async (server: FastifyZodProvider) = }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - return server.services.userEngagement.createUserWish(req.permission.id, req.body.text); + return server.services.userEngagement.createUserWish(req.permission.id, req.permission.orgId, req.body.text); } }); }; diff --git a/backend/src/server/routes/v1/user-router.ts b/backend/src/server/routes/v1/user-router.ts index 4e4583196..a97f11be4 100644 --- a/backend/src/server/routes/v1/user-router.ts +++ b/backend/src/server/routes/v1/user-router.ts @@ -169,4 +169,103 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { return groupMemberships; } }); + + server.route({ + method: "GET", + url: "/me/totp", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: z.object({ + isVerified: z.boolean(), + recoveryCodes: z.string().array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + return server.services.totp.getUserTotpConfig({ + userId: req.permission.id + }); + } + }); + + server.route({ + method: "DELETE", + url: "/me/totp", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + return server.services.totp.deleteUserTotpConfig({ + userId: req.permission.id + }); + } + }); + + server.route({ + method: "POST", + url: "/me/totp/register", + config: { + rateLimit: writeLimit + }, + schema: { + response: { + 200: z.object({ + otpUrl: z.string(), + recoveryCodes: z.string().array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT], { + requireOrg: false + }), + handler: async (req) => { + return server.services.totp.registerUserTotp({ + userId: req.permission.id + }); + } + }); + + server.route({ + method: "POST", + url: "/me/totp/verify", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + totp: z.string() + }), + response: { + 200: z.object({}) + } + }, + onRequest: verifyAuth([AuthMode.JWT], { + requireOrg: false + }), + handler: async (req) => { + return server.services.totp.verifyUserTotpConfig({ + userId: req.permission.id, + totp: req.body.totp + }); + } + }); + + server.route({ + method: "POST", + url: "/me/totp/recovery-codes", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + return server.services.totp.createUserTotpRecoveryCodes({ + userId: req.permission.id + }); + } + }); }; diff --git a/backend/src/server/routes/v2/group-project-router.ts b/backend/src/server/routes/v2/group-project-router.ts index cbc54f5ac..5a081a3d9 100644 --- a/backend/src/server/routes/v2/group-project-router.ts +++ b/backend/src/server/routes/v2/group-project-router.ts @@ -1,4 +1,3 @@ -import ms from "ms"; import { z } from "zod"; import { @@ -7,7 +6,8 @@ import { ProjectMembershipRole, ProjectUserMembershipRolesSchema } from "@app/db/schemas"; -import { PROJECTS } from "@app/lib/api-docs"; +import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs"; +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"; @@ -16,12 +16,14 @@ import { ProjectUserMembershipTemporaryMode } from "@app/services/project-member export const registerGroupProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", - url: "/:projectId/groups/:groupId", + url: "/:projectId/groups/:groupIdOrName", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), config: { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectGroups], description: "Add group to project", security: [ { @@ -30,7 +32,7 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => ], params: z.object({ projectId: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.projectId), - groupId: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.groupId) + groupIdOrName: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.groupIdOrName) }), body: z .object({ @@ -76,7 +78,7 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => actorOrgId: req.permission.orgId, roles: req.body.roles || [{ role: req.body.role }], projectId: req.params.projectId, - groupId: req.params.groupId + groupIdOrName: req.params.groupIdOrName }); return { groupMembership }; @@ -88,6 +90,8 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => url: "/:projectId/groups/:groupId", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.ProjectGroups], description: "Update group in project", security: [ { @@ -147,6 +151,8 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectGroups], description: "Remove group from project", security: [ { @@ -185,6 +191,8 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectGroups], description: "Return list of groups in project", security: [ { @@ -243,6 +251,8 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectGroups], description: "Return project group", security: [ { diff --git a/backend/src/server/routes/v2/identity-org-router.ts b/backend/src/server/routes/v2/identity-org-router.ts index 52940eb44..8680a2dca 100644 --- a/backend/src/server/routes/v2/identity-org-router.ts +++ b/backend/src/server/routes/v2/identity-org-router.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas"; -import { ORGANIZATIONS } from "@app/lib/api-docs"; +import { ApiDocsTags, ORGANIZATIONS } from "@app/lib/api-docs"; import { OrderByDirection } from "@app/lib/types"; import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -17,6 +17,8 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.Organizations], description: "Return organization identity memberships", security: [ { diff --git a/backend/src/server/routes/v2/identity-project-router.ts b/backend/src/server/routes/v2/identity-project-router.ts index b2cc3a8e9..9de7ed1aa 100644 --- a/backend/src/server/routes/v2/identity-project-router.ts +++ b/backend/src/server/routes/v2/identity-project-router.ts @@ -1,4 +1,3 @@ -import ms from "ms"; import { z } from "zod"; import { @@ -7,8 +6,9 @@ import { ProjectMembershipRole, ProjectUserMembershipRolesSchema } from "@app/db/schemas"; -import { ORGANIZATIONS, PROJECT_IDENTITIES } from "@app/lib/api-docs"; +import { ApiDocsTags, ORGANIZATIONS, PROJECT_IDENTITIES } from "@app/lib/api-docs"; import { BadRequestError } from "@app/lib/errors"; +import { ms } from "@app/lib/ms"; import { OrderByDirection } from "@app/lib/types"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -27,6 +27,8 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.ProjectIdentities], description: "Create project identity membership", security: [ { @@ -101,6 +103,8 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.ProjectIdentities], description: "Update project identity memberships", security: [ { @@ -170,6 +174,8 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.ProjectIdentities], description: "Delete project identity memberships", security: [ { @@ -207,6 +213,8 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.ProjectIdentities], description: "Return project identity memberships", security: [ { @@ -300,6 +308,8 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.ProjectIdentities], description: "Return project identity membership", security: [ { @@ -351,4 +361,58 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) return { identityMembership }; } }); + + server.route({ + method: "GET", + url: "/identity-memberships/:identityMembershipId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.ProjectIdentities], + params: z.object({ + identityMembershipId: z.string().trim() + }), + response: { + 200: z.object({ + identityMembership: z.object({ + id: z.string(), + identityId: z.string(), + createdAt: z.date(), + updatedAt: z.date(), + roles: z.array( + z.object({ + id: z.string(), + role: z.string(), + customRoleId: z.string().optional().nullable(), + customRoleName: z.string().optional().nullable(), + customRoleSlug: z.string().optional().nullable(), + isTemporary: z.boolean(), + temporaryMode: z.string().optional().nullable(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional() + }) + ), + identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ + authMethods: z.array(z.string()) + }), + project: SanitizedProjectSchema.pick({ name: true, id: true }) + }) + }) + } + }, + handler: async (req) => { + const identityMembership = await server.services.identityProject.getProjectIdentityByMembershipId({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityMembershipId: req.params.identityMembershipId + }); + return { identityMembership }; + } + }); }; diff --git a/backend/src/server/routes/v2/index.ts b/backend/src/server/routes/v2/index.ts index 3d7581a70..cece502da 100644 --- a/backend/src/server/routes/v2/index.ts +++ b/backend/src/server/routes/v2/index.ts @@ -3,6 +3,7 @@ import { registerIdentityOrgRouter } from "./identity-org-router"; import { registerIdentityProjectRouter } from "./identity-project-router"; import { registerMfaRouter } from "./mfa-router"; import { registerOrgRouter } from "./organization-router"; +import { registerPasswordRouter } from "./password-router"; import { registerProjectMembershipRouter } from "./project-membership-router"; import { registerProjectRouter } from "./project-router"; import { registerServiceTokenRouter } from "./service-token-router"; @@ -12,6 +13,7 @@ export const registerV2Routes = async (server: FastifyZodProvider) => { await server.register(registerMfaRouter, { prefix: "/auth" }); await server.register(registerUserRouter, { prefix: "/users" }); await server.register(registerServiceTokenRouter, { prefix: "/service-token" }); + await server.register(registerPasswordRouter, { prefix: "/password" }); await server.register( async (orgRouter) => { await orgRouter.register(registerOrgRouter); diff --git a/backend/src/server/routes/v2/mfa-router.ts b/backend/src/server/routes/v2/mfa-router.ts index 1c685866d..6f28ec34c 100644 --- a/backend/src/server/routes/v2/mfa-router.ts +++ b/backend/src/server/routes/v2/mfa-router.ts @@ -2,8 +2,9 @@ import jwt from "jsonwebtoken"; import { z } from "zod"; import { getConfig } from "@app/lib/config/env"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { mfaRateLimit } from "@app/server/config/rateLimiter"; -import { AuthModeMfaJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type"; +import { AuthModeMfaJwtTokenPayload, AuthTokenType, MfaMethod } from "@app/services/auth/auth-type"; export const registerMfaRouter = async (server: FastifyZodProvider) => { const cfg = getConfig(); @@ -49,6 +50,38 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/mfa/check/totp", + config: { + rateLimit: mfaRateLimit + }, + schema: { + response: { + 200: z.object({ + isVerified: z.boolean() + }) + } + }, + handler: async (req) => { + try { + const totpConfig = await server.services.totp.getUserTotpConfig({ + userId: req.mfa.userId + }); + + return { + isVerified: Boolean(totpConfig) + }; + } catch (error) { + if (error instanceof NotFoundError || error instanceof BadRequestError) { + return { isVerified: false }; + } + + throw error; + } + } + }); + server.route({ url: "/mfa/verify", method: "POST", @@ -57,7 +90,8 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => { }, schema: { body: z.object({ - mfaToken: z.string().trim() + mfaToken: z.string().trim(), + mfaMethod: z.nativeEnum(MfaMethod).optional().default(MfaMethod.EMAIL) }), response: { 200: z.object({ @@ -86,7 +120,8 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => { ip: req.realIp, userId: req.mfa.userId, orgId: req.mfa.orgId, - mfaToken: req.body.mfaToken + mfaToken: req.body.mfaToken, + mfaMethod: req.body.mfaMethod }); void res.setCookie("jid", token.refresh, { diff --git a/backend/src/server/routes/v2/organization-router.ts b/backend/src/server/routes/v2/organization-router.ts index 5d34bc702..504359726 100644 --- a/backend/src/server/routes/v2/organization-router.ts +++ b/backend/src/server/routes/v2/organization-router.ts @@ -1,17 +1,20 @@ import { z } from "zod"; import { - OrganizationsSchema, OrgMembershipsSchema, ProjectMembershipsSchema, ProjectsSchema, + ProjectType, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; -import { ORGANIZATIONS } from "@app/lib/api-docs"; +import { ApiDocsTags, ORGANIZATIONS } from "@app/lib/api-docs"; +import { getConfig } from "@app/lib/config/env"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { GenericResourceNameSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { ActorType, AuthMode } from "@app/services/auth/auth-type"; +import { sanitizedOrganizationSchema } from "@app/services/org/org-schema"; export const registerOrgRouter = async (server: FastifyZodProvider) => { server.route({ @@ -21,6 +24,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Organizations], description: "Return organization user memberships", security: [ { @@ -69,6 +74,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Organizations], description: "Return projects in organization that user is apart of", security: [ { @@ -78,6 +85,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { params: z.object({ organizationId: z.string().trim().describe(ORGANIZATIONS.GET_PROJECTS.organizationId) }), + querystring: z.object({ + type: z.nativeEnum(ProjectType).optional().describe(ORGANIZATIONS.GET_PROJECTS.type) + }), response: { 200: z.object({ workspaces: z @@ -104,7 +114,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, - orgId: req.params.organizationId + orgId: req.params.organizationId, + type: req.query.type }); return { workspaces }; @@ -172,6 +183,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Organizations], description: "Update organization user memberships", security: [ { @@ -222,6 +235,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Organizations], description: "Delete organization user memberships", security: [ { @@ -281,7 +296,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { lastName: true, id: true }).merge(UserEncryptionKeysSchema.pick({ publicKey: true })), - project: ProjectsSchema.pick({ name: true, id: true }), + project: ProjectsSchema.pick({ name: true, id: true, type: true }), roles: z.array( z.object({ id: z.string(), @@ -324,11 +339,11 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { }, schema: { body: z.object({ - name: z.string().trim() + name: GenericResourceNameSchema }), response: { 200: z.object({ - organization: OrganizationsSchema + organization: sanitizedOrganizationSchema }) } }, @@ -358,20 +373,60 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - organization: OrganizationsSchema + organization: sanitizedOrganizationSchema, + accessToken: z.string() }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]), - handler: async (req) => { + handler: async (req, res) => { if (req.auth.actor !== ActorType.USER) return; - const organization = await server.services.org.deleteOrganizationById( - req.permission.id, - req.params.organizationId, - req.permission.authMethod, - req.permission.orgId - ); + const cfg = getConfig(); + + const { organization, tokens } = await server.services.org.deleteOrganizationById({ + userId: req.permission.id, + orgId: req.params.organizationId, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + authorizationHeader: req.headers.authorization, + userAgentHeader: req.headers["user-agent"], + ipAddress: req.realIp + }); + + void res.setCookie("jid", tokens.refreshToken, { + httpOnly: true, + path: "/", + sameSite: "strict", + secure: cfg.HTTPS_ENABLED + }); + + return { organization, accessToken: tokens.accessToken }; + } + }); + + server.route({ + method: "POST", + url: "/privilege-system-upgrade", + config: { + rateLimit: writeLimit + }, + schema: { + response: { + 200: z.object({ + organization: sanitizedOrganizationSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const organization = await server.services.org.upgradePrivilegeSystem({ + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + orgId: req.permission.orgId + }); + return { organization }; } }); diff --git a/backend/src/server/routes/v2/password-router.ts b/backend/src/server/routes/v2/password-router.ts new file mode 100644 index 000000000..63b6d8aac --- /dev/null +++ b/backend/src/server/routes/v2/password-router.ts @@ -0,0 +1,53 @@ +import { z } from "zod"; + +import { authRateLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns"; +import { ResetPasswordV2Type } from "@app/services/auth/auth-password-type"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerPasswordRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/password-reset", + config: { + rateLimit: authRateLimit + }, + schema: { + body: z.object({ + newPassword: z.string().trim() + }) + }, + handler: async (req) => { + const token = validatePasswordResetAuthorization(req.headers.authorization); + await server.services.password.resetPasswordV2({ + type: ResetPasswordV2Type.Recovery, + newPassword: req.body.newPassword, + userId: token.userId + }); + } + }); + + server.route({ + method: "POST", + url: "/user/password-reset", + schema: { + body: z.object({ + oldPassword: z.string().trim(), + newPassword: z.string().trim() + }) + }, + config: { + rateLimit: authRateLimit + }, + onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }), + handler: async (req) => { + await server.services.password.resetPasswordV2({ + type: ResetPasswordV2Type.LoggedInReset, + userId: req.permission.id, + newPassword: req.body.newPassword, + oldPassword: req.body.oldPassword + }); + } + }); +}; diff --git a/backend/src/server/routes/v2/project-membership-router.ts b/backend/src/server/routes/v2/project-membership-router.ts index 4aa03f33c..a1a1cfc96 100644 --- a/backend/src/server/routes/v2/project-membership-router.ts +++ b/backend/src/server/routes/v2/project-membership-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { OrgMembershipRole, ProjectMembershipRole, ProjectMembershipsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { PROJECT_USERS } from "@app/lib/api-docs"; +import { ApiDocsTags, PROJECT_USERS } from "@app/lib/api-docs"; import { writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -15,6 +15,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectUsers], description: "Invite members to project", security: [ { @@ -27,7 +29,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider body: z.object({ emails: z.string().email().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.emails), usernames: z.string().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.usernames), - roleSlugs: z.string().array().optional().describe(PROJECT_USERS.INVITE_MEMBER.roleSlugs) + roleSlugs: z.string().array().min(1).optional().describe(PROJECT_USERS.INVITE_MEMBER.roleSlugs) }), response: { 200: z.object({ @@ -49,7 +51,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider projects: [ { id: req.params.projectId, - projectRoleSlug: [ProjectMembershipRole.Member] + projectRoleSlug: req.body.roleSlugs || [ProjectMembershipRole.Member] } ] }); @@ -78,6 +80,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.ProjectUsers], description: "Remove members from project", security: [ { diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index c2aa446b4..f7540591e 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -1,4 +1,3 @@ -import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { @@ -6,12 +5,18 @@ import { CertificatesSchema, PkiAlertsSchema, PkiCollectionsSchema, - ProjectKeysSchema + ProjectKeysSchema, + ProjectType } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types"; -import { PROJECTS } from "@app/lib/api-docs"; +import { sanitizedSshCa } from "@app/ee/services/ssh/ssh-certificate-authority-schema"; +import { sanitizedSshCertificate } from "@app/ee/services/ssh-certificate/ssh-certificate-schema"; +import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema"; +import { loginMappingSchema, sanitizedSshHost } from "@app/ee/services/ssh-host/ssh-host-schema"; +import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -24,17 +29,10 @@ import { SanitizedProjectSchema } from "../sanitizedSchemas"; const projectWithEnv = SanitizedProjectSchema.extend({ _id: z.string(), - environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array() + environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array(), + kmsSecretManagerKeyId: z.string().nullable().optional() }); -const slugSchema = z - .string() - .min(5) - .max(36) - .refine((v) => slugify(v) === v, { - message: "Slug must be at least 5 character but no more than 36" - }); - export const registerProjectRouter = async (server: FastifyZodProvider) => { /* Get project key */ server.route({ @@ -153,6 +151,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Projects], description: "Create a new project", security: [ { @@ -161,24 +161,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { ], body: z.object({ projectName: z.string().trim().describe(PROJECTS.CREATE.projectName), - slug: z - .string() - .min(5) - .max(36) - .refine((v) => slugify(v) === v, { - message: "Slug must be a valid slug" - }) - .optional() - .describe(PROJECTS.CREATE.slug), + projectDescription: z.string().trim().optional().describe(PROJECTS.CREATE.projectDescription), + slug: slugSchema({ min: 5, max: 36 }).optional().describe(PROJECTS.CREATE.slug), kmsKeyId: z.string().optional(), - template: z - .string() - .refine((v) => slugify(v) === v, { - message: "Template name must be in slug format" - }) + template: slugSchema({ field: "Template Name", max: 64 }) .optional() .default(InfisicalProjectTemplate.Default) - .describe(PROJECTS.CREATE.template) + .describe(PROJECTS.CREATE.template), + type: z.nativeEnum(ProjectType).default(ProjectType.SecretManager) }), response: { 200: z.object({ @@ -194,9 +184,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, workspaceName: req.body.projectName, + workspaceDescription: req.body.projectDescription, slug: req.body.slug, kmsKeyId: req.body.kmsKeyId, - template: req.body.template + template: req.body.template, + type: req.body.type }); await server.services.telemetry.sendPostHogEvents({ @@ -235,6 +227,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Projects], description: "Delete project", security: [ { @@ -242,7 +236,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - slug: slugSchema.describe("The slug of the project to delete.") + slug: slugSchema({ min: 5, max: 36 }).describe("The slug of the project to delete.") }), response: { 200: SanitizedProjectSchema @@ -276,7 +270,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }, schema: { params: z.object({ - slug: slugSchema.describe("The slug of the project to get.") + slug: slugSchema({ min: 5, max: 36 }).describe("The slug of the project to get.") }), response: { 200: projectWithEnv @@ -309,11 +303,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }, schema: { params: z.object({ - slug: slugSchema.describe("The slug of the project to update.") + slug: slugSchema({ min: 5, max: 36 }).describe("The slug of the project to update.") }), body: z.object({ - name: z.string().trim().optional().describe("The new name of the project."), - autoCapitalization: z.boolean().optional().describe("The new auto-capitalization setting.") + name: z.string().trim().optional().describe(PROJECTS.UPDATE.name), + description: z.string().trim().optional().describe(PROJECTS.UPDATE.projectDescription), + autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization), + hasDeleteProtection: z.boolean().optional().describe(PROJECTS.UPDATE.hasDeleteProtection) }), response: { 200: SanitizedProjectSchema @@ -330,7 +326,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }, update: { name: req.body.name, - autoCapitalization: req.body.autoCapitalization + description: req.body.description, + autoCapitalization: req.body.autoCapitalization, + hasDeleteProtection: req.body.hasDeleteProtection }, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -349,8 +347,10 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], params: z.object({ - slug: slugSchema.describe(PROJECTS.LIST_CAS.slug) + slug: slugSchema({ min: 5, max: 36 }).describe(PROJECTS.LIST_CAS.slug) }), querystring: z.object({ status: z.enum([CaStatus.ACTIVE, CaStatus.PENDING_CERTIFICATE]).optional().describe(PROJECTS.LIST_CAS.status), @@ -390,8 +390,10 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], params: z.object({ - slug: slugSchema.describe(PROJECTS.LIST_CERTIFICATES.slug) + slug: slugSchema({ min: 5, max: 36 }).describe(PROJECTS.LIST_CERTIFICATES.slug) }), querystring: z.object({ friendlyName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.friendlyName), @@ -513,4 +515,139 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { return { certificateTemplates }; } }); + + server.route({ + method: "GET", + url: "/:projectId/ssh-certificates", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + projectId: z.string().trim().describe(PROJECTS.LIST_SSH_CAS.projectId) + }), + querystring: z.object({ + offset: z.coerce.number().default(0).describe(PROJECTS.LIST_SSH_CERTIFICATES.offset), + limit: z.coerce.number().default(25).describe(PROJECTS.LIST_SSH_CERTIFICATES.limit) + }), + response: { + 200: z.object({ + certificates: z.array(sanitizedSshCertificate), + totalCount: z.number() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { certificates, totalCount } = await server.services.project.listProjectSshCertificates({ + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + projectId: req.params.projectId, + offset: req.query.offset, + limit: req.query.limit + }); + + return { certificates, totalCount }; + } + }); + + server.route({ + method: "GET", + url: "/:projectId/ssh-certificate-templates", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateTemplates], + params: z.object({ + projectId: z.string().trim().describe(PROJECTS.LIST_SSH_CERTIFICATE_TEMPLATES.projectId) + }), + response: { + 200: z.object({ + certificateTemplates: z.array(sanitizedSshCertificateTemplate) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { certificateTemplates } = await server.services.project.listProjectSshCertificateTemplates({ + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + projectId: req.params.projectId + }); + + return { certificateTemplates }; + } + }); + + server.route({ + method: "GET", + url: "/:projectId/ssh-cas", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SshCertificateAuthorities], + params: z.object({ + projectId: z.string().trim().describe(PROJECTS.LIST_SSH_CAS.projectId) + }), + response: { + 200: z.object({ + cas: z.array(sanitizedSshCa) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const cas = await server.services.project.listProjectSshCas({ + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + projectId: req.params.projectId + }); + + return { cas }; + } + }); + + server.route({ + method: "GET", + url: "/:projectId/ssh-hosts", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + projectId: z.string().trim().describe(PROJECTS.LIST_SSH_HOSTS.projectId) + }), + response: { + 200: z.object({ + hosts: z.array( + sanitizedSshHost.extend({ + loginMappings: z.array(loginMappingSchema) + }) + ) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const hosts = await server.services.project.listProjectSshHosts({ + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + projectId: req.params.projectId + }); + + return { hosts }; + } + }); }; diff --git a/backend/src/server/routes/v2/service-token-router.ts b/backend/src/server/routes/v2/service-token-router.ts index fb10f17db..aa6165d90 100644 --- a/backend/src/server/routes/v2/service-token-router.ts +++ b/backend/src/server/routes/v2/service-token-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { ServiceTokensSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags } from "@app/lib/api-docs"; import { removeTrailingSlash } from "@app/lib/fn"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -25,6 +26,8 @@ export const registerServiceTokenRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.SERVICE_TOKEN]), schema: { + hide: false, + tags: [ApiDocsTags.ServiceTokens], description: "Return Infisical Token data", security: [ { diff --git a/backend/src/server/routes/v2/user-router.ts b/backend/src/server/routes/v2/user-router.ts index 01c7eda6d..851d9c4ff 100644 --- a/backend/src/server/routes/v2/user-router.ts +++ b/backend/src/server/routes/v2/user-router.ts @@ -1,10 +1,11 @@ import { z } from "zod"; -import { AuthTokenSessionsSchema, OrganizationsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; +import { AuthTokenSessionsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; import { ApiKeysSchema } from "@app/db/schemas/api-keys"; import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { AuthMethod, AuthMode } from "@app/services/auth/auth-type"; +import { AuthMethod, AuthMode, MfaMethod } from "@app/services/auth/auth-type"; +import { sanitizedOrganizationSchema } from "@app/services/org/org-schema"; export const registerUserRouter = async (server: FastifyZodProvider) => { server.route({ @@ -56,7 +57,8 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { }, schema: { body: z.object({ - isMfaEnabled: z.boolean() + isMfaEnabled: z.boolean().optional(), + selectedMfaMethod: z.nativeEnum(MfaMethod).optional() }), response: { 200: z.object({ @@ -66,7 +68,12 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { }, preHandler: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]), handler: async (req) => { - const user = await server.services.user.toggleUserMfa(req.permission.id, req.body.isMfaEnabled); + const user = await server.services.user.updateUserMfa({ + userId: req.permission.id, + isMfaEnabled: req.body.isMfaEnabled, + selectedMfaMethod: req.body.selectedMfaMethod + }); + return { user }; } }); @@ -128,7 +135,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { description: "Return organizations that current user is part of", response: { 200: z.object({ - organizations: OrganizationsSchema.array() + organizations: sanitizedOrganizationSchema.array() }) } }, diff --git a/backend/src/server/routes/v3/login-router.ts b/backend/src/server/routes/v3/login-router.ts index 67f8e2c4c..cddfc1c2b 100644 --- a/backend/src/server/routes/v3/login-router.ts +++ b/backend/src/server/routes/v3/login-router.ts @@ -48,7 +48,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ token: z.string(), - isMfaEnabled: z.boolean() + isMfaEnabled: z.boolean(), + mfaMethod: z.string().optional() }) } }, @@ -64,7 +65,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { if (tokens.isMfaEnabled) { return { token: tokens.mfa as string, - isMfaEnabled: true + isMfaEnabled: true, + mfaMethod: tokens.mfaMethod }; } diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index 61981bef5..40aed624b 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -1,27 +1,24 @@ import picomatch from "picomatch"; import { z } from "zod"; -import { - SecretApprovalRequestsSchema, - SecretsSchema, - SecretTagsSchema, - SecretType, - ServiceTokenScopes -} from "@app/db/schemas"; +import { SecretApprovalRequestsSchema, SecretsSchema, SecretType, ServiceTokenScopes } from "@app/db/schemas"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; -import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs"; +import { ApiDocsTags, RAW_SECRETS, SECRETS } from "@app/lib/api-docs"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { removeTrailingSlash } from "@app/lib/fn"; import { secretsLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { BaseSecretNameSchema, SecretNameSchema } from "@app/server/lib/schemas"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getUserAgentType } from "@app/server/plugins/audit-log"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { ActorType, AuthMode } from "@app/services/auth/auth-type"; import { ProjectFilterType } from "@app/services/project/project-types"; +import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; import { SecretOperations, SecretProtectionType } from "@app/services/secret/secret-types"; +import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; -import { secretRawSchema } from "../sanitizedSchemas"; +import { SanitizedTagSchema, secretRawSchema } from "../sanitizedSchemas"; const SecretReferenceNode = z.object({ key: z.string(), @@ -29,6 +26,14 @@ const SecretReferenceNode = z.object({ environment: z.string(), secretPath: z.string() }); + +const convertStringBoolean = (defaultValue: boolean = false) => { + return z + .enum(["true", "false"]) + .default(defaultValue ? "true" : "false") + .transform((value) => value === "true"); +}; + type TSecretReferenceNode = z.infer & { children: TSecretReferenceNode[] }; const SecretReferenceNodeTree: z.ZodType = SecretReferenceNode.extend({ @@ -43,6 +48,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Secrets], description: "Attach tags to a secret", security: [ { @@ -50,7 +57,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - secretName: z.string().trim().describe(SECRETS.ATTACH_TAGS.secretName) + secretName: SecretNameSchema.describe(SECRETS.ATTACH_TAGS.secretName) }), body: z.object({ projectSlug: z.string().trim().describe(SECRETS.ATTACH_TAGS.projectSlug), @@ -66,17 +73,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - secret: SecretsSchema.omit({ secretBlindIndex: true }).merge( - z.object({ - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - }) - ) + secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({ + tags: SanitizedTagSchema.array() + }) }) } }, @@ -106,6 +105,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Secrets], description: "Detach tags from a secret", security: [ { @@ -113,7 +114,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - secretName: z.string().trim().describe(SECRETS.DETACH_TAGS.secretName) + secretName: z.string().describe(SECRETS.DETACH_TAGS.secretName) }), body: z.object({ projectSlug: z.string().trim().describe(SECRETS.DETACH_TAGS.projectSlug), @@ -130,13 +131,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({ - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() + tags: SanitizedTagSchema.array() }) }) } @@ -167,6 +162,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Secrets], description: "List secrets", security: [ { @@ -174,25 +171,74 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } ], querystring: z.object({ + metadataFilter: z + .string() + .optional() + .transform((val) => { + if (!val) return undefined; + + const result: { key?: string; value?: string }[] = []; + const pairs = val.split("|"); + + for (const pair of pairs) { + const keyValuePair: { key?: string; value?: string } = {}; + const parts = pair.split(/[,=]/); + + for (let i = 0; i < parts.length; i += 2) { + const identifier = parts[i].trim().toLowerCase(); + const value = parts[i + 1]?.trim(); + + if (identifier === "key" && value) { + keyValuePair.key = value; + } else if (identifier === "value" && value) { + keyValuePair.value = value; + } + } + + if (keyValuePair.key && keyValuePair.value) { + result.push(keyValuePair); + } + } + + return result.length ? result : undefined; + }) + .superRefine((metadata, ctx) => { + if (metadata && !Array.isArray(metadata)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "Invalid secretMetadata format. Correct format is key=value1,value=value2|key=value3,value=value4." + }); + } + + if (metadata) { + if (metadata.length > 10) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "You can only filter by up to 10 metadata fields" + }); + } + + for (const item of metadata) { + if (!item.key && !item.value) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "Invalid secretMetadata format, key or value must be provided. Correct format is key=value1,value=value2|key=value3,value=value4." + }); + } + } + } + }) + .describe(RAW_SECRETS.LIST.metadataFilter), workspaceId: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceId), workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug), environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment), secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath), - expandSecretReferences: z - .enum(["true", "false"]) - .default("false") - .transform((value) => value === "true") - .describe(RAW_SECRETS.LIST.expand), - recursive: z - .enum(["true", "false"]) - .default("false") - .transform((value) => value === "true") - .describe(RAW_SECRETS.LIST.recursive), - include_imports: z - .enum(["true", "false"]) - .default("false") - .transform((value) => value === "true") - .describe(RAW_SECRETS.LIST.includeImports), + viewSecretValue: convertStringBoolean(true).describe(RAW_SECRETS.LIST.viewSecretValue), + expandSecretReferences: convertStringBoolean().describe(RAW_SECRETS.LIST.expand), + recursive: convertStringBoolean().describe(RAW_SECRETS.LIST.recursive), + include_imports: convertStringBoolean().describe(RAW_SECRETS.LIST.includeImports), tagSlugs: z .string() .describe(RAW_SECRETS.LIST.tagSlugs) @@ -205,14 +251,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secrets: secretRawSchema .extend({ secretPath: z.string().optional(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional() + secretValueHidden: z.boolean(), + secretMetadata: ResourceMetadataSchema.optional(), + tags: SanitizedTagSchema.array().optional() }) .array(), imports: z @@ -220,7 +261,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretPath: z.string(), environment: z.string(), folderId: z.string().optional(), - secrets: secretRawSchema.omit({ createdAt: true, updatedAt: true }).array() + secrets: secretRawSchema + .omit({ createdAt: true, updatedAt: true }) + .extend({ + secretValueHidden: z.boolean(), + secretMetadata: ResourceMetadataSchema.optional() + }) + .array() }) .array() .optional() @@ -267,7 +314,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { expandSecretReferences: req.query.expandSecretReferences, actorAuthMethod: req.permission.authMethod, projectId: workspaceId, + viewSecretValue: req.query.viewSecretValue, path: secretPath, + metadataFilter: req.query.metadataFilter, includeImports: req.query.include_imports, recursive: req.query.recursive, tagSlugs: req.query.tagSlugs @@ -300,10 +349,48 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); } + return { secrets, imports }; } }); + server.route({ + method: "GET", + url: "/raw/id/:secretId", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Secrets], + params: z.object({ + secretId: z.string() + }), + response: { + 200: z.object({ + secret: secretRawSchema.extend({ + secretPath: z.string(), + tags: SanitizedTagSchema.array().optional(), + secretMetadata: ResourceMetadataSchema.optional() + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { secretId } = req.params; + const secret = await server.services.secret.getSecretByIdRaw({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + secretId + }); + + return { secret }; + } + }); + server.route({ method: "GET", url: "/raw/:secretName", @@ -311,6 +398,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Secrets], description: "Get a secret by name", security: [ { @@ -327,28 +416,16 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath), version: z.coerce.number().optional().describe(RAW_SECRETS.GET.version), type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.GET.type), - expandSecretReferences: z - .enum(["true", "false"]) - .default("false") - .transform((value) => value === "true") - .describe(RAW_SECRETS.GET.expand), - include_imports: z - .enum(["true", "false"]) - .default("false") - .transform((value) => value === "true") - .describe(RAW_SECRETS.GET.includeImports) + viewSecretValue: convertStringBoolean(true).describe(RAW_SECRETS.GET.viewSecretValue), + expandSecretReferences: convertStringBoolean().describe(RAW_SECRETS.GET.expand), + include_imports: convertStringBoolean().describe(RAW_SECRETS.GET.includeImports) }), response: { 200: z.object({ secret: secretRawSchema.extend({ - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional() + secretValueHidden: z.boolean(), + tags: SanitizedTagSchema.array().optional(), + secretMetadata: ResourceMetadataSchema.optional() }) }) } @@ -379,6 +456,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { expandSecretReferences: req.query.expandSecretReferences, environment, projectId: workspaceId, + viewSecretValue: req.query.viewSecretValue, projectSlug: workspaceSlug, path: secretPath, secretName: req.params.secretName, @@ -397,7 +475,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretPath: req.query.secretPath, secretId: secret.id, secretKey: req.params.secretName, - secretVersion: secret.version + secretVersion: secret.version, + secretMetadata: secret.secretMetadata } } }); @@ -427,6 +506,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Secrets], description: "Create secret", security: [ { @@ -434,7 +515,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - secretName: z.string().trim().describe(RAW_SECRETS.CREATE.secretName) + secretName: SecretNameSchema.describe(RAW_SECRETS.CREATE.secretName) }), body: z.object({ workspaceId: z.string().trim().describe(RAW_SECRETS.CREATE.workspaceId), @@ -450,6 +531,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { .transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())) .describe(RAW_SECRETS.CREATE.secretValue), secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment), + secretMetadata: ResourceMetadataSchema.optional(), tagIds: z.string().array().optional().describe(RAW_SECRETS.CREATE.tagIds), skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding), type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.CREATE.type), @@ -458,7 +540,12 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { .optional() .nullable() .describe(RAW_SECRETS.CREATE.secretReminderRepeatDays), - secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.CREATE.secretReminderNote) + secretReminderNote: z + .string() + .max(1024, "Secret reminder note cannot exceed 1024 characters") + .optional() + .nullable() + .describe(RAW_SECRETS.CREATE.secretReminderNote) }), response: { 200: z.union([ @@ -484,6 +571,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretValue: req.body.secretValue, skipMultilineEncoding: req.body.skipMultilineEncoding, secretComment: req.body.secretComment, + secretMetadata: req.body.secretMetadata, tagIds: req.body.tagIds, secretReminderNote: req.body.secretReminderNote, secretReminderRepeatDays: req.body.secretReminderRepeatDays @@ -503,7 +591,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretPath: req.body.secretPath, secretId: secret.id, secretKey: req.params.secretName, - secretVersion: secret.version + secretVersion: secret.version, + secretMetadata: req.body.secretMetadata } } }); @@ -532,6 +621,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Secrets], description: "Update secret", security: [ { @@ -539,7 +630,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - secretName: z.string().trim().describe(RAW_SECRETS.UPDATE.secretName) + secretName: BaseSecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName) }), body: z.object({ workspaceId: z.string().trim().describe(RAW_SECRETS.UPDATE.workspaceId), @@ -547,6 +638,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretValue: z .string() .transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())) + .optional() .describe(RAW_SECRETS.UPDATE.secretValue), secretPath: z .string() @@ -558,19 +650,27 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.UPDATE.type), tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds), metadata: z.record(z.string()).optional(), - secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.UPDATE.secretReminderNote), + secretMetadata: ResourceMetadataSchema.optional(), + secretReminderNote: z + .string() + .max(1024, "Secret reminder note cannot exceed 1024 characters") + .optional() + .nullable() + .describe(RAW_SECRETS.UPDATE.secretReminderNote), secretReminderRepeatDays: z .number() .optional() .nullable() .describe(RAW_SECRETS.UPDATE.secretReminderRepeatDays), - newSecretName: z.string().min(1).optional().describe(RAW_SECRETS.UPDATE.newSecretName), + newSecretName: SecretNameSchema.optional().describe(RAW_SECRETS.UPDATE.newSecretName), secretComment: z.string().optional().describe(RAW_SECRETS.UPDATE.secretComment) }), response: { 200: z.union([ z.object({ - secret: secretRawSchema + secret: secretRawSchema.extend({ + secretValueHidden: z.boolean() + }) }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -595,8 +695,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretReminderNote: req.body.secretReminderNote, metadata: req.body.metadata, newSecretName: req.body.newSecretName, - secretComment: req.body.secretComment + secretComment: req.body.secretComment, + secretMetadata: req.body.secretMetadata }); + if (secretOperation.type === SecretProtectionType.Approval) { return { approval: secretOperation.approval }; } @@ -612,7 +714,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretPath: req.body.secretPath, secretId: secret.id, secretKey: req.params.secretName, - secretVersion: secret.version + secretVersion: secret.version, + secretMetadata: req.body.secretMetadata } } }); @@ -640,6 +743,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Secrets], description: "Delete secret", security: [ { @@ -647,7 +752,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - secretName: z.string().trim().describe(RAW_SECRETS.DELETE.secretName) + secretName: z.string().min(1).describe(RAW_SECRETS.DELETE.secretName) }), body: z.object({ workspaceId: z.string().trim().describe(RAW_SECRETS.DELETE.workspaceId), @@ -663,7 +768,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secret: secretRawSchema + secret: secretRawSchema.extend({ + secretValueHidden: z.boolean() + }) }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -685,6 +792,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { if (secretOperation.type === SecretProtectionType.Approval) { return { approval: secretOperation.approval }; } + const { secret } = secretOperation; await server.services.auditLog.createAuditLog({ @@ -747,13 +855,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { workspace: z.string(), environment: z.string(), secretPath: z.string().optional(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() + tags: SanitizedTagSchema.array() }) .array(), imports: z @@ -849,10 +951,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretPath: z.string().trim().default("/").transform(removeTrailingSlash), type: z.nativeEnum(SecretType).default(SecretType.Shared), version: z.coerce.number().optional(), - include_imports: z - .enum(["true", "false"]) - .default("false") - .transform((value) => value === "true") + include_imports: convertStringBoolean() }), response: { 200: z.object({ @@ -1123,6 +1222,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { z.object({ secret: SecretsSchema.omit({ secretBlindIndex: true }).merge( z.object({ + secretValueHidden: z.boolean(), _id: z.string(), workspace: z.string(), environment: z.string() @@ -1292,13 +1392,12 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secret: SecretsSchema.omit({ secretBlindIndex: true }).merge( - z.object({ - _id: z.string(), - workspace: z.string(), - environment: z.string() - }) - ) + secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({ + _id: z.string(), + secretValueHidden: z.boolean(), + workspace: z.string(), + environment: z.string() + }) }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -1403,6 +1502,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Secrets], body: z.object({ projectSlug: z.string().trim(), sourceEnvironment: z.string().trim(), @@ -1610,7 +1711,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secrets: SecretsSchema.omit({ secretBlindIndex: true }).array() + secrets: SecretsSchema.omit({ secretBlindIndex: true }).extend({ secretValueHidden: z.boolean() }).array() }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -1725,7 +1826,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secrets: SecretsSchema.omit({ secretBlindIndex: true }).array() + secrets: SecretsSchema.omit({ secretBlindIndex: true }) + .extend({ + secretValueHidden: z.boolean() + }) + .array() }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -1824,6 +1929,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Secrets], description: "Create many secrets", security: [ { @@ -1842,7 +1949,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { .describe(RAW_SECRETS.CREATE.secretPath), secrets: z .object({ - secretKey: z.string().trim().describe(RAW_SECRETS.CREATE.secretName), + secretKey: SecretNameSchema.describe(RAW_SECRETS.CREATE.secretName), secretValue: z .string() .transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())) @@ -1850,6 +1957,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment), skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding), metadata: z.record(z.string()).optional(), + secretMetadata: ResourceMetadataSchema.optional(), tagIds: z.string().array().optional().describe(RAW_SECRETS.CREATE.tagIds) }) .array() @@ -1884,6 +1992,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } const { secrets } = secretOperation; + const secretMetadataMap = new Map( + inputSecrets.map(({ secretKey, secretMetadata }) => [secretKey, secretMetadata]) + ); + await server.services.auditLog.createAuditLog({ projectId: secrets[0].workspace, ...req.auditLogInfo, @@ -1895,7 +2007,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secrets: secrets.map((secret) => ({ secretId: secret.id, secretKey: secret.secretKey, - secretVersion: secret.version + secretVersion: secret.version, + secretMetadata: secretMetadataMap.get(secret.secretKey) })) } } @@ -1924,6 +2037,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Secrets], description: "Update many secrets", security: [ { @@ -1940,18 +2055,36 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { .default("/") .transform(removeTrailingSlash) .describe(RAW_SECRETS.UPDATE.secretPath), + mode: z + .nativeEnum(SecretUpdateMode) + .optional() + .default(SecretUpdateMode.FailOnNotFound) + .describe(RAW_SECRETS.UPDATE.mode), secrets: z .object({ - secretKey: z.string().trim().describe(RAW_SECRETS.UPDATE.secretName), + secretKey: SecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName), secretValue: z .string() .transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())) + .optional() .describe(RAW_SECRETS.UPDATE.secretValue), + secretPath: z + .string() + .trim() + .transform(removeTrailingSlash) + .optional() + .describe(RAW_SECRETS.UPDATE.secretPath), secretComment: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.secretComment), skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding), - newSecretName: z.string().min(1).optional().describe(RAW_SECRETS.UPDATE.newSecretName), + newSecretName: SecretNameSchema.optional().describe(RAW_SECRETS.UPDATE.newSecretName), tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds), - secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.UPDATE.secretReminderNote), + secretReminderNote: z + .string() + .max(1024, "Secret reminder note cannot exceed 1024 characters") + .optional() + .nullable() + .describe(RAW_SECRETS.UPDATE.secretReminderNote), + secretMetadata: ResourceMetadataSchema.optional(), secretReminderRepeatDays: z .number() .optional() @@ -1964,7 +2097,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secrets: secretRawSchema.array() + secrets: secretRawSchema.extend({ secretValueHidden: z.boolean() }).array() }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -1982,13 +2115,18 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { environment, projectSlug, projectId: req.body.workspaceId, - secrets: inputSecrets + secrets: inputSecrets, + mode: req.body.mode }); if (secretOperation.type === SecretProtectionType.Approval) { return { approval: secretOperation.approval }; } const { secrets } = secretOperation; + const secretMetadataMap = new Map( + inputSecrets.map(({ secretKey, secretMetadata }) => [secretKey, secretMetadata]) + ); + await server.services.auditLog.createAuditLog({ projectId: secrets[0].workspace, ...req.auditLogInfo, @@ -1997,14 +2135,39 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { metadata: { environment: req.body.environment, secretPath: req.body.secretPath, - secrets: secrets.map((secret) => ({ - secretId: secret.id, - secretKey: secret.secretKey, - secretVersion: secret.version - })) + secrets: secrets + .filter((el) => el.version > 1) + .map((secret) => ({ + secretId: secret.id, + secretPath: secret.secretPath, + secretKey: secret.secretKey, + secretVersion: secret.version, + secretMetadata: secretMetadataMap.get(secret.secretKey) + })) } } }); + const createdSecrets = secrets.filter((el) => el.version === 1); + if (createdSecrets.length) { + await server.services.auditLog.createAuditLog({ + projectId: secrets[0].workspace, + ...req.auditLogInfo, + event: { + type: EventType.CREATE_SECRETS, + metadata: { + environment: req.body.environment, + secretPath: req.body.secretPath, + secrets: createdSecrets.map((secret) => ({ + secretId: secret.id, + secretPath: secret.secretPath, + secretKey: secret.secretKey, + secretVersion: secret.version, + secretMetadata: secretMetadataMap.get(secret.secretKey) + })) + } + } + }); + } await server.services.telemetry.sendPostHogEvents({ event: PostHogEventTypes.SecretUpdated, @@ -2029,6 +2192,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Secrets], description: "Delete many secrets", security: [ { @@ -2047,7 +2212,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { .describe(RAW_SECRETS.DELETE.secretPath), secrets: z .object({ - secretKey: z.string().trim().describe(RAW_SECRETS.DELETE.secretName), + secretKey: z.string().describe(RAW_SECRETS.DELETE.secretName), type: z.nativeEnum(SecretType).default(SecretType.Shared) }) .array() @@ -2056,7 +2221,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.union([ z.object({ - secrets: secretRawSchema.array() + secrets: secretRawSchema + .extend({ + secretValueHidden: z.boolean() + }) + .array() }), z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") ]) @@ -2121,6 +2290,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { rateLimit: secretsLimit }, schema: { + hide: false, + tags: [ApiDocsTags.Secrets], description: "Get secret reference tree", security: [ { diff --git a/backend/src/server/routes/v3/signup-router.ts b/backend/src/server/routes/v3/signup-router.ts index d801e85ef..d9196dc88 100644 --- a/backend/src/server/routes/v3/signup-router.ts +++ b/backend/src/server/routes/v3/signup-router.ts @@ -4,6 +4,7 @@ import { UsersSchema } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { ForbiddenRequestError } from "@app/lib/errors"; import { authRateLimit } from "@app/server/config/rateLimiter"; +import { GenericResourceNameSchema } from "@app/server/lib/schemas"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; @@ -100,7 +101,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { encryptedPrivateKeyTag: z.string().trim(), salt: z.string().trim(), verifier: z.string().trim(), - organizationName: z.string().trim().min(1), + organizationName: GenericResourceNameSchema, providerAuthToken: z.string().trim().optional().nullish(), attributionSource: z.string().trim().optional(), password: z.string() @@ -119,13 +120,6 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { if (!userAgent) throw new Error("user agent header is required"); const appCfg = getConfig(); - const serverCfg = await getServerCfg(); - if (!serverCfg.allowSignUp) { - throw new ForbiddenRequestError({ - message: "Signup's are disabled" - }); - } - const { user, accessToken, refreshToken, organizationId } = await server.services.signup.completeEmailAccountSignup({ ...req.body, diff --git a/backend/src/services/app-connection/app-connection-dal.ts b/backend/src/services/app-connection/app-connection-dal.ts new file mode 100644 index 000000000..f74f7cf06 --- /dev/null +++ b/backend/src/services/app-connection/app-connection-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TAppConnectionDALFactory = ReturnType; + +export const appConnectionDALFactory = (db: TDbClient) => { + const appConnectionOrm = ormify(db, TableName.AppConnection); + + return { ...appConnectionOrm }; +}; diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts new file mode 100644 index 000000000..6b6048f2a --- /dev/null +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -0,0 +1,63 @@ +export enum AppConnection { + GitHub = "github", + AWS = "aws", + Databricks = "databricks", + GCP = "gcp", + AzureKeyVault = "azure-key-vault", + AzureAppConfiguration = "azure-app-configuration", + Humanitec = "humanitec", + TerraformCloud = "terraform-cloud", + Vercel = "vercel", + Postgres = "postgres", + MsSql = "mssql", + Camunda = "camunda", + Windmill = "windmill", + Auth0 = "auth0" +} + +export enum AWSRegion { + // US + US_EAST_1 = "us-east-1", // N. Virginia + US_EAST_2 = "us-east-2", // Ohio + US_WEST_1 = "us-west-1", // N. California + US_WEST_2 = "us-west-2", // Oregon + + // GovCloud + US_GOV_EAST_1 = "us-gov-east-1", // US-East + US_GOV_WEST_1 = "us-gov-west-1", // US-West + + // Africa + AF_SOUTH_1 = "af-south-1", // Cape Town + + // Asia Pacific + AP_EAST_1 = "ap-east-1", // Hong Kong + AP_SOUTH_1 = "ap-south-1", // Mumbai + AP_SOUTH_2 = "ap-south-2", // Hyderabad + AP_NORTHEAST_1 = "ap-northeast-1", // Tokyo + AP_NORTHEAST_2 = "ap-northeast-2", // Seoul + AP_NORTHEAST_3 = "ap-northeast-3", // Osaka + AP_SOUTHEAST_1 = "ap-southeast-1", // Singapore + AP_SOUTHEAST_2 = "ap-southeast-2", // Sydney + AP_SOUTHEAST_3 = "ap-southeast-3", // Jakarta + AP_SOUTHEAST_4 = "ap-southeast-4", // Melbourne + + // Canada + CA_CENTRAL_1 = "ca-central-1", // Central + + // Europe + EU_CENTRAL_1 = "eu-central-1", // Frankfurt + EU_CENTRAL_2 = "eu-central-2", // Zurich + EU_WEST_1 = "eu-west-1", // Ireland + EU_WEST_2 = "eu-west-2", // London + EU_WEST_3 = "eu-west-3", // Paris + EU_SOUTH_1 = "eu-south-1", // Milan + EU_SOUTH_2 = "eu-south-2", // Spain + EU_NORTH_1 = "eu-north-1", // Stockholm + + // Middle East + ME_SOUTH_1 = "me-south-1", // Bahrain + ME_CENTRAL_1 = "me-central-1", // UAE + + // South America + SA_EAST_1 = "sa-east-1" // Sao Paulo +} diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts new file mode 100644 index 000000000..7e08a92b4 --- /dev/null +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -0,0 +1,218 @@ +import { TAppConnections } from "@app/db/schemas/app-connections"; +import { generateHash } from "@app/lib/crypto/encryption"; +import { BadRequestError } from "@app/lib/errors"; +import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps"; +import { + transferSqlConnectionCredentialsToPlatform, + validateSqlConnectionCredentials +} from "@app/services/app-connection/shared/sql"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { AppConnection } from "./app-connection-enums"; +import { TAppConnectionServiceFactoryDep } from "./app-connection-service"; +import { + TAppConnection, + TAppConnectionConfig, + TAppConnectionCredentialsValidator, + TAppConnectionTransitionCredentialsToPlatform +} from "./app-connection-types"; +import { Auth0ConnectionMethod, getAuth0ConnectionListItem, validateAuth0ConnectionCredentials } from "./auth0"; +import { AwsConnectionMethod, getAwsConnectionListItem, validateAwsConnectionCredentials } from "./aws"; +import { + AzureAppConfigurationConnectionMethod, + getAzureAppConfigurationConnectionListItem, + validateAzureAppConfigurationConnectionCredentials +} from "./azure-app-configuration"; +import { + AzureKeyVaultConnectionMethod, + getAzureKeyVaultConnectionListItem, + validateAzureKeyVaultConnectionCredentials +} from "./azure-key-vault"; +import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda"; +import { + DatabricksConnectionMethod, + getDatabricksConnectionListItem, + validateDatabricksConnectionCredentials +} from "./databricks"; +import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp"; +import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github"; +import { + getHumanitecConnectionListItem, + HumanitecConnectionMethod, + validateHumanitecConnectionCredentials +} from "./humanitec"; +import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql"; +import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres"; +import { + getTerraformCloudConnectionListItem, + TerraformCloudConnectionMethod, + validateTerraformCloudConnectionCredentials +} from "./terraform-cloud"; +import { VercelConnectionMethod } from "./vercel"; +import { getVercelConnectionListItem, validateVercelConnectionCredentials } from "./vercel/vercel-connection-fns"; +import { + getWindmillConnectionListItem, + validateWindmillConnectionCredentials, + WindmillConnectionMethod +} from "./windmill"; + +export const listAppConnectionOptions = () => { + return [ + getAwsConnectionListItem(), + getGitHubConnectionListItem(), + getGcpConnectionListItem(), + getAzureKeyVaultConnectionListItem(), + getAzureAppConfigurationConnectionListItem(), + getDatabricksConnectionListItem(), + getHumanitecConnectionListItem(), + getTerraformCloudConnectionListItem(), + getVercelConnectionListItem(), + getPostgresConnectionListItem(), + getMsSqlConnectionListItem(), + getCamundaConnectionListItem(), + getWindmillConnectionListItem(), + getAuth0ConnectionListItem() + ].sort((a, b) => a.name.localeCompare(b.name)); +}; + +export const encryptAppConnectionCredentials = async ({ + orgId, + credentials, + kmsService +}: { + orgId: string; + credentials: TAppConnection["credentials"]; + kmsService: TAppConnectionServiceFactoryDep["kmsService"]; +}) => { + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId + }); + + const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({ + plainText: Buffer.from(JSON.stringify(credentials)) + }); + + return encryptedCredentialsBlob; +}; + +export const decryptAppConnectionCredentials = async ({ + orgId, + encryptedCredentials, + kmsService +}: { + orgId: string; + encryptedCredentials: Buffer; + kmsService: TAppConnectionServiceFactoryDep["kmsService"]; +}) => { + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId + }); + + const decryptedPlainTextBlob = decryptor({ + cipherTextBlob: encryptedCredentials + }); + + return JSON.parse(decryptedPlainTextBlob.toString()) as TAppConnection["credentials"]; +}; + +export const validateAppConnectionCredentials = async ( + appConnection: TAppConnectionConfig +): Promise => { + const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record = { + [AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Databricks]: validateDatabricksConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.GitHub]: validateGitHubConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.GCP]: validateGcpConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.AzureKeyVault]: validateAzureKeyVaultConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.AzureAppConfiguration]: + validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Camunda]: validateCamundaConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator + }; + + return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); +}; + +export const getAppConnectionMethodName = (method: TAppConnection["method"]) => { + switch (method) { + case GitHubConnectionMethod.App: + return "GitHub App"; + case AzureKeyVaultConnectionMethod.OAuth: + case AzureAppConfigurationConnectionMethod.OAuth: + case GitHubConnectionMethod.OAuth: + return "OAuth"; + case AwsConnectionMethod.AccessKey: + return "Access Key"; + case AwsConnectionMethod.AssumeRole: + return "Assume Role"; + case GcpConnectionMethod.ServiceAccountImpersonation: + return "Service Account Impersonation"; + case DatabricksConnectionMethod.ServicePrincipal: + return "Service Principal"; + case CamundaConnectionMethod.ClientCredentials: + return "Client Credentials"; + case HumanitecConnectionMethod.ApiToken: + case TerraformCloudConnectionMethod.ApiToken: + case VercelConnectionMethod.ApiToken: + return "API Token"; + case PostgresConnectionMethod.UsernameAndPassword: + case MsSqlConnectionMethod.UsernameAndPassword: + return "Username & Password"; + case WindmillConnectionMethod.AccessToken: + return "Access Token"; + case Auth0ConnectionMethod.ClientCredentials: + return "Client Credentials"; + default: + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new Error(`Unhandled App Connection Method: ${method}`); + } +}; + +export const decryptAppConnection = async ( + appConnection: TAppConnections, + kmsService: TAppConnectionServiceFactoryDep["kmsService"] +) => { + return { + ...appConnection, + credentials: await decryptAppConnectionCredentials({ + encryptedCredentials: appConnection.encryptedCredentials, + orgId: appConnection.orgId, + kmsService + }), + credentialsHash: generateHash(appConnection.encryptedCredentials) + } as TAppConnection; +}; + +const platformManagedCredentialsNotSupported: TAppConnectionTransitionCredentialsToPlatform = ({ app }) => { + throw new BadRequestError({ + message: `${APP_CONNECTION_NAME_MAP[app]} Connections do not support platform managed credentials.` + }); +}; + +export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< + AppConnection, + TAppConnectionTransitionCredentialsToPlatform +> = { + [AppConnection.AWS]: platformManagedCredentialsNotSupported, + [AppConnection.Databricks]: platformManagedCredentialsNotSupported, + [AppConnection.GitHub]: platformManagedCredentialsNotSupported, + [AppConnection.GCP]: platformManagedCredentialsNotSupported, + [AppConnection.AzureKeyVault]: platformManagedCredentialsNotSupported, + [AppConnection.AzureAppConfiguration]: platformManagedCredentialsNotSupported, + [AppConnection.Humanitec]: platformManagedCredentialsNotSupported, + [AppConnection.Postgres]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform, + [AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform, + [AppConnection.TerraformCloud]: platformManagedCredentialsNotSupported, + [AppConnection.Camunda]: platformManagedCredentialsNotSupported, + [AppConnection.Vercel]: platformManagedCredentialsNotSupported, + [AppConnection.Windmill]: platformManagedCredentialsNotSupported, + [AppConnection.Auth0]: platformManagedCredentialsNotSupported +}; diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts new file mode 100644 index 000000000..762a9bcf2 --- /dev/null +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -0,0 +1,18 @@ +import { AppConnection } from "./app-connection-enums"; + +export const APP_CONNECTION_NAME_MAP: Record = { + [AppConnection.AWS]: "AWS", + [AppConnection.GitHub]: "GitHub", + [AppConnection.GCP]: "GCP", + [AppConnection.AzureKeyVault]: "Azure Key Vault", + [AppConnection.AzureAppConfiguration]: "Azure App Configuration", + [AppConnection.Databricks]: "Databricks", + [AppConnection.Humanitec]: "Humanitec", + [AppConnection.TerraformCloud]: "Terraform Cloud", + [AppConnection.Vercel]: "Vercel", + [AppConnection.Postgres]: "PostgreSQL", + [AppConnection.MsSql]: "Microsoft SQL Server", + [AppConnection.Camunda]: "Camunda", + [AppConnection.Windmill]: "Windmill", + [AppConnection.Auth0]: "Auth0" +}; diff --git a/backend/src/services/app-connection/app-connection-schemas.ts b/backend/src/services/app-connection/app-connection-schemas.ts new file mode 100644 index 000000000..0d3968637 --- /dev/null +++ b/backend/src/services/app-connection/app-connection-schemas.ts @@ -0,0 +1,51 @@ +import { z } from "zod"; + +import { AppConnectionsSchema } from "@app/db/schemas/app-connections"; +import { AppConnections } from "@app/lib/api-docs"; +import { slugSchema } from "@app/server/lib/schemas"; +import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps"; +import { TAppConnectionBaseConfig } from "@app/services/app-connection/app-connection-types"; + +import { AppConnection } from "./app-connection-enums"; + +export const BaseAppConnectionSchema = AppConnectionsSchema.omit({ + encryptedCredentials: true, + app: true, + method: true +}).extend({ + credentialsHash: z.string().optional() +}); + +export const GenericCreateAppConnectionFieldsSchema = ( + app: AppConnection, + { supportsPlatformManagedCredentials = false }: TAppConnectionBaseConfig = {} +) => + z.object({ + name: slugSchema({ field: "name" }).describe(AppConnections.CREATE(app).name), + description: z + .string() + .trim() + .max(256, "Description cannot exceed 256 characters") + .nullish() + .describe(AppConnections.CREATE(app).description), + isPlatformManagedCredentials: supportsPlatformManagedCredentials + ? z.boolean().optional().default(false).describe(AppConnections.CREATE(app).isPlatformManagedCredentials) + : z.literal(false).optional().describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`) + }); + +export const GenericUpdateAppConnectionFieldsSchema = ( + app: AppConnection, + { supportsPlatformManagedCredentials = false }: TAppConnectionBaseConfig = {} +) => + z.object({ + name: slugSchema({ field: "name" }).describe(AppConnections.UPDATE(app).name).optional(), + description: z + .string() + .trim() + .max(256, "Description cannot exceed 256 characters") + .nullish() + .describe(AppConnections.UPDATE(app).description), + isPlatformManagedCredentials: supportsPlatformManagedCredentials + ? z.boolean().optional().describe(AppConnections.UPDATE(app).isPlatformManagedCredentials) + : z.literal(false).optional().describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`) + }); diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts new file mode 100644 index 000000000..5293761a8 --- /dev/null +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -0,0 +1,455 @@ +import { ForbiddenError, subject } from "@casl/ability"; + +import { OrgPermissionAppConnectionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { generateHash } from "@app/lib/crypto/encryption"; +import { DatabaseErrorCode } from "@app/lib/error-codes"; +import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; +import { DiscriminativePick, OrgServiceActor } from "@app/lib/types"; +import { + decryptAppConnection, + encryptAppConnectionCredentials, + getAppConnectionMethodName, + listAppConnectionOptions, + TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM, + validateAppConnectionCredentials +} from "@app/services/app-connection/app-connection-fns"; +import { auth0ConnectionService } from "@app/services/app-connection/auth0/auth0-connection-service"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +import { TAppConnectionDALFactory } from "./app-connection-dal"; +import { AppConnection } from "./app-connection-enums"; +import { APP_CONNECTION_NAME_MAP } from "./app-connection-maps"; +import { + TAppConnection, + TAppConnectionConfig, + TAppConnectionRaw, + TCreateAppConnectionDTO, + TUpdateAppConnectionDTO, + TValidateAppConnectionCredentialsSchema +} from "./app-connection-types"; +import { ValidateAuth0ConnectionCredentialsSchema } from "./auth0"; +import { ValidateAwsConnectionCredentialsSchema } from "./aws"; +import { awsConnectionService } from "./aws/aws-connection-service"; +import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration"; +import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault"; +import { ValidateCamundaConnectionCredentialsSchema } from "./camunda"; +import { camundaConnectionService } from "./camunda/camunda-connection-service"; +import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks"; +import { databricksConnectionService } from "./databricks/databricks-connection-service"; +import { ValidateGcpConnectionCredentialsSchema } from "./gcp"; +import { gcpConnectionService } from "./gcp/gcp-connection-service"; +import { ValidateGitHubConnectionCredentialsSchema } from "./github"; +import { githubConnectionService } from "./github/github-connection-service"; +import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec"; +import { humanitecConnectionService } from "./humanitec/humanitec-connection-service"; +import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql"; +import { ValidatePostgresConnectionCredentialsSchema } from "./postgres"; +import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-cloud"; +import { terraformCloudConnectionService } from "./terraform-cloud/terraform-cloud-connection-service"; +import { ValidateVercelConnectionCredentialsSchema } from "./vercel"; +import { vercelConnectionService } from "./vercel/vercel-connection-service"; +import { ValidateWindmillConnectionCredentialsSchema } from "./windmill"; +import { windmillConnectionService } from "./windmill/windmill-connection-service"; + +export type TAppConnectionServiceFactoryDep = { + appConnectionDAL: TAppConnectionDALFactory; + permissionService: Pick; + kmsService: Pick; +}; + +export type TAppConnectionServiceFactory = ReturnType; + +const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record = { + [AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema, + [AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema, + [AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema, + [AppConnection.AzureKeyVault]: ValidateAzureKeyVaultConnectionCredentialsSchema, + [AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema, + [AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema, + [AppConnection.Humanitec]: ValidateHumanitecConnectionCredentialsSchema, + [AppConnection.TerraformCloud]: ValidateTerraformCloudConnectionCredentialsSchema, + [AppConnection.Vercel]: ValidateVercelConnectionCredentialsSchema, + [AppConnection.Postgres]: ValidatePostgresConnectionCredentialsSchema, + [AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema, + [AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema, + [AppConnection.Windmill]: ValidateWindmillConnectionCredentialsSchema, + [AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema +}; + +export const appConnectionServiceFactory = ({ + appConnectionDAL, + permissionService, + kmsService +}: TAppConnectionServiceFactoryDep) => { + const listAppConnectionsByOrg = async (actor: OrgServiceActor, app?: AppConnection) => { + const { permission } = await permissionService.getOrgPermission( + actor.type, + actor.id, + actor.orgId, + actor.authMethod, + actor.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionAppConnectionActions.Read, + OrgPermissionSubjects.AppConnections + ); + + const appConnections = await appConnectionDAL.find( + app + ? { orgId: actor.orgId, app } + : { + orgId: actor.orgId + } + ); + + return Promise.all( + appConnections + .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())) + .map((appConnection) => decryptAppConnection(appConnection, kmsService)) + ); + }; + + const findAppConnectionById = async (app: AppConnection, connectionId: string, actor: OrgServiceActor) => { + const appConnection = await appConnectionDAL.findById(connectionId); + + if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` }); + + const { permission } = await permissionService.getOrgPermission( + actor.type, + actor.id, + actor.orgId, + actor.authMethod, + appConnection.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionAppConnectionActions.Read, + OrgPermissionSubjects.AppConnections + ); + + if (appConnection.app !== app) + throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` }); + + return decryptAppConnection(appConnection, kmsService); + }; + + const findAppConnectionByName = async (app: AppConnection, connectionName: string, actor: OrgServiceActor) => { + const appConnection = await appConnectionDAL.findOne({ name: connectionName, orgId: actor.orgId }); + + if (!appConnection) + throw new NotFoundError({ message: `Could not find App Connection with name ${connectionName}` }); + + const { permission } = await permissionService.getOrgPermission( + actor.type, + actor.id, + actor.orgId, + actor.authMethod, + appConnection.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionAppConnectionActions.Read, + OrgPermissionSubjects.AppConnections + ); + + if (appConnection.app !== app) + throw new BadRequestError({ message: `App Connection with name ${connectionName} is not for App "${app}"` }); + + return decryptAppConnection(appConnection, kmsService); + }; + + const createAppConnection = async ( + { method, app, credentials, ...params }: TCreateAppConnectionDTO, + actor: OrgServiceActor + ) => { + const { permission } = await permissionService.getOrgPermission( + actor.type, + actor.id, + actor.orgId, + actor.authMethod, + actor.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionAppConnectionActions.Create, + OrgPermissionSubjects.AppConnections + ); + + const validatedCredentials = await validateAppConnectionCredentials({ + app, + credentials, + method, + orgId: actor.orgId + } as TAppConnectionConfig); + + try { + const createConnection = async (connectionCredentials: TAppConnection["credentials"]) => { + const encryptedCredentials = await encryptAppConnectionCredentials({ + credentials: connectionCredentials, + orgId: actor.orgId, + kmsService + }); + + return appConnectionDAL.create({ + orgId: actor.orgId, + encryptedCredentials, + method, + app, + ...params + }); + }; + + let connection: TAppConnectionRaw; + + if (params.isPlatformManagedCredentials) { + connection = await TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM[app]( + { + app, + orgId: actor.orgId, + credentials: validatedCredentials, + method + } as TAppConnectionConfig, + (platformCredentials) => createConnection(platformCredentials) + ); + } else { + connection = await createConnection(validatedCredentials); + } + + return { + ...connection, + credentialsHash: generateHash(connection.encryptedCredentials), + credentials: validatedCredentials + } as TAppConnection; + } catch (err) { + if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) { + throw new BadRequestError({ message: `An App Connection with the name "${params.name}" already exists` }); + } + + throw err; + } + }; + + const updateAppConnection = async ( + { connectionId, credentials, ...params }: TUpdateAppConnectionDTO, + actor: OrgServiceActor + ) => { + const appConnection = await appConnectionDAL.findById(connectionId); + + if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` }); + + const { permission } = await permissionService.getOrgPermission( + actor.type, + actor.id, + actor.orgId, + actor.authMethod, + appConnection.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionAppConnectionActions.Edit, + OrgPermissionSubjects.AppConnections + ); + + // prevent updating credentials or management status if platform managed + if (appConnection.isPlatformManagedCredentials && (params.isPlatformManagedCredentials === false || credentials)) { + throw new BadRequestError({ + message: "Cannot update credentials or management status for platform managed connections" + }); + } + + let updatedCredentials: undefined | TAppConnection["credentials"]; + + const { app, method } = appConnection as DiscriminativePick; + + if (credentials) { + if ( + !VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[app].safeParse({ + method, + credentials + }).success + ) + throw new BadRequestError({ + message: `Invalid credential format for ${ + APP_CONNECTION_NAME_MAP[app] + } Connection with method ${getAppConnectionMethodName(method)}` + }); + + updatedCredentials = await validateAppConnectionCredentials({ + app, + orgId: actor.orgId, + credentials, + method + } as TAppConnectionConfig); + + if (!updatedCredentials) + throw new BadRequestError({ message: "Unable to validate connection - check credentials" }); + } + + try { + const updateConnection = async (connectionCredentials: TAppConnection["credentials"] | undefined) => { + const encryptedCredentials = connectionCredentials + ? await encryptAppConnectionCredentials({ + credentials: connectionCredentials, + orgId: actor.orgId, + kmsService + }) + : undefined; + + return appConnectionDAL.updateById(connectionId, { + orgId: actor.orgId, + encryptedCredentials, + ...params + }); + }; + + let updatedConnection: TAppConnectionRaw; + + if (params.isPlatformManagedCredentials) { + if (!updatedCredentials) + // prevent enabling platform managed credentials without re-confirming credentials + throw new BadRequestError({ message: "Credentials required to transition to platform managed credentials" }); + + updatedConnection = await TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM[app]( + { + app, + orgId: actor.orgId, + credentials: updatedCredentials, + method + } as TAppConnectionConfig, + (platformCredentials) => updateConnection(platformCredentials) + ); + } else { + updatedConnection = await updateConnection(updatedCredentials); + } + + return await decryptAppConnection(updatedConnection, kmsService); + } catch (err) { + if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) { + throw new BadRequestError({ message: `An App Connection with the name "${params.name}" already exists` }); + } + + throw err; + } + }; + + const deleteAppConnection = async (app: AppConnection, connectionId: string, actor: OrgServiceActor) => { + const appConnection = await appConnectionDAL.findById(connectionId); + + if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` }); + + const { permission } = await permissionService.getOrgPermission( + actor.type, + actor.id, + actor.orgId, + actor.authMethod, + appConnection.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionAppConnectionActions.Delete, + OrgPermissionSubjects.AppConnections + ); + + if (appConnection.app !== app) + throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` }); + + // TODO (scott): add option to delete all dependencies + + try { + const deletedAppConnection = await appConnectionDAL.deleteById(connectionId); + + return await decryptAppConnection(deletedAppConnection, kmsService); + } catch (err) { + if ( + err instanceof DatabaseError && + (err.error as { code: string })?.code === DatabaseErrorCode.ForeignKeyViolation + ) { + throw new BadRequestError({ + message: + "Cannot delete App Connection with existing connections. Remove all existing connections and try again." + }); + } + + throw err; + } + }; + + const connectAppConnectionById = async ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor + ) => { + const appConnection = await appConnectionDAL.findById(connectionId); + + if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` }); + + const { permission: orgPermission } = await permissionService.getOrgPermission( + actor.type, + actor.id, + appConnection.orgId, + actor.authMethod, + actor.orgId + ); + + ForbiddenError.from(orgPermission).throwUnlessCan( + OrgPermissionAppConnectionActions.Connect, + subject(OrgPermissionSubjects.AppConnections, { connectionId: appConnection.id }) + ); + + if (appConnection.app !== app) + throw new BadRequestError({ + message: `${ + APP_CONNECTION_NAME_MAP[appConnection.app as AppConnection] + } Connection with ID ${connectionId} cannot be used to connect to ${APP_CONNECTION_NAME_MAP[app]}` + }); + + const connection = await decryptAppConnection(appConnection, kmsService); + + return connection as T; + }; + + const listAvailableAppConnectionsForUser = async (app: AppConnection, actor: OrgServiceActor) => { + const { permission: orgPermission } = await permissionService.getOrgPermission( + actor.type, + actor.id, + actor.orgId, + actor.authMethod, + actor.orgId + ); + + const appConnections = await appConnectionDAL.find({ app, orgId: actor.orgId }); + + const availableConnections = appConnections.filter((connection) => + orgPermission.can( + OrgPermissionAppConnectionActions.Connect, + subject(OrgPermissionSubjects.AppConnections, { connectionId: connection.id }) + ) + ); + + return availableConnections as Omit[]; + }; + + return { + listAppConnectionOptions, + listAppConnectionsByOrg, + findAppConnectionById, + findAppConnectionByName, + createAppConnection, + updateAppConnection, + deleteAppConnection, + connectAppConnectionById, + listAvailableAppConnectionsForUser, + github: githubConnectionService(connectAppConnectionById), + gcp: gcpConnectionService(connectAppConnectionById), + databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), + aws: awsConnectionService(connectAppConnectionById), + humanitec: humanitecConnectionService(connectAppConnectionById), + terraformCloud: terraformCloudConnectionService(connectAppConnectionById), + camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), + vercel: vercelConnectionService(connectAppConnectionById), + windmill: windmillConnectionService(connectAppConnectionById), + auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService) + }; +}; diff --git a/backend/src/services/app-connection/app-connection-types.ts b/backend/src/services/app-connection/app-connection-types.ts new file mode 100644 index 000000000..64d44ccc6 --- /dev/null +++ b/backend/src/services/app-connection/app-connection-types.ts @@ -0,0 +1,182 @@ +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { AWSRegion } from "./app-connection-enums"; +import { + TAuth0Connection, + TAuth0ConnectionConfig, + TAuth0ConnectionInput, + TValidateAuth0ConnectionCredentialsSchema +} from "./auth0"; +import { + TAwsConnection, + TAwsConnectionConfig, + TAwsConnectionInput, + TValidateAwsConnectionCredentialsSchema +} from "./aws"; +import { + TAzureAppConfigurationConnection, + TAzureAppConfigurationConnectionConfig, + TAzureAppConfigurationConnectionInput, + TValidateAzureAppConfigurationConnectionCredentialsSchema +} from "./azure-app-configuration"; +import { + TAzureKeyVaultConnection, + TAzureKeyVaultConnectionConfig, + TAzureKeyVaultConnectionInput, + TValidateAzureKeyVaultConnectionCredentialsSchema +} from "./azure-key-vault"; +import { + TCamundaConnection, + TCamundaConnectionConfig, + TCamundaConnectionInput, + TValidateCamundaConnectionCredentialsSchema +} from "./camunda"; +import { + TDatabricksConnection, + TDatabricksConnectionConfig, + TDatabricksConnectionInput, + TValidateDatabricksConnectionCredentialsSchema +} from "./databricks"; +import { + TGcpConnection, + TGcpConnectionConfig, + TGcpConnectionInput, + TValidateGcpConnectionCredentialsSchema +} from "./gcp"; +import { + TGitHubConnection, + TGitHubConnectionConfig, + TGitHubConnectionInput, + TValidateGitHubConnectionCredentialsSchema +} from "./github"; +import { + THumanitecConnection, + THumanitecConnectionConfig, + THumanitecConnectionInput, + TValidateHumanitecConnectionCredentialsSchema +} from "./humanitec"; +import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql"; +import { + TPostgresConnection, + TPostgresConnectionInput, + TValidatePostgresConnectionCredentialsSchema +} from "./postgres"; +import { + TTerraformCloudConnection, + TTerraformCloudConnectionConfig, + TTerraformCloudConnectionInput, + TValidateTerraformCloudConnectionCredentialsSchema +} from "./terraform-cloud"; +import { + TValidateVercelConnectionCredentialsSchema, + TVercelConnection, + TVercelConnectionConfig, + TVercelConnectionInput +} from "./vercel"; +import { + TValidateWindmillConnectionCredentialsSchema, + TWindmillConnection, + TWindmillConnectionConfig, + TWindmillConnectionInput +} from "./windmill"; + +export type TAppConnection = { id: string } & ( + | TAwsConnection + | TGitHubConnection + | TGcpConnection + | TAzureKeyVaultConnection + | TAzureAppConfigurationConnection + | TDatabricksConnection + | THumanitecConnection + | TTerraformCloudConnection + | TVercelConnection + | TPostgresConnection + | TMsSqlConnection + | TCamundaConnection + | TWindmillConnection + | TAuth0Connection +); + +export type TAppConnectionRaw = NonNullable>>; + +export type TSqlConnection = TPostgresConnection | TMsSqlConnection; + +export type TAppConnectionInput = { id: string } & ( + | TAwsConnectionInput + | TGitHubConnectionInput + | TGcpConnectionInput + | TAzureKeyVaultConnectionInput + | TAzureAppConfigurationConnectionInput + | TDatabricksConnectionInput + | THumanitecConnectionInput + | TTerraformCloudConnectionInput + | TVercelConnectionInput + | TPostgresConnectionInput + | TMsSqlConnectionInput + | TCamundaConnectionInput + | TWindmillConnectionInput + | TAuth0ConnectionInput +); + +export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput; + +export type TCreateAppConnectionDTO = Pick< + TAppConnectionInput, + "credentials" | "method" | "name" | "app" | "description" | "isPlatformManagedCredentials" +>; + +export type TUpdateAppConnectionDTO = Partial> & { + connectionId: string; +}; + +export type TAppConnectionConfig = + | TAwsConnectionConfig + | TGitHubConnectionConfig + | TGcpConnectionConfig + | TAzureKeyVaultConnectionConfig + | TAzureAppConfigurationConnectionConfig + | TDatabricksConnectionConfig + | THumanitecConnectionConfig + | TTerraformCloudConnectionConfig + | TVercelConnectionConfig + | TSqlConnectionConfig + | TCamundaConnectionConfig + | TWindmillConnectionConfig + | TAuth0ConnectionConfig; + +export type TValidateAppConnectionCredentialsSchema = + | TValidateAwsConnectionCredentialsSchema + | TValidateGitHubConnectionCredentialsSchema + | TValidateGcpConnectionCredentialsSchema + | TValidateAzureKeyVaultConnectionCredentialsSchema + | TValidateAzureAppConfigurationConnectionCredentialsSchema + | TValidateDatabricksConnectionCredentialsSchema + | TValidateHumanitecConnectionCredentialsSchema + | TValidatePostgresConnectionCredentialsSchema + | TValidateMsSqlConnectionCredentialsSchema + | TValidateCamundaConnectionCredentialsSchema + | TValidateTerraformCloudConnectionCredentialsSchema + | TValidateVercelConnectionCredentialsSchema + | TValidateWindmillConnectionCredentialsSchema + | TValidateAuth0ConnectionCredentialsSchema; + +export type TListAwsConnectionKmsKeys = { + connectionId: string; + region: AWSRegion; + destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager; +}; + +export type TAppConnectionCredentialsValidator = ( + appConnection: TAppConnectionConfig +) => Promise; + +export type TAppConnectionTransitionCredentialsToPlatform = ( + appConnection: TAppConnectionConfig, + callback: (credentials: TAppConnection["credentials"]) => Promise +) => Promise; + +export type TAppConnectionBaseConfig = { + supportsPlatformManagedCredentials?: boolean; +}; diff --git a/backend/src/services/app-connection/auth0/auth0-connection-enums.ts b/backend/src/services/app-connection/auth0/auth0-connection-enums.ts new file mode 100644 index 000000000..07d725bea --- /dev/null +++ b/backend/src/services/app-connection/auth0/auth0-connection-enums.ts @@ -0,0 +1,3 @@ +export enum Auth0ConnectionMethod { + ClientCredentials = "client-credentials" +} diff --git a/backend/src/services/app-connection/auth0/auth0-connection-fns.ts b/backend/src/services/app-connection/auth0/auth0-connection-fns.ts new file mode 100644 index 000000000..5a9989b43 --- /dev/null +++ b/backend/src/services/app-connection/auth0/auth0-connection-fns.ts @@ -0,0 +1,97 @@ +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +import { Auth0ConnectionMethod } from "./auth0-connection-enums"; +import { TAuth0AccessTokenResponse, TAuth0Connection, TAuth0ConnectionConfig } from "./auth0-connection-types"; + +export const getAuth0ConnectionListItem = () => { + return { + name: "Auth0" as const, + app: AppConnection.Auth0 as const, + methods: Object.values(Auth0ConnectionMethod) as [Auth0ConnectionMethod.ClientCredentials] + }; +}; + +const authorizeAuth0Connection = async ({ + clientId, + clientSecret, + domain, + audience +}: TAuth0ConnectionConfig["credentials"]) => { + const instanceUrl = domain.startsWith("http") ? domain : `https://${domain}`; + await blockLocalAndPrivateIpAddresses(instanceUrl); + + const { data } = await request.request({ + method: "POST", + url: `${removeTrailingSlash(instanceUrl)}/oauth/token`, + headers: { "content-type": "application/x-www-form-urlencoded" }, + data: new URLSearchParams({ + grant_type: "client_credentials", // this will need to be resolved if we support methods other than client credentials + client_id: clientId, + client_secret: clientSecret, + audience + }) + }); + + if (data.token_type !== "Bearer") { + throw new Error(`Unhandled token type: ${data.token_type}`); + } + + return { + accessToken: data.access_token, + // cap token lifespan to 10 minutes + expiresAt: Math.min(data.expires_in * 1000, 600000) + Date.now() + }; +}; + +export const getAuth0ConnectionAccessToken = async ( + { id, orgId, credentials }: TAuth0Connection, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const { expiresAt, accessToken } = credentials; + + // get new token if expired or less than 5 minutes until expiry + if (Date.now() < expiresAt - 300000) { + return accessToken; + } + + const authData = await authorizeAuth0Connection(credentials); + + const updatedCredentials: TAuth0Connection["credentials"] = { + ...credentials, + ...authData + }; + + const encryptedCredentials = await encryptAppConnectionCredentials({ + credentials: updatedCredentials, + orgId, + kmsService + }); + + await appConnectionDAL.updateById(id, { encryptedCredentials }); + + return authData.accessToken; +}; + +export const validateAuth0ConnectionCredentials = async ({ credentials }: TAuth0ConnectionConfig) => { + try { + const { accessToken, expiresAt } = await authorizeAuth0Connection(credentials); + + return { + ...credentials, + accessToken, + expiresAt + }; + } catch (e: unknown) { + throw new BadRequestError({ + message: (e as Error).message ?? `Unable to validate connection: verify credentials` + }); + } +}; diff --git a/backend/src/services/app-connection/auth0/auth0-connection-schemas.ts b/backend/src/services/app-connection/auth0/auth0-connection-schemas.ts new file mode 100644 index 000000000..67992a503 --- /dev/null +++ b/backend/src/services/app-connection/auth0/auth0-connection-schemas.ts @@ -0,0 +1,94 @@ +import { z } from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { Auth0ConnectionMethod } from "./auth0-connection-enums"; + +export const Auth0ConnectionClientCredentialsInputCredentialsSchema = z.object({ + domain: z.string().trim().min(1, "Domain required").describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.domain), + clientId: z + .string() + .trim() + .min(1, "Client ID required") + .describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.clientId), + clientSecret: z + .string() + .trim() + .min(1, "Client Secret required") + .describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.clientSecret), + audience: z + .string() + .trim() + .url() + .min(1, "Audience required") + .describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.audience) +}); + +const Auth0ConnectionClientCredentialsOutputCredentialsSchema = z + .object({ + accessToken: z.string(), + expiresAt: z.number() + }) + .merge(Auth0ConnectionClientCredentialsInputCredentialsSchema); + +const BaseAuth0ConnectionSchema = BaseAppConnectionSchema.extend({ + app: z.literal(AppConnection.Auth0) +}); + +export const Auth0ConnectionSchema = z.intersection( + BaseAuth0ConnectionSchema, + z.discriminatedUnion("method", [ + z.object({ + method: z.literal(Auth0ConnectionMethod.ClientCredentials), + credentials: Auth0ConnectionClientCredentialsOutputCredentialsSchema + }) + ]) +); + +export const SanitizedAuth0ConnectionSchema = z.discriminatedUnion("method", [ + BaseAuth0ConnectionSchema.extend({ + method: z.literal(Auth0ConnectionMethod.ClientCredentials), + credentials: Auth0ConnectionClientCredentialsInputCredentialsSchema.pick({ + domain: true, + clientId: true, + audience: true + }) + }) +]); + +export const ValidateAuth0ConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(Auth0ConnectionMethod.ClientCredentials) + .describe(AppConnections.CREATE(AppConnection.Auth0).method), + credentials: Auth0ConnectionClientCredentialsInputCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.Auth0).credentials + ) + }) +]); + +export const CreateAuth0ConnectionSchema = ValidateAuth0ConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.Auth0) +); + +export const UpdateAuth0ConnectionSchema = z + .object({ + credentials: Auth0ConnectionClientCredentialsInputCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.Auth0).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Auth0)); + +export const Auth0ConnectionListItemSchema = z.object({ + name: z.literal("Auth0"), + app: z.literal(AppConnection.Auth0), + // the below is preferable but currently breaks with our zod to json schema parser + // methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]), + methods: z.nativeEnum(Auth0ConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/auth0/auth0-connection-service.ts b/backend/src/services/app-connection/auth0/auth0-connection-service.ts new file mode 100644 index 000000000..693c55ea6 --- /dev/null +++ b/backend/src/services/app-connection/auth0/auth0-connection-service.ts @@ -0,0 +1,71 @@ +import { request } from "@app/lib/config/request"; +import { OrgServiceActor } from "@app/lib/types"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { getAuth0ConnectionAccessToken } from "@app/services/app-connection/auth0/auth0-connection-fns"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +import { TAuth0Connection, TAuth0ListClient, TAuth0ListClientsResponse } from "./auth0-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +const listAuth0Clients = async ( + appConnection: TAuth0Connection, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const accessToken = await getAuth0ConnectionAccessToken(appConnection, appConnectionDAL, kmsService); + + const { audience, clientId: connectionClientId } = appConnection.credentials; + await blockLocalAndPrivateIpAddresses(audience); + + const clients: TAuth0ListClient[] = []; + let hasMore = true; + let page = 0; + + while (hasMore) { + // eslint-disable-next-line no-await-in-loop + const { data: clientsPage } = await request.get(`${audience}clients`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + }, + params: { + include_totals: true, + per_page: 100, + page + } + }); + + clients.push(...clientsPage.clients); + page += 1; + hasMore = clientsPage.total > clients.length; + } + + return ( + clients.filter((client) => client.client_id !== connectionClientId && client.name !== "All Applications") ?? [] + ); +}; + +export const auth0ConnectionService = ( + getAppConnection: TGetAppConnectionFunc, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const listClients = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Auth0, connectionId, actor); + + const clients = await listAuth0Clients(appConnection, appConnectionDAL, kmsService); + + return clients.map((client) => ({ id: client.client_id, name: client.name })); + }; + + return { + listClients + }; +}; diff --git a/backend/src/services/app-connection/auth0/auth0-connection-types.ts b/backend/src/services/app-connection/auth0/auth0-connection-types.ts new file mode 100644 index 000000000..ebb601946 --- /dev/null +++ b/backend/src/services/app-connection/auth0/auth0-connection-types.ts @@ -0,0 +1,39 @@ +import { z } from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +import { + Auth0ConnectionSchema, + CreateAuth0ConnectionSchema, + ValidateAuth0ConnectionCredentialsSchema +} from "./auth0-connection-schemas"; + +export type TAuth0Connection = z.infer; + +export type TAuth0ConnectionInput = z.infer & { + app: AppConnection.Auth0; +}; + +export type TValidateAuth0ConnectionCredentialsSchema = typeof ValidateAuth0ConnectionCredentialsSchema; + +export type TAuth0ConnectionConfig = DiscriminativePick & { + orgId: string; +}; + +export type TAuth0AccessTokenResponse = { + access_token: string; + expires_in: number; + scope: string; + token_type: string; +}; + +export type TAuth0ListClient = { + name: string; + client_id: string; +}; + +export type TAuth0ListClientsResponse = { + total: number; + clients: TAuth0ListClient[]; +}; diff --git a/backend/src/services/app-connection/auth0/index.ts b/backend/src/services/app-connection/auth0/index.ts new file mode 100644 index 000000000..310ae3ea8 --- /dev/null +++ b/backend/src/services/app-connection/auth0/index.ts @@ -0,0 +1,4 @@ +export * from "./auth0-connection-enums"; +export * from "./auth0-connection-fns"; +export * from "./auth0-connection-schemas"; +export * from "./auth0-connection-types"; diff --git a/backend/src/services/app-connection/aws/aws-connection-enums.ts b/backend/src/services/app-connection/aws/aws-connection-enums.ts new file mode 100644 index 000000000..0b571de0c --- /dev/null +++ b/backend/src/services/app-connection/aws/aws-connection-enums.ts @@ -0,0 +1,4 @@ +export enum AwsConnectionMethod { + AssumeRole = "assume-role", + AccessKey = "access-key" +} diff --git a/backend/src/services/app-connection/aws/aws-connection-fns.ts b/backend/src/services/app-connection/aws/aws-connection-fns.ts new file mode 100644 index 000000000..767cb82fb --- /dev/null +++ b/backend/src/services/app-connection/aws/aws-connection-fns.ts @@ -0,0 +1,108 @@ +import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts"; +import AWS from "aws-sdk"; +import { randomUUID } from "crypto"; + +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError, InternalServerError } from "@app/lib/errors"; +import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums"; + +import { AwsConnectionMethod } from "./aws-connection-enums"; +import { TAwsConnectionConfig } from "./aws-connection-types"; + +export const getAwsConnectionListItem = () => { + const { INF_APP_CONNECTION_AWS_ACCESS_KEY_ID } = getConfig(); + + return { + name: "AWS" as const, + app: AppConnection.AWS as const, + methods: Object.values(AwsConnectionMethod) as [AwsConnectionMethod.AssumeRole, AwsConnectionMethod.AccessKey], + accessKeyId: INF_APP_CONNECTION_AWS_ACCESS_KEY_ID + }; +}; + +export const getAwsConnectionConfig = async (appConnection: TAwsConnectionConfig, region = AWSRegion.US_EAST_1) => { + const appCfg = getConfig(); + + let accessKeyId: string; + let secretAccessKey: string; + let sessionToken: undefined | string; + + const { method, credentials, orgId } = appConnection; + + switch (method) { + case AwsConnectionMethod.AssumeRole: { + const client = new STSClient({ + region, + credentials: + appCfg.INF_APP_CONNECTION_AWS_ACCESS_KEY_ID && appCfg.INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY + ? { + accessKeyId: appCfg.INF_APP_CONNECTION_AWS_ACCESS_KEY_ID, + secretAccessKey: appCfg.INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY + } + : undefined // if hosting on AWS + }); + + const command = new AssumeRoleCommand({ + RoleArn: credentials.roleArn, + RoleSessionName: `infisical-app-connection-${randomUUID()}`, + DurationSeconds: 900, // 15 mins + ExternalId: orgId + }); + + const assumeRes = await client.send(command); + + if (!assumeRes.Credentials?.AccessKeyId || !assumeRes.Credentials?.SecretAccessKey) { + throw new BadRequestError({ message: "Failed to assume role - verify credentials and role configuration" }); + } + + accessKeyId = assumeRes.Credentials.AccessKeyId; + secretAccessKey = assumeRes.Credentials.SecretAccessKey; + sessionToken = assumeRes.Credentials?.SessionToken; + break; + } + case AwsConnectionMethod.AccessKey: { + accessKeyId = credentials.accessKeyId; + secretAccessKey = credentials.secretAccessKey; + break; + } + default: + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new InternalServerError({ message: `Unsupported AWS connection method: ${method}` }); + } + + return new AWS.Config({ + region, + credentials: { + accessKeyId, + secretAccessKey, + sessionToken + } + }); +}; + +export const validateAwsConnectionCredentials = async (appConnection: TAwsConnectionConfig) => { + let resp: AWS.STS.GetCallerIdentityResponse & { + $response: AWS.Response; + }; + + try { + const awsConfig = await getAwsConnectionConfig(appConnection); + const sts = new AWS.STS(awsConfig); + + resp = await sts.getCallerIdentity().promise(); + } catch (e: unknown) { + throw new BadRequestError({ + message: `Unable to validate connection: verify credentials` + }); + } + + if (resp?.$response.httpResponse.statusCode !== 200) + throw new InternalServerError({ + message: `Unable to validate credentials: ${ + resp.$response.error?.message ?? + `AWS responded with a status code of ${resp.$response.httpResponse.statusCode}. Verify credentials and try again.` + }` + }); + + return appConnection.credentials; +}; diff --git a/backend/src/services/app-connection/aws/aws-connection-schemas.ts b/backend/src/services/app-connection/aws/aws-connection-schemas.ts new file mode 100644 index 000000000..8cb19ba26 --- /dev/null +++ b/backend/src/services/app-connection/aws/aws-connection-schemas.ts @@ -0,0 +1,82 @@ +import { z } from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { AwsConnectionMethod } from "./aws-connection-enums"; + +export const AwsConnectionAssumeRoleCredentialsSchema = z.object({ + roleArn: z.string().trim().min(1, "Role ARN required") +}); + +export const AwsConnectionAccessTokenCredentialsSchema = z.object({ + accessKeyId: z.string().trim().min(1, "Access Key ID required"), + secretAccessKey: z.string().trim().min(1, "Secret Access Key required") +}); + +const BaseAwsConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.AWS) }); + +export const AwsConnectionSchema = z.intersection( + BaseAwsConnectionSchema, + z.discriminatedUnion("method", [ + z.object({ + method: z.literal(AwsConnectionMethod.AssumeRole), + credentials: AwsConnectionAssumeRoleCredentialsSchema + }), + z.object({ + method: z.literal(AwsConnectionMethod.AccessKey), + credentials: AwsConnectionAccessTokenCredentialsSchema + }) + ]) +); + +export const SanitizedAwsConnectionSchema = z.discriminatedUnion("method", [ + BaseAwsConnectionSchema.extend({ + method: z.literal(AwsConnectionMethod.AssumeRole), + credentials: AwsConnectionAssumeRoleCredentialsSchema.pick({}) + }), + BaseAwsConnectionSchema.extend({ + method: z.literal(AwsConnectionMethod.AccessKey), + credentials: AwsConnectionAccessTokenCredentialsSchema.pick({ accessKeyId: true }) + }) +]); + +export const ValidateAwsConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z.literal(AwsConnectionMethod.AssumeRole).describe(AppConnections.CREATE(AppConnection.AWS).method), + credentials: AwsConnectionAssumeRoleCredentialsSchema.describe(AppConnections.CREATE(AppConnection.AWS).credentials) + }), + z.object({ + method: z.literal(AwsConnectionMethod.AccessKey).describe(AppConnections.CREATE(AppConnection.AWS).method), + credentials: AwsConnectionAccessTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.AWS).credentials + ) + }) +]); + +export const CreateAwsConnectionSchema = ValidateAwsConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.AWS) +); + +export const UpdateAwsConnectionSchema = z + .object({ + credentials: z + .union([AwsConnectionAccessTokenCredentialsSchema, AwsConnectionAssumeRoleCredentialsSchema]) + .optional() + .describe(AppConnections.UPDATE(AppConnection.AWS).credentials) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AWS)); + +export const AwsConnectionListItemSchema = z.object({ + name: z.literal("AWS"), + app: z.literal(AppConnection.AWS), + // the below is preferable but currently breaks with our zod to json schema parser + // methods: z.tuple([z.literal(AwsConnectionMethod.AssumeRole), z.literal(AwsConnectionMethod.AccessKey)]), + methods: z.nativeEnum(AwsConnectionMethod).array(), + accessKeyId: z.string().optional() +}); diff --git a/backend/src/services/app-connection/aws/aws-connection-service.ts b/backend/src/services/app-connection/aws/aws-connection-service.ts new file mode 100644 index 000000000..689608b81 --- /dev/null +++ b/backend/src/services/app-connection/aws/aws-connection-service.ts @@ -0,0 +1,88 @@ +import AWS from "aws-sdk"; + +import { OrgServiceActor } from "@app/lib/types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { TListAwsConnectionKmsKeys } from "@app/services/app-connection/app-connection-types"; +import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns"; +import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +const listAwsKmsKeys = async ( + appConnection: TAwsConnection, + { region, destination }: Pick +) => { + const { credentials } = await getAwsConnectionConfig(appConnection, region); + + const awsKms = new AWS.KMS({ + credentials, + region + }); + + const aliasEntries: AWS.KMS.AliasList = []; + let aliasMarker: string | undefined; + do { + // eslint-disable-next-line no-await-in-loop + const response = await awsKms.listAliases({ Limit: 100, Marker: aliasMarker }).promise(); + aliasEntries.push(...(response.Aliases || [])); + aliasMarker = response.NextMarker; + } while (aliasMarker); + + const keyMetadataRecord: Record = {}; + for await (const aliasEntry of aliasEntries) { + if (aliasEntry.TargetKeyId) { + const keyDescription = await awsKms.describeKey({ KeyId: aliasEntry.TargetKeyId }).promise(); + + keyMetadataRecord[aliasEntry.TargetKeyId] = keyDescription.KeyMetadata; + } + } + + const validAliasEntries = aliasEntries.filter((aliasEntry) => { + if (!aliasEntry.TargetKeyId) return false; + + if (destination === SecretSync.AWSParameterStore && aliasEntry.AliasName === "alias/aws/ssm") return true; + + if (destination === SecretSync.AWSSecretsManager && aliasEntry.AliasName === "alias/aws/secretsmanager") + return true; + + if (aliasEntry.AliasName?.includes("alias/aws/")) return false; + + const keyMetadata = keyMetadataRecord[aliasEntry.TargetKeyId]; + + if (!keyMetadata || keyMetadata.KeyUsage !== "ENCRYPT_DECRYPT" || keyMetadata.KeySpec !== "SYMMETRIC_DEFAULT") + return false; + + return true; + }); + + const kmsKeys = validAliasEntries.map((aliasEntry) => { + return { + id: aliasEntry.TargetKeyId!, + alias: aliasEntry.AliasName! + }; + }); + + return kmsKeys; +}; + +export const awsConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listKmsKeys = async ( + { connectionId, region, destination }: TListAwsConnectionKmsKeys, + actor: OrgServiceActor + ) => { + const appConnection = await getAppConnection(AppConnection.AWS, connectionId, actor); + + const kmsKeys = await listAwsKmsKeys(appConnection, { region, destination }); + + return kmsKeys; + }; + + return { + listKmsKeys + }; +}; diff --git a/backend/src/services/app-connection/aws/aws-connection-types.ts b/backend/src/services/app-connection/aws/aws-connection-types.ts new file mode 100644 index 000000000..a311d604b --- /dev/null +++ b/backend/src/services/app-connection/aws/aws-connection-types.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +import { + AwsConnectionSchema, + CreateAwsConnectionSchema, + ValidateAwsConnectionCredentialsSchema +} from "./aws-connection-schemas"; + +export type TAwsConnection = z.infer; + +export type TAwsConnectionInput = z.infer & { + app: AppConnection.AWS; +}; + +export type TValidateAwsConnectionCredentialsSchema = typeof ValidateAwsConnectionCredentialsSchema; + +export type TAwsConnectionConfig = DiscriminativePick & { + orgId: string; +}; diff --git a/backend/src/services/app-connection/aws/index.ts b/backend/src/services/app-connection/aws/index.ts new file mode 100644 index 000000000..4608a3483 --- /dev/null +++ b/backend/src/services/app-connection/aws/index.ts @@ -0,0 +1,4 @@ +export * from "./aws-connection-enums"; +export * from "./aws-connection-fns"; +export * from "./aws-connection-schemas"; +export * from "./aws-connection-types"; diff --git a/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-enums.ts b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-enums.ts new file mode 100644 index 000000000..450cb9255 --- /dev/null +++ b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-enums.ts @@ -0,0 +1,3 @@ +export enum AzureAppConfigurationConnectionMethod { + OAuth = "oauth" +} diff --git a/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts new file mode 100644 index 000000000..937a8a84f --- /dev/null +++ b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts @@ -0,0 +1,98 @@ +import { AxiosError, AxiosResponse } from "axios"; + +import { getConfig } from "@app/lib/config/env"; +import { request } from "@app/lib/config/request"; +import { BadRequestError, InternalServerError } from "@app/lib/errors"; +import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; + +import { AppConnection } from "../app-connection-enums"; +import { AzureAppConfigurationConnectionMethod } from "./azure-app-configuration-connection-enums"; +import { + ExchangeCodeAzureResponse, + TAzureAppConfigurationConnectionConfig +} from "./azure-app-configuration-connection-types"; + +export const getAzureAppConfigurationConnectionListItem = () => { + const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig(); + + return { + name: "Azure App Configuration" as const, + app: AppConnection.AzureAppConfiguration as const, + methods: Object.values(AzureAppConfigurationConnectionMethod) as [AzureAppConfigurationConnectionMethod.OAuth], + oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID + }; +}; + +export const validateAzureAppConfigurationConnectionCredentials = async ( + config: TAzureAppConfigurationConnectionConfig +) => { + const { credentials: inputCredentials, method } = config; + + const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig(); + + if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) { + throw new InternalServerError({ + message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured` + }); + } + + let tokenResp: AxiosResponse | null = null; + let tokenError: AxiosError | null = null; + + try { + tokenResp = await request.post( + IntegrationUrls.AZURE_TOKEN_URL.replace("common", inputCredentials.tenantId || "common"), + new URLSearchParams({ + grant_type: "authorization_code", + code: inputCredentials.code, + scope: `openid offline_access https://azconfig.io/.default`, + client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID, + client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET, + redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback` + }) + ); + } catch (e: unknown) { + if (e instanceof AxiosError) { + tokenError = e; + } else { + throw new BadRequestError({ + message: `Unable to validate connection: verify credentials` + }); + } + } + + if (tokenError) { + if (tokenError instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to get access token: ${ + (tokenError?.response?.data as { error_description?: string })?.error_description || "Unknown error" + }` + }); + } else { + throw new InternalServerError({ + message: "Failed to get access token" + }); + } + } + + if (!tokenResp) { + throw new InternalServerError({ + message: `Failed to get access token: Token was empty with no error` + }); + } + + switch (method) { + case AzureAppConfigurationConnectionMethod.OAuth: + return { + tenantId: inputCredentials.tenantId, + accessToken: tokenResp.data.access_token, + refreshToken: tokenResp.data.refresh_token, + expiresAt: Date.now() + tokenResp.data.expires_in * 1000 + }; + default: + throw new InternalServerError({ + message: `Unhandled Azure connection method: ${method as AzureAppConfigurationConnectionMethod}` + }); + } +}; diff --git a/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-schemas.ts b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-schemas.ts new file mode 100644 index 000000000..183376acf --- /dev/null +++ b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-schemas.ts @@ -0,0 +1,76 @@ +import { z } from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { AzureAppConfigurationConnectionMethod } from "./azure-app-configuration-connection-enums"; + +export const AzureAppConfigurationConnectionOAuthInputCredentialsSchema = z.object({ + code: z.string().trim().min(1, "OAuth code required"), + tenantId: z.string().trim().optional() +}); + +export const AzureAppConfigurationConnectionOAuthOutputCredentialsSchema = z.object({ + tenantId: z.string().optional(), + accessToken: z.string(), + refreshToken: z.string(), + expiresAt: z.number() +}); + +export const ValidateAzureAppConfigurationConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(AzureAppConfigurationConnectionMethod.OAuth) + .describe(AppConnections.CREATE(AppConnection.AzureAppConfiguration).method), + credentials: AzureAppConfigurationConnectionOAuthInputCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.AzureAppConfiguration).credentials + ) + }) +]); + +export const CreateAzureAppConfigurationConnectionSchema = ValidateAzureAppConfigurationConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.AzureAppConfiguration) +); + +export const UpdateAzureAppConfigurationConnectionSchema = z + .object({ + credentials: AzureAppConfigurationConnectionOAuthInputCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.AzureAppConfiguration).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureAppConfiguration)); + +const BaseAzureAppConfigurationConnectionSchema = BaseAppConnectionSchema.extend({ + app: z.literal(AppConnection.AzureAppConfiguration) +}); + +export const AzureAppConfigurationConnectionSchema = z.intersection( + BaseAzureAppConfigurationConnectionSchema, + z.discriminatedUnion("method", [ + z.object({ + method: z.literal(AzureAppConfigurationConnectionMethod.OAuth), + credentials: AzureAppConfigurationConnectionOAuthOutputCredentialsSchema + }) + ]) +); + +export const SanitizedAzureAppConfigurationConnectionSchema = z.discriminatedUnion("method", [ + BaseAzureAppConfigurationConnectionSchema.extend({ + method: z.literal(AzureAppConfigurationConnectionMethod.OAuth), + credentials: AzureAppConfigurationConnectionOAuthOutputCredentialsSchema.pick({ + tenantId: true + }) + }) +]); + +export const AzureAppConfigurationConnectionListItemSchema = z.object({ + name: z.literal("Azure App Configuration"), + app: z.literal(AppConnection.AzureAppConfiguration), + methods: z.nativeEnum(AzureAppConfigurationConnectionMethod).array(), + oauthClientId: z.string().optional() +}); diff --git a/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-types.ts b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-types.ts new file mode 100644 index 000000000..8111b4c50 --- /dev/null +++ b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-types.ts @@ -0,0 +1,41 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + AzureAppConfigurationConnectionOAuthOutputCredentialsSchema, + AzureAppConfigurationConnectionSchema, + CreateAzureAppConfigurationConnectionSchema, + ValidateAzureAppConfigurationConnectionCredentialsSchema +} from "./azure-app-configuration-connection-schemas"; + +export type TAzureAppConfigurationConnection = z.infer; + +export type TAzureAppConfigurationConnectionInput = z.infer & { + app: AppConnection.AzureAppConfiguration; +}; + +export type TValidateAzureAppConfigurationConnectionCredentialsSchema = + typeof ValidateAzureAppConfigurationConnectionCredentialsSchema; + +export type TAzureAppConfigurationConnectionConfig = DiscriminativePick< + TAzureAppConfigurationConnectionInput, + "method" | "app" | "credentials" +> & { + orgId: string; +}; + +export type ExchangeCodeAzureResponse = { + token_type: string; + scope: string; + expires_in: number; + ext_expires_in: number; + access_token: string; + refresh_token: string; + id_token: string; +}; + +export type TAzureAppConfigurationConnectionCredentials = z.infer< + typeof AzureAppConfigurationConnectionOAuthOutputCredentialsSchema +>; diff --git a/backend/src/services/app-connection/azure-app-configuration/index.ts b/backend/src/services/app-connection/azure-app-configuration/index.ts new file mode 100644 index 000000000..5fbe876f5 --- /dev/null +++ b/backend/src/services/app-connection/azure-app-configuration/index.ts @@ -0,0 +1,4 @@ +export * from "./azure-app-configuration-connection-enums"; +export * from "./azure-app-configuration-connection-fns"; +export * from "./azure-app-configuration-connection-schemas"; +export * from "./azure-app-configuration-connection-types"; diff --git a/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-enums.ts b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-enums.ts new file mode 100644 index 000000000..895e88298 --- /dev/null +++ b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-enums.ts @@ -0,0 +1,3 @@ +export enum AzureKeyVaultConnectionMethod { + OAuth = "oauth" +} diff --git a/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts new file mode 100644 index 000000000..8e8a6b2a7 --- /dev/null +++ b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts @@ -0,0 +1,170 @@ +import { AxiosError, AxiosResponse } from "axios"; + +import { getConfig } from "@app/lib/config/env"; +import { request } from "@app/lib/config/request"; +import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; +import { + decryptAppConnectionCredentials, + encryptAppConnectionCredentials, + getAppConnectionMethodName +} from "@app/services/app-connection/app-connection-fns"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +import { TAppConnectionDALFactory } from "../app-connection-dal"; +import { AppConnection } from "../app-connection-enums"; +import { AzureKeyVaultConnectionMethod } from "./azure-key-vault-connection-enums"; +import { + ExchangeCodeAzureResponse, + TAzureKeyVaultConnectionConfig, + TAzureKeyVaultConnectionCredentials +} from "./azure-key-vault-connection-types"; + +export const getAzureConnectionAccessToken = async ( + connectionId: string, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const appCfg = getConfig(); + if (!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET) { + throw new BadRequestError({ + message: `Azure environment variables have not been configured` + }); + } + + const appConnection = await appConnectionDAL.findById(connectionId); + + if (!appConnection) { + throw new NotFoundError({ message: `Connection with ID '${connectionId}' not found` }); + } + + if (appConnection.app !== AppConnection.AzureKeyVault && appConnection.app !== AppConnection.AzureAppConfiguration) { + throw new BadRequestError({ message: `Connection with ID '${connectionId}' is not an Azure Key Vault connection` }); + } + + const credentials = (await decryptAppConnectionCredentials({ + orgId: appConnection.orgId, + kmsService, + encryptedCredentials: appConnection.encryptedCredentials + })) as TAzureKeyVaultConnectionCredentials; + + const { data } = await request.post( + IntegrationUrls.AZURE_TOKEN_URL.replace("common", credentials.tenantId || "common"), + new URLSearchParams({ + grant_type: "refresh_token", + scope: `openid offline_access`, + client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID, + client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET, + refresh_token: credentials.refreshToken + }) + ); + + const accessExpiresAt = new Date(); + accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in); + + const updatedCredentials = { + ...credentials, + accessToken: data.access_token, + expiresAt: accessExpiresAt.getTime(), + refreshToken: data.refresh_token + }; + + const encryptedCredentials = await encryptAppConnectionCredentials({ + credentials: updatedCredentials, + orgId: appConnection.orgId, + kmsService + }); + + await appConnectionDAL.update( + { id: connectionId }, + { + encryptedCredentials + } + ); + + return { + accessToken: data.access_token + }; +}; + +export const getAzureKeyVaultConnectionListItem = () => { + const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig(); + + return { + name: "Azure Key Vault" as const, + app: AppConnection.AzureKeyVault as const, + methods: Object.values(AzureKeyVaultConnectionMethod) as [AzureKeyVaultConnectionMethod.OAuth], + oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID + }; +}; + +export const validateAzureKeyVaultConnectionCredentials = async (config: TAzureKeyVaultConnectionConfig) => { + const { credentials: inputCredentials, method } = config; + + const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig(); + + if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) { + throw new InternalServerError({ + message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured` + }); + } + + let tokenResp: AxiosResponse | null = null; + let tokenError: AxiosError | null = null; + + try { + tokenResp = await request.post( + IntegrationUrls.AZURE_TOKEN_URL.replace("common", inputCredentials.tenantId || "common"), + new URLSearchParams({ + grant_type: "authorization_code", + code: inputCredentials.code, + scope: `openid offline_access https://vault.azure.net/.default`, + client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID, + client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET, + redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback` + }) + ); + } catch (e: unknown) { + if (e instanceof AxiosError) { + tokenError = e; + } else { + throw new BadRequestError({ + message: `Unable to validate connection: verify credentials` + }); + } + } + + if (tokenError) { + if (tokenError instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to get access token: ${ + (tokenError?.response?.data as { error_description?: string })?.error_description || "Unknown error" + }` + }); + } else { + throw new InternalServerError({ + message: "Failed to get access token" + }); + } + } + + if (!tokenResp) { + throw new InternalServerError({ + message: `Failed to get access token: Token was empty with no error` + }); + } + + switch (method) { + case AzureKeyVaultConnectionMethod.OAuth: + return { + tenantId: inputCredentials.tenantId, + accessToken: tokenResp.data.access_token, + refreshToken: tokenResp.data.refresh_token, + expiresAt: Date.now() + tokenResp.data.expires_in * 1000 + }; + default: + throw new InternalServerError({ + message: `Unhandled Azure connection method: ${method as AzureKeyVaultConnectionMethod}` + }); + } +}; diff --git a/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-schemas.ts b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-schemas.ts new file mode 100644 index 000000000..f3c7c43b8 --- /dev/null +++ b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-schemas.ts @@ -0,0 +1,76 @@ +import { z } from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { AzureKeyVaultConnectionMethod } from "./azure-key-vault-connection-enums"; + +export const AzureKeyVaultConnectionOAuthInputCredentialsSchema = z.object({ + code: z.string().trim().min(1, "OAuth code required"), + tenantId: z.string().trim().optional() +}); + +export const AzureKeyVaultConnectionOAuthOutputCredentialsSchema = z.object({ + tenantId: z.string().optional(), + accessToken: z.string(), + refreshToken: z.string(), + expiresAt: z.number() +}); + +export const ValidateAzureKeyVaultConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(AzureKeyVaultConnectionMethod.OAuth) + .describe(AppConnections.CREATE(AppConnection.AzureKeyVault).method), + credentials: AzureKeyVaultConnectionOAuthInputCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.AzureKeyVault).credentials + ) + }) +]); + +export const CreateAzureKeyVaultConnectionSchema = ValidateAzureKeyVaultConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.AzureKeyVault) +); + +export const UpdateAzureKeyVaultConnectionSchema = z + .object({ + credentials: AzureKeyVaultConnectionOAuthInputCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.AzureKeyVault).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureKeyVault)); + +const BaseAzureKeyVaultConnectionSchema = BaseAppConnectionSchema.extend({ + app: z.literal(AppConnection.AzureKeyVault) +}); + +export const AzureKeyVaultConnectionSchema = z.intersection( + BaseAzureKeyVaultConnectionSchema, + z.discriminatedUnion("method", [ + z.object({ + method: z.literal(AzureKeyVaultConnectionMethod.OAuth), + credentials: AzureKeyVaultConnectionOAuthOutputCredentialsSchema + }) + ]) +); + +export const SanitizedAzureKeyVaultConnectionSchema = z.discriminatedUnion("method", [ + BaseAzureKeyVaultConnectionSchema.extend({ + method: z.literal(AzureKeyVaultConnectionMethod.OAuth), + credentials: AzureKeyVaultConnectionOAuthOutputCredentialsSchema.pick({ + tenantId: true + }) + }) +]); + +export const AzureKeyVaultConnectionListItemSchema = z.object({ + name: z.literal("Azure Key Vault"), + app: z.literal(AppConnection.AzureKeyVault), + methods: z.nativeEnum(AzureKeyVaultConnectionMethod).array(), + oauthClientId: z.string().optional() +}); diff --git a/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-types.ts b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-types.ts new file mode 100644 index 000000000..95e68952a --- /dev/null +++ b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-types.ts @@ -0,0 +1,38 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + AzureKeyVaultConnectionOAuthOutputCredentialsSchema, + AzureKeyVaultConnectionSchema, + CreateAzureKeyVaultConnectionSchema, + ValidateAzureKeyVaultConnectionCredentialsSchema +} from "./azure-key-vault-connection-schemas"; + +export type TAzureKeyVaultConnection = z.infer; + +export type TAzureKeyVaultConnectionInput = z.infer & { + app: AppConnection.AzureKeyVault; +}; + +export type TValidateAzureKeyVaultConnectionCredentialsSchema = typeof ValidateAzureKeyVaultConnectionCredentialsSchema; + +export type TAzureKeyVaultConnectionConfig = DiscriminativePick< + TAzureKeyVaultConnectionInput, + "method" | "app" | "credentials" +> & { + orgId: string; +}; + +export type ExchangeCodeAzureResponse = { + token_type: string; + scope: string; + expires_in: number; + ext_expires_in: number; + access_token: string; + refresh_token: string; + id_token: string; +}; + +export type TAzureKeyVaultConnectionCredentials = z.infer; diff --git a/backend/src/services/app-connection/azure-key-vault/index.ts b/backend/src/services/app-connection/azure-key-vault/index.ts new file mode 100644 index 000000000..b80b07c17 --- /dev/null +++ b/backend/src/services/app-connection/azure-key-vault/index.ts @@ -0,0 +1,4 @@ +export * from "./azure-key-vault-connection-enums"; +export * from "./azure-key-vault-connection-fns"; +export * from "./azure-key-vault-connection-schemas"; +export * from "./azure-key-vault-connection-types"; diff --git a/backend/src/services/app-connection/camunda/camunda-connection-enums.ts b/backend/src/services/app-connection/camunda/camunda-connection-enums.ts new file mode 100644 index 000000000..ea1ea0aaf --- /dev/null +++ b/backend/src/services/app-connection/camunda/camunda-connection-enums.ts @@ -0,0 +1,3 @@ +export enum CamundaConnectionMethod { + ClientCredentials = "client-credentials" +} diff --git a/backend/src/services/app-connection/camunda/camunda-connection-fns.ts b/backend/src/services/app-connection/camunda/camunda-connection-fns.ts new file mode 100644 index 000000000..90d9744b8 --- /dev/null +++ b/backend/src/services/app-connection/camunda/camunda-connection-fns.ts @@ -0,0 +1,88 @@ +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +import { TAppConnectionDALFactory } from "../app-connection-dal"; +import { CamundaConnectionMethod } from "./camunda-connection-enums"; +import { TAuthorizeCamundaConnection, TCamundaConnection, TCamundaConnectionConfig } from "./camunda-connection-types"; + +export const getCamundaConnectionListItem = () => { + return { + name: "Camunda" as const, + app: AppConnection.Camunda as const, + methods: Object.values(CamundaConnectionMethod) as [CamundaConnectionMethod.ClientCredentials] + }; +}; + +const authorizeCamundaConnection = async ({ + clientId, + clientSecret +}: Pick) => { + const { data } = await request.post( + IntegrationUrls.CAMUNDA_TOKEN_URL, + { + grant_type: "client_credentials", + client_id: clientId, + client_secret: clientSecret, + audience: "api.cloud.camunda.io" + }, + { + headers: { + "Content-Type": "application/json" + } + } + ); + + return { accessToken: data.access_token, expiresAt: data.expires_in * 1000 + Date.now() }; +}; + +export const getCamundaConnectionAccessToken = async ( + { id, orgId, credentials }: TCamundaConnection, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const { clientSecret, clientId, accessToken, expiresAt } = credentials; + + // get new token if less than 30 seconds from expiry + if (Date.now() < expiresAt - 30_000) { + return accessToken; + } + + const authData = await authorizeCamundaConnection({ clientId, clientSecret }); + + const updatedCredentials: TCamundaConnection["credentials"] = { + ...credentials, + ...authData + }; + + const encryptedCredentials = await encryptAppConnectionCredentials({ + credentials: updatedCredentials, + orgId, + kmsService + }); + + await appConnectionDAL.updateById(id, { encryptedCredentials }); + + return authData.accessToken; +}; + +export const validateCamundaConnectionCredentials = async (appConnection: TCamundaConnectionConfig) => { + const { credentials } = appConnection; + + try { + const { accessToken, expiresAt } = await authorizeCamundaConnection(appConnection.credentials); + + return { + ...credentials, + accessToken, + expiresAt + }; + } catch (e: unknown) { + throw new BadRequestError({ + message: `Unable to validate connection: verify credentials` + }); + } +}; diff --git a/backend/src/services/app-connection/camunda/camunda-connection-schema.ts b/backend/src/services/app-connection/camunda/camunda-connection-schema.ts new file mode 100644 index 000000000..fa769c650 --- /dev/null +++ b/backend/src/services/app-connection/camunda/camunda-connection-schema.ts @@ -0,0 +1,77 @@ +import { z } from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { CamundaConnectionMethod } from "./camunda-connection-enums"; + +const BaseCamundaConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Camunda) }); + +export const CamundaConnectionClientCredentialsInputCredentialsSchema = z.object({ + clientId: z.string().trim().min(1, "Client ID required").describe(AppConnections.CREDENTIALS.CAMUNDA.clientId), + clientSecret: z + .string() + .trim() + .min(1, "Client Secret required") + .describe(AppConnections.CREDENTIALS.CAMUNDA.clientSecret) +}); + +export const CamundaConnectionClientCredentialsOutputCredentialsSchema = z + .object({ + accessToken: z.string(), + expiresAt: z.number() + }) + .merge(CamundaConnectionClientCredentialsInputCredentialsSchema); + +export const CamundaConnectionSchema = z.intersection( + BaseCamundaConnectionSchema, + z.discriminatedUnion("method", [ + z.object({ + method: z.literal(CamundaConnectionMethod.ClientCredentials), + credentials: CamundaConnectionClientCredentialsOutputCredentialsSchema + }) + ]) +); + +export const SanitizedCamundaConnectionSchema = z.discriminatedUnion("method", [ + BaseCamundaConnectionSchema.extend({ + method: z.literal(CamundaConnectionMethod.ClientCredentials), + credentials: CamundaConnectionClientCredentialsOutputCredentialsSchema.pick({ + clientId: true + }) + }) +]); + +export const ValidateCamundaConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(CamundaConnectionMethod.ClientCredentials) + .describe(AppConnections.CREATE(AppConnection.Camunda).method), + credentials: CamundaConnectionClientCredentialsInputCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.Camunda).credentials + ) + }) +]); + +export const CreateCamundaConnectionSchema = ValidateCamundaConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.Camunda) +); + +export const UpdateCamundaConnectionSchema = z + .object({ + credentials: CamundaConnectionClientCredentialsInputCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.Camunda).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Camunda)); + +export const CamundaConnectionListItemSchema = z.object({ + name: z.literal("Camunda"), + app: z.literal(AppConnection.Camunda), + methods: z.nativeEnum(CamundaConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/camunda/camunda-connection-service.ts b/backend/src/services/app-connection/camunda/camunda-connection-service.ts new file mode 100644 index 000000000..28b3882fa --- /dev/null +++ b/backend/src/services/app-connection/camunda/camunda-connection-service.ts @@ -0,0 +1,50 @@ +import { request } from "@app/lib/config/request"; +import { OrgServiceActor } from "@app/lib/types"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +import { getCamundaConnectionAccessToken } from "./camunda-connection-fns"; +import { TCamundaConnection, TCamundaListClustersResponse } from "./camunda-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +const listCamundaClusters = async ( + appConnection: TCamundaConnection, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const accessToken = await getCamundaConnectionAccessToken(appConnection, appConnectionDAL, kmsService); + + const { data } = await request.get(`${IntegrationUrls.CAMUNDA_API_URL}/clusters`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + }); + + return data ?? []; +}; + +export const camundaConnectionService = ( + getAppConnection: TGetAppConnectionFunc, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const listClusters = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Camunda, connectionId, actor); + + const clusters = await listCamundaClusters(appConnection, appConnectionDAL, kmsService); + + return clusters; + }; + + return { + listClusters + }; +}; diff --git a/backend/src/services/app-connection/camunda/camunda-connection-types.ts b/backend/src/services/app-connection/camunda/camunda-connection-types.ts new file mode 100644 index 000000000..d59a8c7ce --- /dev/null +++ b/backend/src/services/app-connection/camunda/camunda-connection-types.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +import { + CamundaConnectionSchema, + CreateCamundaConnectionSchema, + ValidateCamundaConnectionCredentialsSchema +} from "./camunda-connection-schema"; + +export type TCamundaConnection = z.infer; + +export type TCamundaConnectionInput = z.infer & { + app: AppConnection.Camunda; +}; + +export type TValidateCamundaConnectionCredentialsSchema = typeof ValidateCamundaConnectionCredentialsSchema; + +export type TCamundaConnectionConfig = DiscriminativePick & { + orgId: string; +}; + +export type TAuthorizeCamundaConnection = { + access_token: string; + scope: string; + token_type: string; + expires_in: number; +}; + +export type TCamundaListClustersResponse = { uuid: string; name: string }[]; diff --git a/backend/src/services/app-connection/camunda/index.ts b/backend/src/services/app-connection/camunda/index.ts new file mode 100644 index 000000000..445871724 --- /dev/null +++ b/backend/src/services/app-connection/camunda/index.ts @@ -0,0 +1,4 @@ +export * from "./camunda-connection-enums"; +export * from "./camunda-connection-fns"; +export * from "./camunda-connection-schema"; +export * from "./camunda-connection-types"; diff --git a/backend/src/services/app-connection/databricks/databricks-connection-enums.ts b/backend/src/services/app-connection/databricks/databricks-connection-enums.ts new file mode 100644 index 000000000..b65161e45 --- /dev/null +++ b/backend/src/services/app-connection/databricks/databricks-connection-enums.ts @@ -0,0 +1,3 @@ +export enum DatabricksConnectionMethod { + ServicePrincipal = "service-principal" +} diff --git a/backend/src/services/app-connection/databricks/databricks-connection-fns.ts b/backend/src/services/app-connection/databricks/databricks-connection-fns.ts new file mode 100644 index 000000000..a35cc4aec --- /dev/null +++ b/backend/src/services/app-connection/databricks/databricks-connection-fns.ts @@ -0,0 +1,95 @@ +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +import { DatabricksConnectionMethod } from "./databricks-connection-enums"; +import { + TAuthorizeDatabricksConnection, + TDatabricksConnection, + TDatabricksConnectionConfig +} from "./databricks-connection-types"; + +export const getDatabricksConnectionListItem = () => { + return { + name: "Databricks" as const, + app: AppConnection.Databricks as const, + methods: Object.values(DatabricksConnectionMethod) as [DatabricksConnectionMethod.ServicePrincipal] + }; +}; + +const authorizeDatabricksConnection = async ({ + clientId, + clientSecret, + workspaceUrl +}: Pick) => { + await blockLocalAndPrivateIpAddresses(workspaceUrl); + + const { data } = await request.post( + `${removeTrailingSlash(workspaceUrl)}/oidc/v1/token`, + "grant_type=client_credentials&scope=all-apis", + { + auth: { + username: clientId, + password: clientSecret + }, + headers: { + "Content-Type": "application/x-www-form-urlencoded" + } + } + ); + + return { accessToken: data.access_token, expiresAt: data.expires_in * 1000 + Date.now() }; +}; + +export const getDatabricksConnectionAccessToken = async ( + { id, orgId, credentials }: TDatabricksConnection, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const { clientSecret, clientId, workspaceUrl, accessToken, expiresAt } = credentials; + + // get new token if less than 10 minutes from expiry + if (Date.now() < expiresAt - 10_000) { + return accessToken; + } + + const authData = await authorizeDatabricksConnection({ clientId, clientSecret, workspaceUrl }); + + const updatedCredentials: TDatabricksConnection["credentials"] = { + ...credentials, + ...authData + }; + + const encryptedCredentials = await encryptAppConnectionCredentials({ + credentials: updatedCredentials, + orgId, + kmsService + }); + + await appConnectionDAL.updateById(id, { encryptedCredentials }); + + return authData.accessToken; +}; + +export const validateDatabricksConnectionCredentials = async (appConnection: TDatabricksConnectionConfig) => { + const { credentials } = appConnection; + + try { + const { accessToken, expiresAt } = await authorizeDatabricksConnection(appConnection.credentials); + + return { + ...credentials, + accessToken, + expiresAt + }; + } catch (e: unknown) { + throw new BadRequestError({ + message: `Unable to validate connection: verify credentials` + }); + } +}; diff --git a/backend/src/services/app-connection/databricks/databricks-connection-schemas.ts b/backend/src/services/app-connection/databricks/databricks-connection-schemas.ts new file mode 100644 index 000000000..2ac58b070 --- /dev/null +++ b/backend/src/services/app-connection/databricks/databricks-connection-schemas.ts @@ -0,0 +1,77 @@ +import { z } from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { DatabricksConnectionMethod } from "./databricks-connection-enums"; + +export const DatabricksConnectionServicePrincipalInputCredentialsSchema = z.object({ + clientId: z.string().trim().min(1, "Client ID required"), + clientSecret: z.string().trim().min(1, "Client Secret required"), + workspaceUrl: z.string().trim().url().min(1, "Workspace URL required") +}); + +const DatabricksConnectionServicePrincipalOutputCredentialsSchema = z + .object({ + accessToken: z.string(), + expiresAt: z.number() + }) + .merge(DatabricksConnectionServicePrincipalInputCredentialsSchema); + +const BaseDatabricksConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Databricks) }); + +export const DatabricksConnectionSchema = z.intersection( + BaseDatabricksConnectionSchema, + z.discriminatedUnion("method", [ + z.object({ + method: z.literal(DatabricksConnectionMethod.ServicePrincipal), + credentials: DatabricksConnectionServicePrincipalOutputCredentialsSchema + }) + ]) +); + +export const SanitizedDatabricksConnectionSchema = z.discriminatedUnion("method", [ + BaseDatabricksConnectionSchema.extend({ + method: z.literal(DatabricksConnectionMethod.ServicePrincipal), + credentials: DatabricksConnectionServicePrincipalOutputCredentialsSchema.pick({ + clientId: true, + workspaceUrl: true + }) + }) +]); + +export const ValidateDatabricksConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(DatabricksConnectionMethod.ServicePrincipal) + .describe(AppConnections.CREATE(AppConnection.Databricks).method), + credentials: DatabricksConnectionServicePrincipalInputCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.Databricks).credentials + ) + }) +]); + +export const CreateDatabricksConnectionSchema = ValidateDatabricksConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.Databricks) +); + +export const UpdateDatabricksConnectionSchema = z + .object({ + credentials: DatabricksConnectionServicePrincipalInputCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.Databricks).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Databricks)); + +export const DatabricksConnectionListItemSchema = z.object({ + name: z.literal("Databricks"), + app: z.literal(AppConnection.Databricks), + // the below is preferable but currently breaks with our zod to json schema parser + // methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]), + methods: z.nativeEnum(DatabricksConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/databricks/databricks-connection-service.ts b/backend/src/services/app-connection/databricks/databricks-connection-service.ts new file mode 100644 index 000000000..37b88705a --- /dev/null +++ b/backend/src/services/app-connection/databricks/databricks-connection-service.ts @@ -0,0 +1,60 @@ +import { request } from "@app/lib/config/request"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { OrgServiceActor } from "@app/lib/types"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { getDatabricksConnectionAccessToken } from "@app/services/app-connection/databricks/databricks-connection-fns"; +import { + TDatabricksConnection, + TDatabricksListSecretScopesResponse +} from "@app/services/app-connection/databricks/databricks-connection-types"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +const listDatabricksSecretScopes = async ( + appConnection: TDatabricksConnection, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const { + credentials: { workspaceUrl } + } = appConnection; + + const accessToken = await getDatabricksConnectionAccessToken(appConnection, appConnectionDAL, kmsService); + + const { data } = await request.get( + `${removeTrailingSlash(workspaceUrl)}/api/2.0/secrets/scopes/list`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + // not present in response if no scopes exists + return data.scopes ?? []; +}; + +export const databricksConnectionService = ( + getAppConnection: TGetAppConnectionFunc, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const listSecretScopes = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Databricks, connectionId, actor); + + const secretScopes = await listDatabricksSecretScopes(appConnection, appConnectionDAL, kmsService); + + return secretScopes; + }; + + return { + listSecretScopes + }; +}; diff --git a/backend/src/services/app-connection/databricks/databricks-connection-types.ts b/backend/src/services/app-connection/databricks/databricks-connection-types.ts new file mode 100644 index 000000000..611d40fa0 --- /dev/null +++ b/backend/src/services/app-connection/databricks/databricks-connection-types.ts @@ -0,0 +1,36 @@ +import { z } from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +import { + CreateDatabricksConnectionSchema, + DatabricksConnectionSchema, + ValidateDatabricksConnectionCredentialsSchema +} from "./databricks-connection-schemas"; + +export type TDatabricksConnection = z.infer; + +export type TDatabricksConnectionInput = z.infer & { + app: AppConnection.Databricks; +}; + +export type TValidateDatabricksConnectionCredentialsSchema = typeof ValidateDatabricksConnectionCredentialsSchema; + +export type TDatabricksConnectionConfig = DiscriminativePick< + TDatabricksConnection, + "method" | "app" | "credentials" +> & { + orgId: string; +}; + +export type TAuthorizeDatabricksConnection = { + access_token: string; + scope: string; + token_type: string; + expires_in: number; +}; + +export type TDatabricksListSecretScopesResponse = { + scopes?: { name: string; backend_type: string; keyvault_metadata: { resource_id: string; dns_name: string } }[]; +}; diff --git a/backend/src/services/app-connection/databricks/index.ts b/backend/src/services/app-connection/databricks/index.ts new file mode 100644 index 000000000..844000af4 --- /dev/null +++ b/backend/src/services/app-connection/databricks/index.ts @@ -0,0 +1,4 @@ +export * from "./databricks-connection-enums"; +export * from "./databricks-connection-fns"; +export * from "./databricks-connection-schemas"; +export * from "./databricks-connection-types"; diff --git a/backend/src/services/app-connection/gcp/gcp-connection-enums.ts b/backend/src/services/app-connection/gcp/gcp-connection-enums.ts new file mode 100644 index 000000000..7f3f6d529 --- /dev/null +++ b/backend/src/services/app-connection/gcp/gcp-connection-enums.ts @@ -0,0 +1,3 @@ +export enum GcpConnectionMethod { + ServiceAccountImpersonation = "service-account-impersonation" +} diff --git a/backend/src/services/app-connection/gcp/gcp-connection-fns.ts b/backend/src/services/app-connection/gcp/gcp-connection-fns.ts new file mode 100644 index 000000000..8bde74062 --- /dev/null +++ b/backend/src/services/app-connection/gcp/gcp-connection-fns.ts @@ -0,0 +1,164 @@ +import { gaxios, Impersonated, JWT } from "google-auth-library"; +import { GetAccessTokenResponse } from "google-auth-library/build/src/auth/oauth2client"; + +import { getConfig } from "@app/lib/config/env"; +import { request } from "@app/lib/config/request"; +import { BadRequestError, InternalServerError } from "@app/lib/errors"; +import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; + +import { AppConnection } from "../app-connection-enums"; +import { GcpConnectionMethod } from "./gcp-connection-enums"; +import { + GCPApp, + GCPGetProjectsRes, + GCPGetServiceRes, + TGcpConnection, + TGcpConnectionConfig +} from "./gcp-connection-types"; + +export const getGcpConnectionListItem = () => { + return { + name: "GCP" as const, + app: AppConnection.GCP as const, + methods: Object.values(GcpConnectionMethod) as [GcpConnectionMethod.ServiceAccountImpersonation] + }; +}; + +export const getGcpConnectionAuthToken = async (appConnection: TGcpConnectionConfig) => { + const appCfg = getConfig(); + if (!appCfg.INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL) { + throw new InternalServerError({ + message: `Environment variables have not been configured for GCP ${getAppConnectionMethodName( + GcpConnectionMethod.ServiceAccountImpersonation + )}` + }); + } + + const credJson = JSON.parse(appCfg.INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL) as { + client_email: string; + private_key: string; + }; + + const sourceClient = new JWT({ + email: credJson.client_email, + key: credJson.private_key, + scopes: ["https://www.googleapis.com/auth/cloud-platform"] + }); + + const impersonatedCredentials = new Impersonated({ + sourceClient, + targetPrincipal: appConnection.credentials.serviceAccountEmail, + lifetime: 3600, + delegates: [], + targetScopes: ["https://www.googleapis.com/auth/cloud-platform"] + }); + + let tokenResponse: GetAccessTokenResponse | undefined; + try { + tokenResponse = await impersonatedCredentials.getAccessToken(); + } catch (error) { + let message = "Unable to validate connection"; + if (error instanceof gaxios.GaxiosError) { + message = error.message; + } + + throw new BadRequestError({ + message + }); + } + + if (!tokenResponse || !tokenResponse.token) { + throw new BadRequestError({ + message: `Unable to validate connection` + }); + } + + return tokenResponse.token; +}; + +export const getGcpSecretManagerProjects = async (appConnection: TGcpConnection) => { + const accessToken = await getGcpConnectionAuthToken(appConnection); + + let gcpApps: GCPApp[] = []; + + const pageSize = 100; + let pageToken: string | undefined; + let hasMorePages = true; + + const projects: { + name: string; + id: string; + }[] = []; + + while (hasMorePages) { + const params = new URLSearchParams({ + pageSize: String(pageSize), + ...(pageToken ? { pageToken } : {}) + }); + + // eslint-disable-next-line no-await-in-loop + const { data } = await request.get(`${IntegrationUrls.GCP_API_URL}/v1/projects`, { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + }); + + gcpApps = gcpApps.concat(data.projects); + + if (!data.nextPageToken) { + hasMorePages = false; + } + + pageToken = data.nextPageToken; + } + + // eslint-disable-next-line + for await (const gcpApp of gcpApps) { + try { + const res = ( + await request.get( + `${IntegrationUrls.GCP_SERVICE_USAGE_URL}/v1/projects/${gcpApp.projectId}/services/${IntegrationUrls.GCP_SECRET_MANAGER_SERVICE_NAME}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ) + ).data; + + if (res.state === "ENABLED") { + projects.push({ + name: gcpApp.name, + id: gcpApp.projectId + }); + } + } catch { + // eslint-disable-next-line + continue; + } + } + + return projects; +}; + +export const validateGcpConnectionCredentials = async (appConnection: TGcpConnectionConfig) => { + // Check if provided service account email suffix matches organization ID. + // We do this to mitigate confused deputy attacks in multi-tenant instances + if (appConnection.credentials.serviceAccountEmail) { + const expectedAccountIdSuffix = appConnection.orgId.split("-").slice(0, 2).join("-"); + const serviceAccountId = appConnection.credentials.serviceAccountEmail.split("@")[0]; + if (!serviceAccountId.endsWith(expectedAccountIdSuffix)) { + throw new BadRequestError({ + message: `GCP service account ID must have a suffix of "${expectedAccountIdSuffix}" e.g. service-account-${expectedAccountIdSuffix}@my-project.iam.gserviceaccount.com"` + }); + } + } + + await getGcpConnectionAuthToken(appConnection); + + return appConnection.credentials; +}; diff --git a/backend/src/services/app-connection/gcp/gcp-connection-schemas.ts b/backend/src/services/app-connection/gcp/gcp-connection-schemas.ts new file mode 100644 index 000000000..3637f06dc --- /dev/null +++ b/backend/src/services/app-connection/gcp/gcp-connection-schemas.ts @@ -0,0 +1,65 @@ +import z from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { GcpConnectionMethod } from "./gcp-connection-enums"; + +export const GcpConnectionServiceAccountImpersonationCredentialsSchema = z.object({ + serviceAccountEmail: z.string().email().trim().min(1, "Service account email required") +}); + +const BaseGcpConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.GCP) }); + +export const GcpConnectionSchema = z.intersection( + BaseGcpConnectionSchema, + z.discriminatedUnion("method", [ + z.object({ + method: z.literal(GcpConnectionMethod.ServiceAccountImpersonation), + credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema + }) + ]) +); + +export const SanitizedGcpConnectionSchema = z.discriminatedUnion("method", [ + BaseGcpConnectionSchema.extend({ + method: z.literal(GcpConnectionMethod.ServiceAccountImpersonation), + credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema.pick({}) + }) +]); + +export const ValidateGcpConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(GcpConnectionMethod.ServiceAccountImpersonation) + .describe(AppConnections.CREATE(AppConnection.GCP).method), + credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.GCP).credentials + ) + }) +]); + +export const CreateGcpConnectionSchema = ValidateGcpConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.GCP) +); + +export const UpdateGcpConnectionSchema = z + .object({ + credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.GCP).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.GCP)); + +export const GcpConnectionListItemSchema = z.object({ + name: z.literal("GCP"), + app: z.literal(AppConnection.GCP), + // the below is preferable but currently breaks with our zod to json schema parser + // methods: z.tuple([z.literal(GitHubConnectionMethod.App), z.literal(GitHubConnectionMethod.OAuth)]), + methods: z.nativeEnum(GcpConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/gcp/gcp-connection-service.ts b/backend/src/services/app-connection/gcp/gcp-connection-service.ts new file mode 100644 index 000000000..96b795a8f --- /dev/null +++ b/backend/src/services/app-connection/gcp/gcp-connection-service.ts @@ -0,0 +1,29 @@ +import { OrgServiceActor } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { getGcpSecretManagerProjects } from "./gcp-connection-fns"; +import { TGcpConnection } from "./gcp-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +export const gcpConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listSecretManagerProjects = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.GCP, connectionId, actor); + + try { + const projects = await getGcpSecretManagerProjects(appConnection); + + return projects; + } catch (error) { + return []; + } + }; + + return { + listSecretManagerProjects + }; +}; diff --git a/backend/src/services/app-connection/gcp/gcp-connection-types.ts b/backend/src/services/app-connection/gcp/gcp-connection-types.ts new file mode 100644 index 000000000..2bb518820 --- /dev/null +++ b/backend/src/services/app-connection/gcp/gcp-connection-types.ts @@ -0,0 +1,45 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateGcpConnectionSchema, + GcpConnectionSchema, + ValidateGcpConnectionCredentialsSchema +} from "./gcp-connection-schemas"; + +export type TGcpConnection = z.infer; + +export type TGcpConnectionInput = z.infer & { + app: AppConnection.GCP; +}; + +export type TValidateGcpConnectionCredentialsSchema = typeof ValidateGcpConnectionCredentialsSchema; + +export type TGcpConnectionConfig = DiscriminativePick & { + orgId: string; +}; + +export type GCPApp = { + projectNumber: string; + projectId: string; + lifecycleState: "ACTIVE" | "LIFECYCLE_STATE_UNSPECIFIED" | "DELETE_REQUESTED" | "DELETE_IN_PROGRESS"; + name: string; + createTime: string; + parent: { + type: "organization" | "folder" | "project"; + id: string; + }; +}; + +export type GCPGetProjectsRes = { + projects: GCPApp[]; + nextPageToken?: string; +}; + +export type GCPGetServiceRes = { + name: string; + parent: string; + state: "ENABLED" | "DISABLED" | "STATE_UNSPECIFIED"; +}; diff --git a/backend/src/services/app-connection/gcp/index.ts b/backend/src/services/app-connection/gcp/index.ts new file mode 100644 index 000000000..60ebf13e7 --- /dev/null +++ b/backend/src/services/app-connection/gcp/index.ts @@ -0,0 +1,4 @@ +export * from "./gcp-connection-enums"; +export * from "./gcp-connection-fns"; +export * from "./gcp-connection-schemas"; +export * from "./gcp-connection-types"; diff --git a/backend/src/services/app-connection/github/github-connection-enums.ts b/backend/src/services/app-connection/github/github-connection-enums.ts new file mode 100644 index 000000000..77a4eebac --- /dev/null +++ b/backend/src/services/app-connection/github/github-connection-enums.ts @@ -0,0 +1,4 @@ +export enum GitHubConnectionMethod { + OAuth = "oauth", + App = "github-app" +} diff --git a/backend/src/services/app-connection/github/github-connection-fns.ts b/backend/src/services/app-connection/github/github-connection-fns.ts new file mode 100644 index 000000000..6ec675c0f --- /dev/null +++ b/backend/src/services/app-connection/github/github-connection-fns.ts @@ -0,0 +1,260 @@ +import { createAppAuth } from "@octokit/auth-app"; +import { Octokit } from "@octokit/rest"; +import { AxiosResponse } from "axios"; + +import { getConfig } from "@app/lib/config/env"; +import { request } from "@app/lib/config/request"; +import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors"; +import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; + +import { AppConnection } from "../app-connection-enums"; +import { GitHubConnectionMethod } from "./github-connection-enums"; +import { TGitHubConnection, TGitHubConnectionConfig } from "./github-connection-types"; + +export const getGitHubConnectionListItem = () => { + const { INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITHUB_APP_SLUG } = getConfig(); + + return { + name: "GitHub" as const, + app: AppConnection.GitHub as const, + methods: Object.values(GitHubConnectionMethod) as [GitHubConnectionMethod.App, GitHubConnectionMethod.OAuth], + oauthClientId: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, + appClientSlug: INF_APP_CONNECTION_GITHUB_APP_SLUG + }; +}; + +export const getGitHubClient = (appConnection: TGitHubConnection) => { + const appCfg = getConfig(); + + const { method, credentials } = appConnection; + + let client: Octokit; + + switch (method) { + case GitHubConnectionMethod.App: + if (!appCfg.INF_APP_CONNECTION_GITHUB_APP_ID || !appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY) { + throw new InternalServerError({ + message: `GitHub ${getAppConnectionMethodName(method).replace( + "GitHub", + "" + )} environment variables have not been configured` + }); + } + + client = new Octokit({ + authStrategy: createAppAuth, + auth: { + appId: appCfg.INF_APP_CONNECTION_GITHUB_APP_ID, + privateKey: appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY, + installationId: credentials.installationId + } + }); + break; + case GitHubConnectionMethod.OAuth: + client = new Octokit({ + auth: credentials.accessToken + }); + break; + default: + throw new InternalServerError({ + message: `Unhandled GitHub connection method: ${method as GitHubConnectionMethod}` + }); + } + + return client; +}; + +type GitHubOrganization = { + login: string; + id: number; +}; + +type GitHubRepository = { + id: number; + name: string; + owner: GitHubOrganization; +}; + +export const getGitHubRepositories = async (appConnection: TGitHubConnection) => { + const client = getGitHubClient(appConnection); + + let repositories: GitHubRepository[]; + + switch (appConnection.method) { + case GitHubConnectionMethod.App: + repositories = await client.paginate("GET /installation/repositories"); + break; + case GitHubConnectionMethod.OAuth: + default: + repositories = (await client.paginate("GET /user/repos")).filter((repo) => repo.permissions?.admin); + break; + } + + return repositories; +}; + +export const getGitHubOrganizations = async (appConnection: TGitHubConnection) => { + const client = getGitHubClient(appConnection); + + let organizations: GitHubOrganization[]; + + switch (appConnection.method) { + case GitHubConnectionMethod.App: { + const installationRepositories = await client.paginate("GET /installation/repositories"); + + const organizationMap: Record = {}; + + installationRepositories.forEach((repo) => { + if (repo.owner.type === "Organization") { + organizationMap[repo.owner.id] = repo.owner; + } + }); + + organizations = Object.values(organizationMap); + + break; + } + case GitHubConnectionMethod.OAuth: + default: + organizations = await client.paginate("GET /user/orgs"); + break; + } + + return organizations; +}; + +export const getGitHubEnvironments = async (appConnection: TGitHubConnection, owner: string, repo: string) => { + const client = getGitHubClient(appConnection); + + try { + const environments = await client.paginate("GET /repos/{owner}/{repo}/environments", { + owner, + repo + }); + + return environments; + } catch (e) { + // repo doesn't have envs + if ((e as { status: number }).status === 404) { + return []; + } + + throw e; + } +}; + +type TokenRespData = { + access_token: string; + scope: string; + token_type: string; + error?: string; +}; + +export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => { + const { credentials, method } = config; + + const { + INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, + INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET, + INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID, + INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET, + SITE_URL + } = getConfig(); + + const { clientId, clientSecret } = + method === GitHubConnectionMethod.App + ? { + clientId: INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID, + clientSecret: INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET + } + : // oauth + { + clientId: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, + clientSecret: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET + }; + + if (!clientId || !clientSecret) { + throw new InternalServerError({ + message: `GitHub ${getAppConnectionMethodName(method).replace( + "GitHub", + "" + )} environment variables have not been configured` + }); + } + + let tokenResp: AxiosResponse; + + try { + tokenResp = await request.get("https://github.com/login/oauth/access_token", { + params: { + client_id: clientId, + client_secret: clientSecret, + code: credentials.code, + redirect_uri: `${SITE_URL}/organization/app-connections/github/oauth/callback` + }, + headers: { + Accept: "application/json", + "Accept-Encoding": "application/json" + } + }); + } catch (e: unknown) { + throw new BadRequestError({ + message: `Unable to validate connection: verify credentials` + }); + } + + if (tokenResp.status !== 200) { + throw new BadRequestError({ + message: `Unable to validate credentials: GitHub responded with a status code of ${tokenResp.status} (${tokenResp.statusText}). Verify credentials and try again.` + }); + } + + if (method === GitHubConnectionMethod.App) { + const installationsResp = await request.get<{ + installations: { + id: number; + account: { + login: string; + type: string; + id: number; + }; + }[]; + }>(IntegrationUrls.GITHUB_USER_INSTALLATIONS, { + headers: { + Accept: "application/json", + Authorization: `Bearer ${tokenResp.data.access_token}`, + "Accept-Encoding": "application/json" + } + }); + + const matchingInstallation = installationsResp.data.installations.find( + (installation) => installation.id === +credentials.installationId + ); + + if (!matchingInstallation) { + throw new ForbiddenRequestError({ + message: "User does not have access to the provided installation" + }); + } + } + + if (!tokenResp.data.access_token) { + throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` }); + } + + switch (method) { + case GitHubConnectionMethod.App: + return { + installationId: credentials.installationId + }; + case GitHubConnectionMethod.OAuth: + return { + accessToken: tokenResp.data.access_token + }; + default: + throw new InternalServerError({ + message: `Unhandled GitHub connection method: ${method as GitHubConnectionMethod}` + }); + } +}; diff --git a/backend/src/services/app-connection/github/github-connection-schemas.ts b/backend/src/services/app-connection/github/github-connection-schemas.ts new file mode 100644 index 000000000..e98b9169d --- /dev/null +++ b/backend/src/services/app-connection/github/github-connection-schemas.ts @@ -0,0 +1,93 @@ +import { z } from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { GitHubConnectionMethod } from "./github-connection-enums"; + +export const GitHubConnectionOAuthInputCredentialsSchema = z.object({ + code: z.string().trim().min(1, "OAuth code required") +}); + +export const GitHubConnectionAppInputCredentialsSchema = z.object({ + code: z.string().trim().min(1, "GitHub App code required"), + installationId: z.string().min(1, "GitHub App Installation ID required") +}); + +export const GitHubConnectionOAuthOutputCredentialsSchema = z.object({ + accessToken: z.string() +}); + +export const GitHubConnectionAppOutputCredentialsSchema = z.object({ + installationId: z.string() +}); + +export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z.literal(GitHubConnectionMethod.App).describe(AppConnections.CREATE(AppConnection.GitHub).method), + credentials: GitHubConnectionAppInputCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.GitHub).credentials + ) + }), + z.object({ + method: z.literal(GitHubConnectionMethod.OAuth).describe(AppConnections.CREATE(AppConnection.GitHub).method), + credentials: GitHubConnectionOAuthInputCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.GitHub).credentials + ) + }) +]); + +export const CreateGitHubConnectionSchema = ValidateGitHubConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.GitHub) +); + +export const UpdateGitHubConnectionSchema = z + .object({ + credentials: z + .union([GitHubConnectionAppInputCredentialsSchema, GitHubConnectionOAuthInputCredentialsSchema]) + .optional() + .describe(AppConnections.UPDATE(AppConnection.GitHub).credentials) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.GitHub)); + +const BaseGitHubConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.GitHub) }); + +export const GitHubConnectionSchema = z.intersection( + BaseGitHubConnectionSchema, + z.discriminatedUnion("method", [ + z.object({ + method: z.literal(GitHubConnectionMethod.App), + credentials: GitHubConnectionAppOutputCredentialsSchema + }), + z.object({ + method: z.literal(GitHubConnectionMethod.OAuth), + credentials: GitHubConnectionOAuthOutputCredentialsSchema + }) + ]) +); + +export const SanitizedGitHubConnectionSchema = z.discriminatedUnion("method", [ + BaseGitHubConnectionSchema.extend({ + method: z.literal(GitHubConnectionMethod.App), + credentials: GitHubConnectionAppOutputCredentialsSchema.pick({}) + }), + BaseGitHubConnectionSchema.extend({ + method: z.literal(GitHubConnectionMethod.OAuth), + credentials: GitHubConnectionOAuthOutputCredentialsSchema.pick({}) + }) +]); + +export const GitHubConnectionListItemSchema = z.object({ + name: z.literal("GitHub"), + app: z.literal(AppConnection.GitHub), + // the below is preferable but currently breaks with our zod to json schema parser + // methods: z.tuple([z.literal(GitHubConnectionMethod.App), z.literal(GitHubConnectionMethod.OAuth)]), + methods: z.nativeEnum(GitHubConnectionMethod).array(), + oauthClientId: z.string().optional(), + appClientSlug: z.string().optional() +}); diff --git a/backend/src/services/app-connection/github/github-connection-service.ts b/backend/src/services/app-connection/github/github-connection-service.ts new file mode 100644 index 000000000..b4e95c5a7 --- /dev/null +++ b/backend/src/services/app-connection/github/github-connection-service.ts @@ -0,0 +1,55 @@ +import { OrgServiceActor } from "@app/lib/types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + getGitHubEnvironments, + getGitHubOrganizations, + getGitHubRepositories +} from "@app/services/app-connection/github/github-connection-fns"; +import { TGitHubConnection } from "@app/services/app-connection/github/github-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +type TListGitHubEnvironmentsDTO = { + connectionId: string; + repo: string; + owner: string; +}; + +export const githubConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listRepositories = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.GitHub, connectionId, actor); + + const repositories = await getGitHubRepositories(appConnection); + + return repositories; + }; + + const listOrganizations = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.GitHub, connectionId, actor); + + const organizations = await getGitHubOrganizations(appConnection); + + return organizations; + }; + + const listEnvironments = async ( + { connectionId, repo, owner }: TListGitHubEnvironmentsDTO, + actor: OrgServiceActor + ) => { + const appConnection = await getAppConnection(AppConnection.GitHub, connectionId, actor); + + const environments = await getGitHubEnvironments(appConnection, owner, repo); + + return environments; + }; + + return { + listRepositories, + listOrganizations, + listEnvironments + }; +}; diff --git a/backend/src/services/app-connection/github/github-connection-types.ts b/backend/src/services/app-connection/github/github-connection-types.ts new file mode 100644 index 000000000..600506277 --- /dev/null +++ b/backend/src/services/app-connection/github/github-connection-types.ts @@ -0,0 +1,20 @@ +import { z } from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateGitHubConnectionSchema, + GitHubConnectionSchema, + ValidateGitHubConnectionCredentialsSchema +} from "./github-connection-schemas"; + +export type TGitHubConnection = z.infer; + +export type TGitHubConnectionInput = z.infer & { + app: AppConnection.GitHub; +}; + +export type TValidateGitHubConnectionCredentialsSchema = typeof ValidateGitHubConnectionCredentialsSchema; + +export type TGitHubConnectionConfig = DiscriminativePick; diff --git a/backend/src/services/app-connection/github/index.ts b/backend/src/services/app-connection/github/index.ts new file mode 100644 index 000000000..35915046b --- /dev/null +++ b/backend/src/services/app-connection/github/index.ts @@ -0,0 +1,4 @@ +export * from "./github-connection-enums"; +export * from "./github-connection-fns"; +export * from "./github-connection-schemas"; +export * from "./github-connection-types"; diff --git a/backend/src/services/app-connection/humanitec/humanitec-connection-enums.ts b/backend/src/services/app-connection/humanitec/humanitec-connection-enums.ts new file mode 100644 index 000000000..8011999b2 --- /dev/null +++ b/backend/src/services/app-connection/humanitec/humanitec-connection-enums.ts @@ -0,0 +1,3 @@ +export enum HumanitecConnectionMethod { + ApiToken = "api-token" +} diff --git a/backend/src/services/app-connection/humanitec/humanitec-connection-fns.ts b/backend/src/services/app-connection/humanitec/humanitec-connection-fns.ts new file mode 100644 index 000000000..b8d257026 --- /dev/null +++ b/backend/src/services/app-connection/humanitec/humanitec-connection-fns.ts @@ -0,0 +1,95 @@ +import { AxiosError, AxiosResponse } from "axios"; + +import { request } from "@app/lib/config/request"; +import { BadRequestError, InternalServerError } from "@app/lib/errors"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; + +import { HumanitecConnectionMethod } from "./humanitec-connection-enums"; +import { + HumanitecApp, + HumanitecOrg, + HumanitecOrgWithApps, + THumanitecConnection, + THumanitecConnectionConfig +} from "./humanitec-connection-types"; + +export const getHumanitecConnectionListItem = () => { + return { + name: "Humanitec" as const, + app: AppConnection.Humanitec as const, + methods: Object.values(HumanitecConnectionMethod) as [HumanitecConnectionMethod.ApiToken] + }; +}; + +export const validateHumanitecConnectionCredentials = async (config: THumanitecConnectionConfig) => { + const { credentials: inputCredentials } = config; + + let response: AxiosResponse | null = null; + + try { + response = await request.get(`${IntegrationUrls.HUMANITEC_API_URL}/orgs`, { + headers: { + Authorization: `Bearer ${inputCredentials.apiToken}` + } + }); + } catch (error: unknown) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to validate credentials: ${error.message || "Unknown error"}` + }); + } + throw new BadRequestError({ + message: "Unable to validate connection: verify credentials" + }); + } + + if (!response?.data) { + throw new InternalServerError({ + message: "Failed to get organizations: Response was empty" + }); + } + + return inputCredentials; +}; + +export const listOrganizations = async (appConnection: THumanitecConnection): Promise => { + const { + credentials: { apiToken } + } = appConnection; + const response = await request.get(`${IntegrationUrls.HUMANITEC_API_URL}/orgs`, { + headers: { + Authorization: `Bearer ${apiToken}` + } + }); + + if (!response.data) { + throw new InternalServerError({ + message: "Failed to get organizations: Response was empty" + }); + } + const orgs = response.data; + const orgsWithApps: HumanitecOrgWithApps[] = []; + + for (const org of orgs) { + // eslint-disable-next-line no-await-in-loop + const appsResponse = await request.get(`${IntegrationUrls.HUMANITEC_API_URL}/orgs/${org.id}/apps`, { + headers: { + Authorization: `Bearer ${apiToken}` + } + }); + + if (appsResponse.data) { + const apps = appsResponse.data; + orgsWithApps.push({ + ...org, + apps: apps.map((app) => ({ + name: app.name, + id: app.id, + envs: app.envs + })) + }); + } + } + return orgsWithApps; +}; diff --git a/backend/src/services/app-connection/humanitec/humanitec-connection-schemas.ts b/backend/src/services/app-connection/humanitec/humanitec-connection-schemas.ts new file mode 100644 index 000000000..4e6cb0078 --- /dev/null +++ b/backend/src/services/app-connection/humanitec/humanitec-connection-schemas.ts @@ -0,0 +1,58 @@ +import z from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { HumanitecConnectionMethod } from "./humanitec-connection-enums"; + +export const HumanitecConnectionAccessTokenCredentialsSchema = z.object({ + apiToken: z.string().trim().min(1, "API Token required") +}); + +const BaseHumanitecConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Humanitec) }); + +export const HumanitecConnectionSchema = BaseHumanitecConnectionSchema.extend({ + method: z.literal(HumanitecConnectionMethod.ApiToken), + credentials: HumanitecConnectionAccessTokenCredentialsSchema +}); + +export const SanitizedHumanitecConnectionSchema = z.discriminatedUnion("method", [ + BaseHumanitecConnectionSchema.extend({ + method: z.literal(HumanitecConnectionMethod.ApiToken), + credentials: HumanitecConnectionAccessTokenCredentialsSchema.pick({}) + }) +]); + +export const ValidateHumanitecConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(HumanitecConnectionMethod.ApiToken) + .describe(AppConnections.CREATE(AppConnection.Humanitec).method), + credentials: HumanitecConnectionAccessTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.Humanitec).credentials + ) + }) +]); + +export const CreateHumanitecConnectionSchema = ValidateHumanitecConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.Humanitec) +); + +export const UpdateHumanitecConnectionSchema = z + .object({ + credentials: HumanitecConnectionAccessTokenCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.Humanitec).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Humanitec)); + +export const HumanitecConnectionListItemSchema = z.object({ + name: z.literal("Humanitec"), + app: z.literal(AppConnection.Humanitec), + methods: z.nativeEnum(HumanitecConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/humanitec/humanitec-connection-service.ts b/backend/src/services/app-connection/humanitec/humanitec-connection-service.ts new file mode 100644 index 000000000..5ade43450 --- /dev/null +++ b/backend/src/services/app-connection/humanitec/humanitec-connection-service.ts @@ -0,0 +1,29 @@ +import { logger } from "@app/lib/logger"; +import { OrgServiceActor } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { listOrganizations as getHumanitecOrganizations } from "./humanitec-connection-fns"; +import { THumanitecConnection } from "./humanitec-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +export const humanitecConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listOrganizations = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Humanitec, connectionId, actor); + try { + const organizations = await getHumanitecOrganizations(appConnection); + return organizations; + } catch (error) { + logger.error(error, "Failed to establish connection with Humanitec"); + return []; + } + }; + + return { + listOrganizations + }; +}; diff --git a/backend/src/services/app-connection/humanitec/humanitec-connection-types.ts b/backend/src/services/app-connection/humanitec/humanitec-connection-types.ts new file mode 100644 index 000000000..b9d084a86 --- /dev/null +++ b/backend/src/services/app-connection/humanitec/humanitec-connection-types.ts @@ -0,0 +1,40 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateHumanitecConnectionSchema, + HumanitecConnectionSchema, + ValidateHumanitecConnectionCredentialsSchema +} from "./humanitec-connection-schemas"; + +export type THumanitecConnection = z.infer; + +export type THumanitecConnectionInput = z.infer & { + app: AppConnection.Humanitec; +}; + +export type TValidateHumanitecConnectionCredentialsSchema = typeof ValidateHumanitecConnectionCredentialsSchema; + +export type THumanitecConnectionConfig = DiscriminativePick< + THumanitecConnectionInput, + "method" | "app" | "credentials" +> & { + orgId: string; +}; + +export type HumanitecOrg = { + id: string; + name: string; +}; + +export type HumanitecApp = { + name: string; + id: string; + envs: { name: string; id: string }[]; +}; + +export type HumanitecOrgWithApps = HumanitecOrg & { + apps: HumanitecApp[]; +}; diff --git a/backend/src/services/app-connection/humanitec/index.ts b/backend/src/services/app-connection/humanitec/index.ts new file mode 100644 index 000000000..52fb6b3c2 --- /dev/null +++ b/backend/src/services/app-connection/humanitec/index.ts @@ -0,0 +1,4 @@ +export * from "./humanitec-connection-enums"; +export * from "./humanitec-connection-fns"; +export * from "./humanitec-connection-schemas"; +export * from "./humanitec-connection-types"; diff --git a/backend/src/services/app-connection/mssql/index.ts b/backend/src/services/app-connection/mssql/index.ts new file mode 100644 index 000000000..81044d0aa --- /dev/null +++ b/backend/src/services/app-connection/mssql/index.ts @@ -0,0 +1,4 @@ +export * from "./mssql-connection-enums"; +export * from "./mssql-connection-fns"; +export * from "./mssql-connection-schemas"; +export * from "./mssql-connection-types"; diff --git a/backend/src/services/app-connection/mssql/mssql-connection-enums.ts b/backend/src/services/app-connection/mssql/mssql-connection-enums.ts new file mode 100644 index 000000000..335b00441 --- /dev/null +++ b/backend/src/services/app-connection/mssql/mssql-connection-enums.ts @@ -0,0 +1,3 @@ +export enum MsSqlConnectionMethod { + UsernameAndPassword = "username-and-password" +} diff --git a/backend/src/services/app-connection/mssql/mssql-connection-fns.ts b/backend/src/services/app-connection/mssql/mssql-connection-fns.ts new file mode 100644 index 000000000..3b6ecf98a --- /dev/null +++ b/backend/src/services/app-connection/mssql/mssql-connection-fns.ts @@ -0,0 +1,12 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +import { MsSqlConnectionMethod } from "./mssql-connection-enums"; + +export const getMsSqlConnectionListItem = () => { + return { + name: "Microsoft SQL Server" as const, + app: AppConnection.MsSql as const, + methods: Object.values(MsSqlConnectionMethod) as [MsSqlConnectionMethod.UsernameAndPassword], + supportsPlatformManagement: true as const + }; +}; diff --git a/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts b/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts new file mode 100644 index 000000000..38ef0eef6 --- /dev/null +++ b/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts @@ -0,0 +1,67 @@ +import z from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { AppConnection } from "../app-connection-enums"; +import { BaseSqlUsernameAndPasswordConnectionSchema } from "../shared/sql"; +import { MsSqlConnectionMethod } from "./mssql-connection-enums"; + +export const MsSqlConnectionAccessTokenCredentialsSchema = BaseSqlUsernameAndPasswordConnectionSchema; + +const BaseMsSqlConnectionSchema = BaseAppConnectionSchema.extend({ + app: z.literal(AppConnection.MsSql) +}); + +export const MsSqlConnectionSchema = BaseMsSqlConnectionSchema.extend({ + method: z.literal(MsSqlConnectionMethod.UsernameAndPassword), + credentials: MsSqlConnectionAccessTokenCredentialsSchema +}); + +export const SanitizedMsSqlConnectionSchema = z.discriminatedUnion("method", [ + BaseMsSqlConnectionSchema.extend({ + method: z.literal(MsSqlConnectionMethod.UsernameAndPassword), + credentials: MsSqlConnectionAccessTokenCredentialsSchema.pick({ + host: true, + database: true, + port: true, + username: true, + sslEnabled: true, + sslRejectUnauthorized: true + }) + }) +]); + +export const ValidateMsSqlConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(MsSqlConnectionMethod.UsernameAndPassword) + .describe(AppConnections.CREATE(AppConnection.MsSql).method), + credentials: MsSqlConnectionAccessTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.MsSql).credentials + ) + }) +]); + +export const CreateMsSqlConnectionSchema = ValidateMsSqlConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.MsSql, { supportsPlatformManagedCredentials: true }) +); + +export const UpdateMsSqlConnectionSchema = z + .object({ + credentials: MsSqlConnectionAccessTokenCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.MsSql).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.MsSql, { supportsPlatformManagedCredentials: true })); + +export const MsSqlConnectionListItemSchema = z.object({ + name: z.literal("Microsoft SQL Server"), + app: z.literal(AppConnection.MsSql), + methods: z.nativeEnum(MsSqlConnectionMethod).array(), + supportsPlatformManagement: z.literal(true) +}); diff --git a/backend/src/services/app-connection/mssql/mssql-connection-types.ts b/backend/src/services/app-connection/mssql/mssql-connection-types.ts new file mode 100644 index 000000000..dda4dfe9d --- /dev/null +++ b/backend/src/services/app-connection/mssql/mssql-connection-types.ts @@ -0,0 +1,16 @@ +import z from "zod"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateMsSqlConnectionSchema, + MsSqlConnectionSchema, + ValidateMsSqlConnectionCredentialsSchema +} from "./mssql-connection-schemas"; + +export type TMsSqlConnection = z.infer; + +export type TMsSqlConnectionInput = z.infer & { + app: AppConnection.MsSql; +}; + +export type TValidateMsSqlConnectionCredentialsSchema = typeof ValidateMsSqlConnectionCredentialsSchema; diff --git a/backend/src/services/app-connection/postgres/index.ts b/backend/src/services/app-connection/postgres/index.ts new file mode 100644 index 000000000..23ddbba98 --- /dev/null +++ b/backend/src/services/app-connection/postgres/index.ts @@ -0,0 +1,4 @@ +export * from "./postgres-connection-enums"; +export * from "./postgres-connection-fns"; +export * from "./postgres-connection-schemas"; +export * from "./postgres-connection-types"; diff --git a/backend/src/services/app-connection/postgres/postgres-connection-enums.ts b/backend/src/services/app-connection/postgres/postgres-connection-enums.ts new file mode 100644 index 000000000..a29807987 --- /dev/null +++ b/backend/src/services/app-connection/postgres/postgres-connection-enums.ts @@ -0,0 +1,3 @@ +export enum PostgresConnectionMethod { + UsernameAndPassword = "username-and-password" +} diff --git a/backend/src/services/app-connection/postgres/postgres-connection-fns.ts b/backend/src/services/app-connection/postgres/postgres-connection-fns.ts new file mode 100644 index 000000000..39053dbf6 --- /dev/null +++ b/backend/src/services/app-connection/postgres/postgres-connection-fns.ts @@ -0,0 +1,12 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +import { PostgresConnectionMethod } from "./postgres-connection-enums"; + +export const getPostgresConnectionListItem = () => { + return { + name: "PostgreSQL" as const, + app: AppConnection.Postgres as const, + methods: Object.values(PostgresConnectionMethod) as [PostgresConnectionMethod.UsernameAndPassword], + supportsPlatformManagement: true as const + }; +}; diff --git a/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts b/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts new file mode 100644 index 000000000..510f7b7d0 --- /dev/null +++ b/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts @@ -0,0 +1,65 @@ +import z from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { AppConnection } from "../app-connection-enums"; +import { BaseSqlUsernameAndPasswordConnectionSchema } from "../shared/sql"; +import { PostgresConnectionMethod } from "./postgres-connection-enums"; + +export const PostgresConnectionAccessTokenCredentialsSchema = BaseSqlUsernameAndPasswordConnectionSchema; + +const BasePostgresConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Postgres) }); + +export const PostgresConnectionSchema = BasePostgresConnectionSchema.extend({ + method: z.literal(PostgresConnectionMethod.UsernameAndPassword), + credentials: PostgresConnectionAccessTokenCredentialsSchema +}); + +export const SanitizedPostgresConnectionSchema = z.discriminatedUnion("method", [ + BasePostgresConnectionSchema.extend({ + method: z.literal(PostgresConnectionMethod.UsernameAndPassword), + credentials: PostgresConnectionAccessTokenCredentialsSchema.pick({ + host: true, + database: true, + port: true, + username: true, + sslEnabled: true, + sslRejectUnauthorized: true + }) + }) +]); + +export const ValidatePostgresConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(PostgresConnectionMethod.UsernameAndPassword) + .describe(AppConnections.CREATE(AppConnection.Postgres).method), + credentials: PostgresConnectionAccessTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.Postgres).credentials + ) + }) +]); + +export const CreatePostgresConnectionSchema = ValidatePostgresConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.Postgres, { supportsPlatformManagedCredentials: true }) +); + +export const UpdatePostgresConnectionSchema = z + .object({ + credentials: PostgresConnectionAccessTokenCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.Postgres).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Postgres, { supportsPlatformManagedCredentials: true })); + +export const PostgresConnectionListItemSchema = z.object({ + name: z.literal("PostgreSQL"), + app: z.literal(AppConnection.Postgres), + methods: z.nativeEnum(PostgresConnectionMethod).array(), + supportsPlatformManagement: z.literal(true) +}); diff --git a/backend/src/services/app-connection/postgres/postgres-connection-types.ts b/backend/src/services/app-connection/postgres/postgres-connection-types.ts new file mode 100644 index 000000000..845b2b825 --- /dev/null +++ b/backend/src/services/app-connection/postgres/postgres-connection-types.ts @@ -0,0 +1,16 @@ +import z from "zod"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreatePostgresConnectionSchema, + PostgresConnectionSchema, + ValidatePostgresConnectionCredentialsSchema +} from "./postgres-connection-schemas"; + +export type TPostgresConnection = z.infer; + +export type TPostgresConnectionInput = z.infer & { + app: AppConnection.Postgres; +}; + +export type TValidatePostgresConnectionCredentialsSchema = typeof ValidatePostgresConnectionCredentialsSchema; diff --git a/backend/src/services/app-connection/shared/sql/index.ts b/backend/src/services/app-connection/shared/sql/index.ts new file mode 100644 index 000000000..107929154 --- /dev/null +++ b/backend/src/services/app-connection/shared/sql/index.ts @@ -0,0 +1,2 @@ +export * from "./sql-connection-fns"; +export * from "./sql-connection-schemas"; diff --git a/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts b/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts new file mode 100644 index 000000000..bc98e9bcc --- /dev/null +++ b/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts @@ -0,0 +1,141 @@ +import knex, { Knex } from "knex"; + +import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns"; +import { + TSqlCredentialsRotationGeneratedCredentials, + TSqlCredentialsRotationWithConnection +} from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types"; +import { BadRequestError, DatabaseError } from "@app/lib/errors"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { TAppConnectionRaw, TSqlConnection } from "@app/services/app-connection/app-connection-types"; +import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types"; + +const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000; + +const SQL_CONNECTION_CLIENT_MAP = { + [AppConnection.Postgres]: "pg", + [AppConnection.MsSql]: "mssql" +}; + +const getConnectionConfig = ({ + app, + credentials: { host, sslCertificate, sslEnabled, sslRejectUnauthorized } +}: Pick) => { + switch (app) { + case AppConnection.Postgres: { + return { + ssl: sslEnabled + ? { + rejectUnauthorized: sslRejectUnauthorized, + ca: sslCertificate, + servername: host + } + : false + }; + } + case AppConnection.MsSql: { + return { + options: sslEnabled + ? { + trustServerCertificate: !sslRejectUnauthorized, + encrypt: true, + cryptoCredentialsDetails: sslCertificate ? { ca: sslCertificate } : {} + } + : { encrypt: false } + }; + } + default: + throw new Error(`Unhandled SQL Connection Config: ${app as AppConnection}`); + } +}; + +export const getSqlConnectionClient = async (appConnection: Pick) => { + const { + app, + credentials: { host: baseHost, database, port, password, username } + } = appConnection; + + const [host] = await verifyHostInputValidity(baseHost); + + const client = knex({ + client: SQL_CONNECTION_CLIENT_MAP[app], + connection: { + database, + port, + host, + user: username, + password, + connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT, + ...getConnectionConfig(appConnection) + } + }); + + return client; +}; + +export const validateSqlConnectionCredentials = async (config: TSqlConnectionConfig) => { + const { credentials, app } = config; + + let client: Knex | undefined; + + try { + client = await getSqlConnectionClient({ app, credentials }); + + await client.raw(`Select 1`); + + return credentials; + } catch (error) { + throw new BadRequestError({ + message: `Unable to validate connection: ${ + (error as Error)?.message?.replaceAll(credentials.password, "********************") ?? "verify credentials" + }` + }); + } finally { + await client?.destroy(); + } +}; + +export const SQL_CONNECTION_ALTER_LOGIN_STATEMENT: Record< + TSqlCredentialsRotationWithConnection["connection"]["app"], + (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => [string, Knex.RawBinding] +> = { + [AppConnection.Postgres]: ({ username, password }) => [`ALTER USER ?? WITH PASSWORD '${password}';`, [username]], + [AppConnection.MsSql]: ({ username, password }) => [`ALTER LOGIN ?? WITH PASSWORD = '${password}';`, [username]] +}; + +export const transferSqlConnectionCredentialsToPlatform = async ( + config: TSqlConnectionConfig, + callback: (credentials: TSqlConnectionConfig["credentials"]) => Promise +) => { + const { credentials, app } = config; + + const client = await getSqlConnectionClient({ app, credentials }); + + const newPassword = alphaNumericNanoId(32); + + try { + return await client.transaction(async (tx) => { + await tx.raw( + ...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[app]({ username: credentials.username, password: newPassword }) + ); + return callback({ + ...credentials, + password: newPassword + }); + }); + } catch (error) { + // update/create service function will handle + if (error instanceof DatabaseError) { + throw error; + } + + throw new BadRequestError({ + message: + (error as Error)?.message?.replaceAll(newPassword, "********************") ?? + "Encountered an error transferring credentials to platform" + }); + } finally { + await client.destroy(); + } +}; diff --git a/backend/src/services/app-connection/shared/sql/sql-connection-schemas.ts b/backend/src/services/app-connection/shared/sql/sql-connection-schemas.ts new file mode 100644 index 000000000..500ed596a --- /dev/null +++ b/backend/src/services/app-connection/shared/sql/sql-connection-schemas.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; + +export const BaseSqlUsernameAndPasswordConnectionSchema = z.object({ + host: z.string().trim().min(1, "Host required").describe(AppConnections.CREDENTIALS.SQL_CONNECTION.host), + port: z.coerce.number().describe(AppConnections.CREDENTIALS.SQL_CONNECTION.port), + database: z.string().trim().min(1, "Database required").describe(AppConnections.CREDENTIALS.SQL_CONNECTION.database), + username: z.string().trim().min(1, "Username required").describe(AppConnections.CREDENTIALS.SQL_CONNECTION.username), + password: z.string().trim().min(1, "Password required").describe(AppConnections.CREDENTIALS.SQL_CONNECTION.password), + sslEnabled: z.boolean().describe(AppConnections.CREDENTIALS.SQL_CONNECTION.sslEnabled), + sslRejectUnauthorized: z.boolean().describe(AppConnections.CREDENTIALS.SQL_CONNECTION.sslRejectUnauthorized), + sslCertificate: z + .string() + .trim() + .transform((value) => value || undefined) + .optional() + .describe(AppConnections.CREDENTIALS.SQL_CONNECTION.sslCertificate) +}); diff --git a/backend/src/services/app-connection/shared/sql/sql-connection-types.ts b/backend/src/services/app-connection/shared/sql/sql-connection-types.ts new file mode 100644 index 000000000..bbfe4086c --- /dev/null +++ b/backend/src/services/app-connection/shared/sql/sql-connection-types.ts @@ -0,0 +1,6 @@ +import { DiscriminativePick } from "@app/lib/types"; +import { TSqlConnectionInput } from "@app/services/app-connection/app-connection-types"; + +export type TSqlConnectionConfig = DiscriminativePick & { + orgId: string; +}; diff --git a/backend/src/services/app-connection/terraform-cloud/index.ts b/backend/src/services/app-connection/terraform-cloud/index.ts new file mode 100644 index 000000000..dd7493443 --- /dev/null +++ b/backend/src/services/app-connection/terraform-cloud/index.ts @@ -0,0 +1,4 @@ +export * from "./terraform-cloud-connection-enums"; +export * from "./terraform-cloud-connection-fns"; +export * from "./terraform-cloud-connection-schemas"; +export * from "./terraform-cloud-connection-types"; diff --git a/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-enums.ts b/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-enums.ts new file mode 100644 index 000000000..7f6696ef2 --- /dev/null +++ b/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-enums.ts @@ -0,0 +1,3 @@ +export enum TerraformCloudConnectionMethod { + ApiToken = "api-token" +} diff --git a/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-fns.ts b/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-fns.ts new file mode 100644 index 000000000..a49e0951e --- /dev/null +++ b/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-fns.ts @@ -0,0 +1,135 @@ +import { AxiosError, AxiosResponse } from "axios"; + +import { request } from "@app/lib/config/request"; +import { BadRequestError, InternalServerError } from "@app/lib/errors"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; + +import { TerraformCloudConnectionMethod } from "./terraform-cloud-connection-enums"; +import { + TTerraformCloudConnection, + TTerraformCloudConnectionConfig, + TTerraformCloudOrganization, + TTerraformCloudVariableSet, + TTerraformCloudWorkspace +} from "./terraform-cloud-connection-types"; + +export const getTerraformCloudConnectionListItem = () => { + return { + name: "Terraform Cloud" as const, + app: AppConnection.TerraformCloud as const, + methods: Object.values(TerraformCloudConnectionMethod) as [TerraformCloudConnectionMethod.ApiToken] + }; +}; + +export const validateTerraformCloudConnectionCredentials = async (config: TTerraformCloudConnectionConfig) => { + const { credentials: inputCredentials } = config; + + let response: AxiosResponse<{ data: TTerraformCloudOrganization[] }> | null = null; + + try { + response = await request.get<{ data: TTerraformCloudOrganization[] }>( + `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations`, + { + headers: { + Authorization: `Bearer ${inputCredentials.apiToken}`, + "Content-Type": "application/vnd.api+json" + } + } + ); + } catch (error: unknown) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to validate credentials: ${error.message || "Unknown error"}` + }); + } + throw new BadRequestError({ + message: "Unable to validate connection: verify credentials" + }); + } + + if (!response?.data) { + throw new InternalServerError({ + message: "Failed to get organizations: Response was empty" + }); + } + + return inputCredentials; +}; + +export const listOrganizations = async ( + appConnection: TTerraformCloudConnection +): Promise => { + const { + credentials: { apiToken } + } = appConnection; + + const headers = { + Authorization: `Bearer ${apiToken}`, + "Content-Type": "application/vnd.api+json" + }; + + const fetchAllPages = async (url: string): Promise => { + let results: T[] = []; + let nextUrl: string | null = url; + + while (nextUrl) { + // eslint-disable-next-line no-await-in-loop + const res: AxiosResponse<{ data: T[]; links?: { next?: string } }> = await request.get(nextUrl, { headers }); + results = results.concat(res.data.data); + nextUrl = res.data.links?.next || null; + } + + return results; + }; + + const orgEntities = await fetchAllPages<{ id: string; attributes: { name: string } }>( + `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations` + ); + + const orgsWithVariableSetsAndWorkspaces: TTerraformCloudOrganization[] = []; + + const variableSetPromises = orgEntities.map((org) => + fetchAllPages<{ id: string; attributes: { name: string; description?: string; global?: boolean } }>( + `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations/${org.id}/varsets` + ).catch(() => []) + ); + + const workspacePromises = orgEntities.map((org) => + fetchAllPages<{ id: string; attributes: { name: string } }>( + `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/organizations/${org.id}/workspaces` + ).catch(() => []) + ); + + const [variableSetResults, workspaceResults] = await Promise.all([ + Promise.all(variableSetPromises), + Promise.all(workspacePromises) + ]); + + for (let i = 0; i < orgEntities.length; i += 1) { + const org = orgEntities[i]; + const variableSetsData = variableSetResults[i]; + const workspacesData = workspaceResults[i]; + + const variableSets: TTerraformCloudVariableSet[] = variableSetsData.map((varSet) => ({ + id: varSet.id, + name: varSet.attributes.name, + description: varSet.attributes.description, + global: varSet.attributes.global + })); + + const workspaces: TTerraformCloudWorkspace[] = workspacesData.map((workspace) => ({ + id: workspace.id, + name: workspace.attributes.name + })); + + orgsWithVariableSetsAndWorkspaces.push({ + id: org.id, + name: org.attributes.name, + variableSets, + workspaces + }); + } + + return orgsWithVariableSetsAndWorkspaces; +}; diff --git a/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-schemas.ts b/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-schemas.ts new file mode 100644 index 000000000..0d408ba4f --- /dev/null +++ b/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-schemas.ts @@ -0,0 +1,60 @@ +import z from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { TerraformCloudConnectionMethod } from "./terraform-cloud-connection-enums"; + +export const TerraformCloudConnectionAccessTokenCredentialsSchema = z.object({ + apiToken: z.string().trim().min(1, "API Token required").describe(AppConnections.CREDENTIALS.TERRAFORM_CLOUD.apiToken) +}); + +const BaseTerraformCloudConnectionSchema = BaseAppConnectionSchema.extend({ + app: z.literal(AppConnection.TerraformCloud) +}); + +export const TerraformCloudConnectionSchema = BaseTerraformCloudConnectionSchema.extend({ + method: z.literal(TerraformCloudConnectionMethod.ApiToken), + credentials: TerraformCloudConnectionAccessTokenCredentialsSchema +}); + +export const SanitizedTerraformCloudConnectionSchema = z.discriminatedUnion("method", [ + BaseTerraformCloudConnectionSchema.extend({ + method: z.literal(TerraformCloudConnectionMethod.ApiToken), + credentials: TerraformCloudConnectionAccessTokenCredentialsSchema.pick({}) + }) +]); + +export const ValidateTerraformCloudConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(TerraformCloudConnectionMethod.ApiToken) + .describe(AppConnections?.CREATE(AppConnection.TerraformCloud).method), + credentials: TerraformCloudConnectionAccessTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.TerraformCloud).credentials + ) + }) +]); + +export const CreateTerraformCloudConnectionSchema = ValidateTerraformCloudConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.TerraformCloud) +); + +export const UpdateTerraformCloudConnectionSchema = z + .object({ + credentials: TerraformCloudConnectionAccessTokenCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.TerraformCloud).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.TerraformCloud)); + +export const TerraformCloudConnectionListItemSchema = z.object({ + name: z.literal("Terraform Cloud"), + app: z.literal(AppConnection.TerraformCloud), + methods: z.nativeEnum(TerraformCloudConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-service.ts b/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-service.ts new file mode 100644 index 000000000..56d56492b --- /dev/null +++ b/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-service.ts @@ -0,0 +1,29 @@ +import { logger } from "@app/lib/logger"; +import { OrgServiceActor } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { listOrganizations as getTerraformCloudOrganizations } from "./terraform-cloud-connection-fns"; +import { TTerraformCloudConnection } from "./terraform-cloud-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +export const terraformCloudConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listOrganizations = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.TerraformCloud, connectionId, actor); + try { + const organizations = await getTerraformCloudOrganizations(appConnection); + return organizations; + } catch (error) { + logger.error(error, "Failed to establish connection with Terraform Cloud"); + return []; + } + }; + + return { + listOrganizations + }; +}; diff --git a/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-types.ts b/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-types.ts new file mode 100644 index 000000000..cabcbb146 --- /dev/null +++ b/backend/src/services/app-connection/terraform-cloud/terraform-cloud-connection-types.ts @@ -0,0 +1,45 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateTerraformCloudConnectionSchema, + TerraformCloudConnectionSchema, + ValidateTerraformCloudConnectionCredentialsSchema +} from "./terraform-cloud-connection-schemas"; + +export type TTerraformCloudConnection = z.infer; + +export type TTerraformCloudConnectionInput = z.infer & { + app: AppConnection.TerraformCloud; +}; + +export type TValidateTerraformCloudConnectionCredentialsSchema = + typeof ValidateTerraformCloudConnectionCredentialsSchema; + +export type TTerraformCloudConnectionConfig = DiscriminativePick< + TTerraformCloudConnectionInput, + "method" | "app" | "credentials" +> & { + orgId: string; +}; + +export type TTerraformCloudVariableSet = { + id: string; + name: string; + description?: string; + global?: boolean; +}; + +export type TTerraformCloudWorkspace = { + id: string; + name: string; +}; + +export type TTerraformCloudOrganization = { + id: string; + name: string; + variableSets: TTerraformCloudVariableSet[]; + workspaces: TTerraformCloudWorkspace[]; +}; diff --git a/backend/src/services/app-connection/vercel/index.ts b/backend/src/services/app-connection/vercel/index.ts new file mode 100644 index 000000000..82d8493f8 --- /dev/null +++ b/backend/src/services/app-connection/vercel/index.ts @@ -0,0 +1,4 @@ +export * from "./vercel-connection-enums"; +export * from "./vercel-connection-fns"; +export * from "./vercel-connection-schemas"; +export * from "./vercel-connection-types"; diff --git a/backend/src/services/app-connection/vercel/vercel-connection-enums.ts b/backend/src/services/app-connection/vercel/vercel-connection-enums.ts new file mode 100644 index 000000000..1bff0eb57 --- /dev/null +++ b/backend/src/services/app-connection/vercel/vercel-connection-enums.ts @@ -0,0 +1,3 @@ +export enum VercelConnectionMethod { + ApiToken = "api-token" +} diff --git a/backend/src/services/app-connection/vercel/vercel-connection-fns.ts b/backend/src/services/app-connection/vercel/vercel-connection-fns.ts new file mode 100644 index 000000000..01eac84dc --- /dev/null +++ b/backend/src/services/app-connection/vercel/vercel-connection-fns.ts @@ -0,0 +1,268 @@ +/* eslint-disable no-await-in-loop */ +import { AxiosError } from "axios"; + +import { request } from "@app/lib/config/request"; +import { BadRequestError, InternalServerError } from "@app/lib/errors"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { TVercelBranches } from "@app/services/integration-auth/integration-auth-types"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; + +import { VercelConnectionMethod } from "./vercel-connection-enums"; +import { + TVercelConnection, + TVercelConnectionConfig, + VercelApp, + VercelEnvironment, + VercelOrgWithApps +} from "./vercel-connection-types"; + +export const getVercelConnectionListItem = () => { + return { + name: "Vercel" as const, + app: AppConnection.Vercel as const, + methods: Object.values(VercelConnectionMethod) as [VercelConnectionMethod.ApiToken] + }; +}; + +export const validateVercelConnectionCredentials = async (config: TVercelConnectionConfig) => { + const { credentials: inputCredentials } = config; + + try { + await request.get(`${IntegrationUrls.VERCEL_API_URL}/v2/user`, { + headers: { + Authorization: `Bearer ${inputCredentials.apiToken}` + } + }); + } catch (error: unknown) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + message: `Failed to validate credentials: ${ + error.response?.data ? JSON.stringify(error.response?.data) : error.message || "Unknown error" + }` + }); + } + throw new BadRequestError({ + message: `Unable to validate connection: ${(error as Error).message || "Verify credentials"}` + }); + } + + return inputCredentials; +}; + +interface ApiResponse { + pagination?: { + count: number; + next: number; + }; + data: T[]; + [key: string]: unknown; +} + +async function fetchAllPages( + apiUrl: string, + apiToken: string, + initialParams: Record = {}, + dataPath?: string +): Promise { + const allItems: T[] = []; + let hasMoreItems = true; + let params: Record = { ...initialParams, limit: 100 }; + + while (hasMoreItems) { + try { + const response = await request.get>(apiUrl, { + params, + headers: { + Authorization: `Bearer ${apiToken}`, + "Accept-Encoding": "application/json" + } + }); + + if (!response?.data) { + throw new InternalServerError({ + message: `Failed to fetch data from ${apiUrl}: Response was empty or malformed` + }); + } + + let itemsData: T[]; + + if (dataPath && dataPath in response.data) { + itemsData = response.data[dataPath] as T[]; + } else { + itemsData = response.data.data; + } + + if (!Array.isArray(itemsData)) { + throw new InternalServerError({ + message: `Failed to fetch data from ${apiUrl}: Expected array but got ${typeof itemsData}` + }); + } + + allItems.push(...itemsData); + + if (response.data.pagination?.next) { + params = { ...params, since: response.data.pagination.next }; + } else { + hasMoreItems = false; + } + } catch (error) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to fetch data from ${apiUrl}: ${error.message || "Unknown error"}` + }); + } + throw error; + } + } + + return allItems; +} + +async function fetchOrgProjects(orgId: string, apiToken: string): Promise { + return fetchAllPages( + `${IntegrationUrls.VERCEL_API_URL}/v9/projects`, + apiToken, + { teamId: orgId }, + "projects" + ); +} + +async function fetchProjectEnvironments( + projectId: string, + teamId: string, + apiToken: string +): Promise { + try { + return await fetchAllPages( + `${IntegrationUrls.VERCEL_API_URL}/v9/projects/${projectId}/custom-environments?teamId=${teamId}`, + apiToken, + {}, + "environments" + ); + } catch (error) { + return []; + } +} + +async function fetchPreviewBranches(projectId: string, apiToken: string): Promise { + try { + const { data } = await request.get( + `${IntegrationUrls.VERCEL_API_URL}/v1/integrations/git-branches`, + { + params: { + projectId + }, + headers: { + Authorization: `Bearer ${apiToken}`, + "Accept-Encoding": "application/json" + } + } + ); + return data.filter((b) => b.ref !== "main").map((b) => b.ref); + } catch (error) { + return []; + } +} + +type VercelTeam = { + id: string; + name: string; + slug: string; +}; + +type VercelUserResponse = { + user: { + id: string; + name: string; + username: string; + }; +}; + +export const listProjects = async (appConnection: TVercelConnection): Promise => { + const { credentials } = appConnection; + const { apiToken } = credentials; + + const orgs = await fetchAllPages(`${IntegrationUrls.VERCEL_API_URL}/v2/teams`, apiToken, {}, "teams"); + + const personalAccountResponse = await request.get(`${IntegrationUrls.VERCEL_API_URL}/v2/user`, { + headers: { + Authorization: `Bearer ${apiToken}`, + "Accept-Encoding": "application/json" + } + }); + + if (personalAccountResponse?.data?.user) { + const { user } = personalAccountResponse.data; + orgs.push({ + id: user.id, + name: user.name || "Personal Account", + slug: user.username || "personal" + }); + } + + const orgsWithApps: VercelOrgWithApps[] = []; + + const orgPromises = orgs.map(async (org) => { + try { + const projects = await fetchOrgProjects(org.id, apiToken); + + const enhancedProjectsPromises = projects.map(async (project) => { + try { + const [environments, previewBranches] = await Promise.all([ + fetchProjectEnvironments(project.name, org.id, apiToken), + fetchPreviewBranches(project.id, apiToken) + ]); + + return { + name: project.name, + id: project.id, + envs: environments, + previewBranches + }; + } catch (error) { + return { + name: project.name, + id: project.id, + envs: [], + previewBranches: [] + }; + } + }); + + const enhancedProjects = await Promise.all(enhancedProjectsPromises); + + return { + ...org, + apps: enhancedProjects + }; + } catch (error) { + return null; + } + }); + + const results = await Promise.all(orgPromises); + + results.forEach((result) => { + if (result !== null) { + orgsWithApps.push(result); + } + }); + + return orgsWithApps; +}; + +export const getProjectEnvironmentVariables = (project: VercelApp): Record => { + const envVars: Record = {}; + + if (!project.envs) return envVars; + + project.envs.forEach((env) => { + if (env.slug && env.type !== "gitBranch") { + const { id, slug } = env; + envVars[id] = slug; + } + }); + + return envVars; +}; diff --git a/backend/src/services/app-connection/vercel/vercel-connection-schemas.ts b/backend/src/services/app-connection/vercel/vercel-connection-schemas.ts new file mode 100644 index 000000000..60baa4f5c --- /dev/null +++ b/backend/src/services/app-connection/vercel/vercel-connection-schemas.ts @@ -0,0 +1,58 @@ +import z from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { VercelConnectionMethod } from "./vercel-connection-enums"; + +export const VercelConnectionAccessTokenCredentialsSchema = z.object({ + apiToken: z.string().trim().min(1, "API Token required").describe(AppConnections.CREDENTIALS.VERCEL.apiToken) +}); + +const BaseVercelConnectionSchema = BaseAppConnectionSchema.extend({ + app: z.literal(AppConnection.Vercel) +}); + +export const VercelConnectionSchema = BaseVercelConnectionSchema.extend({ + method: z.literal(VercelConnectionMethod.ApiToken), + credentials: VercelConnectionAccessTokenCredentialsSchema +}); + +export const SanitizedVercelConnectionSchema = z.discriminatedUnion("method", [ + BaseVercelConnectionSchema.extend({ + method: z.literal(VercelConnectionMethod.ApiToken), + credentials: VercelConnectionAccessTokenCredentialsSchema.pick({}) + }) +]); + +export const ValidateVercelConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z.literal(VercelConnectionMethod.ApiToken).describe(AppConnections.CREATE(AppConnection.Vercel).method), + credentials: VercelConnectionAccessTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.Vercel).credentials + ) + }) +]); + +export const CreateVercelConnectionSchema = ValidateVercelConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.Vercel) +); + +export const UpdateVercelConnectionSchema = z + .object({ + credentials: VercelConnectionAccessTokenCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.Vercel).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Vercel)); + +export const VercelConnectionListItemSchema = z.object({ + name: z.literal("Vercel"), + app: z.literal(AppConnection.Vercel), + methods: z.nativeEnum(VercelConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/vercel/vercel-connection-service.ts b/backend/src/services/app-connection/vercel/vercel-connection-service.ts new file mode 100644 index 000000000..68e5215e9 --- /dev/null +++ b/backend/src/services/app-connection/vercel/vercel-connection-service.ts @@ -0,0 +1,29 @@ +import { logger } from "@app/lib/logger"; +import { OrgServiceActor } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { listProjects as getVercelProjects } from "./vercel-connection-fns"; +import { TVercelConnection } from "./vercel-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +export const vercelConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listProjects = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Vercel, connectionId, actor); + try { + const projects = await getVercelProjects(appConnection); + return projects; + } catch (error) { + logger.error(error, "Failed to establish connection with Vercel"); + return []; + } + }; + + return { + listProjects + }; +}; diff --git a/backend/src/services/app-connection/vercel/vercel-connection-types.ts b/backend/src/services/app-connection/vercel/vercel-connection-types.ts new file mode 100644 index 000000000..4ab69d1df --- /dev/null +++ b/backend/src/services/app-connection/vercel/vercel-connection-types.ts @@ -0,0 +1,73 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateVercelConnectionSchema, + ValidateVercelConnectionCredentialsSchema, + VercelConnectionSchema +} from "./vercel-connection-schemas"; + +export type TVercelConnection = z.infer; + +export type TVercelConnectionInput = z.infer & { + app: AppConnection.Vercel; +}; + +export type TValidateVercelConnectionCredentialsSchema = typeof ValidateVercelConnectionCredentialsSchema; + +export type TVercelConnectionConfig = DiscriminativePick & { + orgId: string; +}; + +export type VercelTeam = { + id: string; + name: string; + slug: string; +}; + +export type VercelEnvironment = { + id: string; + slug: string; + type: string; + target?: string[]; + gitBranch?: string; + createdAt?: number; + updatedAt?: number; +}; + +export type VercelAppMeta = { + githubCommitRef?: string; + githubCommitSha?: string; + githubCommitMessage?: string; + githubCommitAuthorName?: string; +}; + +export type VercelDeployment = { + id: string; + name: string; + url: string; + created: number; + meta?: VercelAppMeta; + target?: "production" | "preview" | "development"; +}; + +export type VercelApp = { + name: string; + id: string; + envs?: VercelEnvironment[]; + previewBranches?: string[]; +}; + +export type VercelOrgWithApps = VercelTeam & { + apps: VercelApp[]; +}; + +export type VercelUserResponse = { + user: { + id: string; + name: string; + username: string; + }; +}; diff --git a/backend/src/services/app-connection/windmill/index.ts b/backend/src/services/app-connection/windmill/index.ts new file mode 100644 index 000000000..835562171 --- /dev/null +++ b/backend/src/services/app-connection/windmill/index.ts @@ -0,0 +1,4 @@ +export * from "./windmill-connection-enums"; +export * from "./windmill-connection-fns"; +export * from "./windmill-connection-schemas"; +export * from "./windmill-connection-types"; diff --git a/backend/src/services/app-connection/windmill/windmill-connection-enums.ts b/backend/src/services/app-connection/windmill/windmill-connection-enums.ts new file mode 100644 index 000000000..ffc01234f --- /dev/null +++ b/backend/src/services/app-connection/windmill/windmill-connection-enums.ts @@ -0,0 +1,3 @@ +export enum WindmillConnectionMethod { + AccessToken = "access-token" +} diff --git a/backend/src/services/app-connection/windmill/windmill-connection-fns.ts b/backend/src/services/app-connection/windmill/windmill-connection-fns.ts new file mode 100644 index 000000000..8e478f8bb --- /dev/null +++ b/backend/src/services/app-connection/windmill/windmill-connection-fns.ts @@ -0,0 +1,65 @@ +import { AxiosError } from "axios"; + +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +import { WindmillConnectionMethod } from "./windmill-connection-enums"; +import { TWindmillConnection, TWindmillConnectionConfig, TWindmillWorkspace } from "./windmill-connection-types"; + +export const getWindmillInstanceUrl = async (config: TWindmillConnectionConfig) => { + const instanceUrl = config.credentials.instanceUrl + ? removeTrailingSlash(config.credentials.instanceUrl) + : "https://app.windmill.dev"; + + await blockLocalAndPrivateIpAddresses(instanceUrl); + + return instanceUrl; +}; + +export const getWindmillConnectionListItem = () => { + return { + name: "Windmill" as const, + app: AppConnection.Windmill as const, + methods: Object.values(WindmillConnectionMethod) as [WindmillConnectionMethod.AccessToken] + }; +}; + +export const validateWindmillConnectionCredentials = async (config: TWindmillConnectionConfig) => { + const instanceUrl = await getWindmillInstanceUrl(config); + const { accessToken } = config.credentials; + + try { + await request.get(`${instanceUrl}/api/workspaces/list`, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + } catch (error: unknown) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to validate credentials: ${error.message || "Unknown error"}` + }); + } + throw new BadRequestError({ + message: "Unable to validate connection: verify credentials" + }); + } + + return config.credentials; +}; + +export const listWindmillWorkspaces = async (appConnection: TWindmillConnection) => { + const instanceUrl = await getWindmillInstanceUrl(appConnection); + const { accessToken } = appConnection.credentials; + + const resp = await request.get(`${instanceUrl}/api/workspaces/list`, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + + return resp.data.filter((workspace) => !workspace.deleted); +}; diff --git a/backend/src/services/app-connection/windmill/windmill-connection-schemas.ts b/backend/src/services/app-connection/windmill/windmill-connection-schemas.ts new file mode 100644 index 000000000..eb7f74ecd --- /dev/null +++ b/backend/src/services/app-connection/windmill/windmill-connection-schemas.ts @@ -0,0 +1,70 @@ +import z from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { WindmillConnectionMethod } from "./windmill-connection-enums"; + +export const WindmillConnectionAccessTokenCredentialsSchema = z.object({ + accessToken: z + .string() + .trim() + .min(1, "Access Token required") + .describe(AppConnections.CREDENTIALS.WINDMILL.accessToken), + instanceUrl: z + .string() + .trim() + .url("Invalid Instance URL") + .optional() + .describe(AppConnections.CREDENTIALS.WINDMILL.instanceUrl) +}); + +const BaseWindmillConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Windmill) }); + +export const WindmillConnectionSchema = BaseWindmillConnectionSchema.extend({ + method: z.literal(WindmillConnectionMethod.AccessToken), + credentials: WindmillConnectionAccessTokenCredentialsSchema +}); + +export const SanitizedWindmillConnectionSchema = z.discriminatedUnion("method", [ + BaseWindmillConnectionSchema.extend({ + method: z.literal(WindmillConnectionMethod.AccessToken), + credentials: WindmillConnectionAccessTokenCredentialsSchema.pick({ + instanceUrl: true + }) + }) +]); + +export const ValidateWindmillConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(WindmillConnectionMethod.AccessToken) + .describe(AppConnections.CREATE(AppConnection.Windmill).method), + credentials: WindmillConnectionAccessTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.Windmill).credentials + ) + }) +]); + +export const CreateWindmillConnectionSchema = ValidateWindmillConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.Windmill) +); + +export const UpdateWindmillConnectionSchema = z + .object({ + credentials: WindmillConnectionAccessTokenCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.Windmill).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Windmill)); + +export const WindmillConnectionListItemSchema = z.object({ + name: z.literal("Windmill"), + app: z.literal(AppConnection.Windmill), + methods: z.nativeEnum(WindmillConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/windmill/windmill-connection-service.ts b/backend/src/services/app-connection/windmill/windmill-connection-service.ts new file mode 100644 index 000000000..89306985f --- /dev/null +++ b/backend/src/services/app-connection/windmill/windmill-connection-service.ts @@ -0,0 +1,28 @@ +import { OrgServiceActor } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { listWindmillWorkspaces } from "./windmill-connection-fns"; +import { TWindmillConnection } from "./windmill-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +export const windmillConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listWorkspaces = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Windmill, connectionId, actor); + + try { + const workspaces = await listWindmillWorkspaces(appConnection); + return workspaces; + } catch (error) { + return []; + } + }; + + return { + listWorkspaces + }; +}; diff --git a/backend/src/services/app-connection/windmill/windmill-connection-types.ts b/backend/src/services/app-connection/windmill/windmill-connection-types.ts new file mode 100644 index 000000000..9747ce14b --- /dev/null +++ b/backend/src/services/app-connection/windmill/windmill-connection-types.ts @@ -0,0 +1,27 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateWindmillConnectionSchema, + ValidateWindmillConnectionCredentialsSchema, + WindmillConnectionSchema +} from "./windmill-connection-schemas"; + +export type TWindmillConnection = z.infer; + +export type TWindmillConnectionInput = z.infer & { + app: AppConnection.Windmill; +}; + +export type TValidateWindmillConnectionCredentialsSchema = typeof ValidateWindmillConnectionCredentialsSchema; + +export type TWindmillConnectionConfig = DiscriminativePick< + TWindmillConnectionInput, + "method" | "app" | "credentials" +> & { + orgId: string; +}; + +export type TWindmillWorkspace = { id: string; name: string; deleted: boolean }; diff --git a/backend/src/services/auth-token/auth-token-dal.ts b/backend/src/services/auth-token/auth-token-dal.ts index c058c13e8..221b691cf 100644 --- a/backend/src/services/auth-token/auth-token-dal.ts +++ b/backend/src/services/auth-token/auth-token-dal.ts @@ -12,9 +12,12 @@ export type TTokenDALFactory = ReturnType; export const tokenDALFactory = (db: TDbClient) => { const authOrm = ormify(db, TableName.AuthTokens); - const findOneTokenSession = async (filter: Partial): Promise => { + const findOneTokenSession = async ( + filter: Partial, + tx?: Knex + ): Promise => { try { - const doc = await db.replicaNode()(TableName.AuthTokenSession).where(filter).first(); + const doc = await (tx || db.replicaNode())(TableName.AuthTokenSession).where(filter).first(); return doc; } catch (error) { throw new DatabaseError({ error, name: "FindOneTokenSession" }); @@ -54,10 +57,11 @@ export const tokenDALFactory = (db: TDbClient) => { const insertTokenSession = async ( userId: string, ip: string, - userAgent: string + userAgent: string, + tx?: Knex ): Promise => { try { - const [session] = await db(TableName.AuthTokenSession) + const [session] = await (tx || db)(TableName.AuthTokenSession) .insert({ userId, ip, diff --git a/backend/src/services/auth-token/auth-token-service.ts b/backend/src/services/auth-token/auth-token-service.ts index 321abb5b3..2468e3c8a 100644 --- a/backend/src/services/auth-token/auth-token-service.ts +++ b/backend/src/services/auth-token/auth-token-service.ts @@ -1,13 +1,15 @@ import crypto from "node:crypto"; import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; +import { Knex } from "knex"; import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; -import { AuthModeJwtTokenPayload } from "../auth/auth-type"; +import { AuthModeJwtTokenPayload, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "../auth/auth-type"; import { TUserDALFactory } from "../user/user-dal"; import { TTokenDALFactory } from "./auth-token-dal"; import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types"; @@ -56,6 +58,12 @@ export const getTokenConfig = (tokenType: TokenType) => { const expiresAt = new Date(new Date().getTime() + 86400000); return { token, expiresAt }; } + case TokenType.TOKEN_EMAIL_PASSWORD_SETUP: { + // generate random hex + const token = crypto.randomBytes(16).toString("hex"); + const expiresAt = new Date(new Date().getTime() + 86400000); + return { token, expiresAt }; + } case TokenType.TOKEN_USER_UNLOCK: { const token = crypto.randomBytes(16).toString("hex"); const expiresAt = new Date(new Date().getTime() + 259200000); @@ -123,14 +131,13 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu return deletedToken?.[0]; }; - const getUserTokenSession = async ({ - userId, - ip, - userAgent - }: TIssueAuthTokenDTO): Promise => { - let session = await tokenDAL.findOneTokenSession({ userId, ip, userAgent }); + const getUserTokenSession = async ( + { userId, ip, userAgent }: TIssueAuthTokenDTO, + tx?: Knex + ): Promise => { + let session = await tokenDAL.findOneTokenSession({ userId, ip, userAgent }, tx); if (!session) { - session = await tokenDAL.insertTokenSession(userId, ip, userAgent); + session = await tokenDAL.insertTokenSession(userId, ip, userAgent, tx); } return session; }; @@ -144,6 +151,40 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu const revokeAllMySessions = async (userId: string) => tokenDAL.deleteTokenSession({ userId }); + const validateRefreshToken = async (refreshToken?: string) => { + const appCfg = getConfig(); + if (!refreshToken) + throw new NotFoundError({ + name: "AuthTokenNotFound", + message: "Failed to find refresh token" + }); + + const decodedToken = jwt.verify(refreshToken, appCfg.AUTH_SECRET) as AuthModeRefreshJwtTokenPayload; + + if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN) + throw new UnauthorizedError({ + message: "The token provided is not a refresh token", + name: "InvalidToken" + }); + + const tokenVersion = await getUserTokenSessionById(decodedToken.tokenVersionId, decodedToken.userId); + + if (!tokenVersion) + throw new UnauthorizedError({ + message: "Valid token version not found", + name: "InvalidToken" + }); + + if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) { + throw new UnauthorizedError({ + message: "Token version mismatch", + name: "InvalidToken" + }); + } + + return { decodedToken, tokenVersion }; + }; + // to parse jwt identity in inject identity plugin const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload) => { const session = await tokenDAL.findOneTokenSession({ @@ -182,6 +223,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu clearTokenSessionById, getTokenSessionByUser, revokeAllMySessions, + validateRefreshToken, fnValidateJwtIdentity, getUserTokenSessionById }; diff --git a/backend/src/services/auth-token/auth-token-types.ts b/backend/src/services/auth-token/auth-token-types.ts index 65d16850a..5f5843bc6 100644 --- a/backend/src/services/auth-token/auth-token-types.ts +++ b/backend/src/services/auth-token/auth-token-types.ts @@ -6,6 +6,7 @@ export enum TokenType { TOKEN_EMAIL_MFA = "emailMfa", TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation", TOKEN_EMAIL_PASSWORD_RESET = "passwordReset", + TOKEN_EMAIL_PASSWORD_SETUP = "passwordSetup", TOKEN_USER_UNLOCK = "userUnlock" } diff --git a/backend/src/services/auth/auth-fns.ts b/backend/src/services/auth/auth-fns.ts index 5f7aca812..ec6e0a303 100644 --- a/backend/src/services/auth/auth-fns.ts +++ b/backend/src/services/auth/auth-fns.ts @@ -45,6 +45,36 @@ export const validateSignUpAuthorization = (token: string, userId: string, valid if (decodedToken.userId !== userId) throw new UnauthorizedError(); }; +export const validatePasswordResetAuthorization = (token?: string) => { + if (!token) throw new UnauthorizedError(); + + const appCfg = getConfig(); + const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>token?.split(" ", 2) ?? [null, null]; + if (AUTH_TOKEN_TYPE === null) { + throw new UnauthorizedError({ message: "Missing Authorization Header in the request header." }); + } + if (AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") { + throw new UnauthorizedError({ + message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.` + }); + } + if (AUTH_TOKEN_VALUE === null) { + throw new UnauthorizedError({ + message: "Missing Authorization Body in the request header" + }); + } + + const decodedToken = jwt.verify(AUTH_TOKEN_VALUE, appCfg.AUTH_SECRET) as AuthModeProviderSignUpTokenPayload; + + if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) { + throw new UnauthorizedError({ + message: `The provided authentication token type is not supported.` + }); + } + + return decodedToken; +}; + export const enforceUserLockStatus = (isLocked: boolean, temporaryLockDateEnd?: Date | null) => { if (isLocked) { throw new ForbiddenRequestError({ diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index 83da4724e..e576d6768 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -1,7 +1,10 @@ import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; +import { Knex } from "knex"; -import { TUsers, UserDeviceSchema } from "@app/db/schemas"; +import { OrgMembershipRole, TUsers, UserDeviceSchema } from "@app/db/schemas"; +import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; @@ -10,6 +13,7 @@ import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { getUserPrivateKey } from "@app/lib/crypto/srp"; import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; +import { getUserAgentType } from "@app/server/plugins/audit-log"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; @@ -17,6 +21,7 @@ import { TokenType } from "../auth-token/auth-token-types"; import { TOrgDALFactory } from "../org/org-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { LoginMethod } from "../super-admin/super-admin-types"; +import { TTotpServiceFactory } from "../totp/totp-service"; import { TUserDALFactory } from "../user/user-dal"; import { enforceUserLockStatus, validateProviderAuthToken } from "./auth-fns"; import { @@ -26,13 +31,23 @@ import { TOauthTokenExchangeDTO, TVerifyMfaTokenDTO } from "./auth-login-type"; -import { AuthMethod, AuthModeJwtTokenPayload, AuthModeMfaJwtTokenPayload, AuthTokenType } from "./auth-type"; +import { + ActorType, + AuthMethod, + AuthModeJwtTokenPayload, + AuthModeMfaJwtTokenPayload, + AuthTokenType, + MfaMethod +} from "./auth-type"; +import { removeTrailingSlash } from "@app/lib/fn"; type TAuthLoginServiceFactoryDep = { userDAL: TUserDALFactory; orgDAL: TOrgDALFactory; tokenService: TAuthTokenServiceFactory; smtpService: TSmtpService; + totpService: Pick; + auditLogService: Pick; }; export type TAuthLoginFactory = ReturnType; @@ -40,20 +55,22 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService, - orgDAL + orgDAL, + totpService, + auditLogService }: TAuthLoginServiceFactoryDep) => { /* * Private * Not exported. This is to update user device list * If new device is found. Will be saved and a mail will be send */ - const updateUserDeviceSession = async (user: TUsers, ip: string, userAgent: string) => { + const updateUserDeviceSession = async (user: TUsers, ip: string, userAgent: string, tx?: Knex) => { const devices = await UserDeviceSchema.parseAsync(user.devices || []); const isDeviceSeen = devices.some((device) => device.ip === ip && device.userAgent === userAgent); if (!isDeviceSeen) { const newDeviceList = devices.concat([{ ip, userAgent }]); - await userDAL.updateById(user.id, { devices: JSON.stringify(newDeviceList) }); + await userDAL.updateById(user.id, { devices: JSON.stringify(newDeviceList) }, tx); if (user.email) { await smtpService.sendMail({ template: SmtpTemplates.NewDeviceJoin, @@ -94,28 +111,36 @@ export const authLoginServiceFactory = ({ * Check user device and send mail if new device * generate the auth and refresh token. fn shared by mfa verification and login verification with mfa disabled */ - const generateUserTokens = async ({ - user, - ip, - userAgent, - organizationId, - authMethod, - isMfaVerified - }: { - user: TUsers; - ip: string; - userAgent: string; - organizationId?: string; - authMethod: AuthMethod; - isMfaVerified?: boolean; - }) => { - const cfg = getConfig(); - await updateUserDeviceSession(user, ip, userAgent); - const tokenSession = await tokenService.getUserTokenSession({ - userAgent, + const generateUserTokens = async ( + { + user, ip, - userId: user.id - }); + userAgent, + organizationId, + authMethod, + isMfaVerified, + mfaMethod + }: { + user: TUsers; + ip: string; + userAgent: string; + organizationId?: string; + authMethod: AuthMethod; + isMfaVerified?: boolean; + mfaMethod?: MfaMethod; + }, + tx?: Knex + ) => { + const cfg = getConfig(); + await updateUserDeviceSession(user, ip, userAgent, tx); + const tokenSession = await tokenService.getUserTokenSession( + { + userAgent, + ip, + userId: user.id + }, + tx + ); if (!tokenSession) throw new Error("Failed to create token"); const accessToken = jwt.sign( @@ -126,7 +151,8 @@ export const authLoginServiceFactory = ({ tokenVersionId: tokenSession.id, accessVersion: tokenSession.accessVersion, organizationId, - isMfaVerified + isMfaVerified, + mfaMethod }, cfg.AUTH_SECRET, { expiresIn: cfg.JWT_AUTH_LIFETIME } @@ -140,7 +166,8 @@ export const authLoginServiceFactory = ({ tokenVersionId: tokenSession.id, refreshVersion: tokenSession.refreshVersion, organizationId, - isMfaVerified + isMfaVerified, + mfaMethod }, cfg.AUTH_SECRET, { expiresIn: cfg.JWT_REFRESH_LIFETIME } @@ -160,20 +187,25 @@ export const authLoginServiceFactory = ({ const userEnc = await userDAL.findUserEncKeyByUsername({ username: email }); + const serverCfg = await getServerCfg(); + if (!userEnc || (userEnc && !userEnc.isAccepted)) { + throw new Error("Failed to find user"); + } + if ( serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.EMAIL) && !providerAuthToken ) { - throw new BadRequestError({ - message: "Login with email is disabled by administrator." - }); - } - - if (!userEnc || (userEnc && !userEnc.isAccepted)) { - throw new Error("Failed to find user"); + // bypass server configuration when user is an organization admin - this is to prevent lockout + const userOrgs = await orgDAL.findAllOrgsByUserId(userEnc.userId); + if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) { + throw new BadRequestError({ + message: "Login with email is disabled by administrator." + }); + } } if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) { @@ -353,8 +385,12 @@ export const authLoginServiceFactory = ({ }); } - // send multi factor auth token if they it enabled - if ((selectedOrg.enforceMfa || user.isMfaEnabled) && user.email && !decodedToken.isMfaVerified) { + const shouldCheckMfa = selectedOrg.enforceMfa || user.isMfaEnabled; + const orgMfaMethod = selectedOrg.enforceMfa ? selectedOrg.selectedMfaMethod ?? MfaMethod.EMAIL : undefined; + const userMfaMethod = user.isMfaEnabled ? user.selectedMfaMethod ?? MfaMethod.EMAIL : undefined; + const mfaMethod = orgMfaMethod ?? userMfaMethod; + + if (shouldCheckMfa && (!decodedToken.isMfaVerified || decodedToken.mfaMethod !== mfaMethod)) { enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd); const mfaToken = jwt.sign( @@ -369,12 +405,14 @@ export const authLoginServiceFactory = ({ } ); - await sendUserMfaCode({ - userId: user.id, - email: user.email - }); + if (mfaMethod === MfaMethod.EMAIL && user.email) { + await sendUserMfaCode({ + userId: user.id, + email: user.email + }); + } - return { isMfaEnabled: true, mfa: mfaToken } as const; + return { isMfaEnabled: true, mfa: mfaToken, mfaMethod } as const; } const tokens = await generateUserTokens({ @@ -383,9 +421,59 @@ export const authLoginServiceFactory = ({ userAgent, ip: ipAddress, organizationId, - isMfaVerified: decodedToken.isMfaVerified + isMfaVerified: decodedToken.isMfaVerified, + mfaMethod: decodedToken.mfaMethod }); + // In the event of this being a break-glass request (non-saml / non-oidc, when either is enforced) + if ( + selectedOrg.authEnforced && + selectedOrg.bypassOrgAuthEnabled && + !isAuthMethodSaml(decodedToken.authMethod) && + decodedToken.authMethod !== AuthMethod.OIDC + ) { + await auditLogService.createAuditLog({ + orgId: organizationId, + ipAddress, + userAgent, + userAgentType: getUserAgentType(userAgent), + actor: { + type: ActorType.USER, + metadata: { + email: user.email, + userId: user.id, + username: user.username + } + }, + event: { + type: EventType.ORG_ADMIN_BYPASS_SSO, + metadata: {} + } + }); + + // Notify all admins via email (besides the actor) + const orgAdmins = await orgDAL.findOrgMembersByRole(organizationId, OrgMembershipRole.Admin); + const adminEmails = orgAdmins + .filter((admin) => admin.user.id !== user.id) + .map((admin) => admin.user.email) + .filter(Boolean) as string[]; + + if (adminEmails.length > 0) { + await smtpService.sendMail({ + recipients: adminEmails, + subjectLine: "Security Alert: Admin SSO Bypass", + substitutions: { + email: user.email, + timestamp: new Date().toISOString(), + ip: ipAddress, + userAgent, + siteUrl: removeTrailingSlash(cfg.SITE_URL || "https://app.infisical.com") + }, + template: SmtpTemplates.OrgAdminBreakglassAccess + }); + } + } + return { ...tokens, isMfaEnabled: false @@ -458,17 +546,39 @@ export const authLoginServiceFactory = ({ * Multi factor authentication verification of code * Third step of login in which user completes with mfa * */ - const verifyMfaToken = async ({ userId, mfaToken, mfaJwtToken, ip, userAgent, orgId }: TVerifyMfaTokenDTO) => { + const verifyMfaToken = async ({ + userId, + mfaToken, + mfaMethod, + mfaJwtToken, + ip, + userAgent, + orgId + }: TVerifyMfaTokenDTO) => { const appCfg = getConfig(); const user = await userDAL.findById(userId); enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd); try { - await tokenService.validateTokenForUser({ - type: TokenType.TOKEN_EMAIL_MFA, - userId, - code: mfaToken - }); + if (mfaMethod === MfaMethod.EMAIL) { + await tokenService.validateTokenForUser({ + type: TokenType.TOKEN_EMAIL_MFA, + userId, + code: mfaToken + }); + } else if (mfaMethod === MfaMethod.TOTP) { + if (mfaToken.length === 6) { + await totpService.verifyUserTotp({ + userId, + totp: mfaToken + }); + } else { + await totpService.verifyWithUserRecoveryCode({ + userId, + recoveryCode: mfaToken + }); + } + } } catch (err) { const updatedUser = await processFailedMfaAttempt(userId); if (updatedUser.isLocked) { @@ -513,7 +623,8 @@ export const authLoginServiceFactory = ({ userAgent, organizationId: orgId, authMethod: decodedToken.authMethod, - isMfaVerified: true + isMfaVerified: true, + mfaMethod }); return { token, user: userEnc }; @@ -529,28 +640,40 @@ export const authLoginServiceFactory = ({ switch (authMethod) { case AuthMethod.GITHUB: { if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITHUB)) { - throw new BadRequestError({ - message: "Login with Github is disabled by administrator.", - name: "Oauth 2 login" - }); + // bypass server configuration when user is an organization admin - this is to prevent lockout + const userOrgs = await orgDAL.findAllOrgsByUserId(user.id); + if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) { + throw new BadRequestError({ + message: "Login with Github is disabled by administrator.", + name: "Oauth 2 login" + }); + } } break; } case AuthMethod.GOOGLE: { if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GOOGLE)) { - throw new BadRequestError({ - message: "Login with Google is disabled by administrator.", - name: "Oauth 2 login" - }); + // bypass server configuration when user is an organization admin - this is to prevent lockout + const userOrgs = await orgDAL.findAllOrgsByUserId(user.id); + if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) { + throw new BadRequestError({ + message: "Login with Google is disabled by administrator.", + name: "Oauth 2 login" + }); + } } break; } case AuthMethod.GITLAB: { if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITLAB)) { - throw new BadRequestError({ - message: "Login with Gitlab is disabled by administrator.", - name: "Oauth 2 login" - }); + // bypass server configuration when user is an organization admin - this is to prevent lockout + const userOrgs = await orgDAL.findAllOrgsByUserId(user.id); + if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) { + throw new BadRequestError({ + message: "Login with Gitlab is disabled by administrator.", + name: "Oauth 2 login" + }); + } } break; } diff --git a/backend/src/services/auth/auth-login-type.ts b/backend/src/services/auth/auth-login-type.ts index db57d730e..d9d9520a8 100644 --- a/backend/src/services/auth/auth-login-type.ts +++ b/backend/src/services/auth/auth-login-type.ts @@ -1,4 +1,4 @@ -import { AuthMethod } from "./auth-type"; +import { AuthMethod, MfaMethod } from "./auth-type"; export type TLoginGenServerPublicKeyDTO = { email: string; @@ -19,6 +19,7 @@ export type TLoginClientProofDTO = { export type TVerifyMfaTokenDTO = { userId: string; mfaToken: string; + mfaMethod: MfaMethod; mfaJwtToken: string; ip: string; userAgent: string; diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts index 0e6558966..14fb58258 100644 --- a/backend/src/services/auth/auth-password-service.ts +++ b/backend/src/services/auth/auth-password-service.ts @@ -4,20 +4,35 @@ import jwt from "jsonwebtoken"; import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; +import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; +import { generateUserSrpKeys } from "@app/lib/crypto/srp"; +import { BadRequestError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; +import { OrgServiceActor } from "@app/lib/types"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; +import { TTotpConfigDALFactory } from "../totp/totp-config-dal"; import { TUserDALFactory } from "../user/user-dal"; +import { UserEncryption } from "../user/user-types"; import { TAuthDALFactory } from "./auth-dal"; -import { TChangePasswordDTO, TCreateBackupPrivateKeyDTO, TResetPasswordViaBackupKeyDTO } from "./auth-password-type"; -import { AuthTokenType } from "./auth-type"; +import { + ResetPasswordV2Type, + TChangePasswordDTO, + TCreateBackupPrivateKeyDTO, + TResetPasswordV2DTO, + TResetPasswordViaBackupKeyDTO, + TSetupPasswordViaBackupKeyDTO +} from "./auth-password-type"; +import { ActorType, AuthMethod, AuthTokenType } from "./auth-type"; type TAuthPasswordServiceFactoryDep = { authDAL: TAuthDALFactory; userDAL: TUserDALFactory; tokenService: TAuthTokenServiceFactory; smtpService: TSmtpService; + totpConfigDAL: Pick; }; export type TAuthPasswordFactory = ReturnType; @@ -25,7 +40,8 @@ export const authPaswordServiceFactory = ({ authDAL, userDAL, tokenService, - smtpService + smtpService, + totpConfigDAL }: TAuthPasswordServiceFactoryDep) => { /* * Pre setup for pass change with srp protocol @@ -104,26 +120,31 @@ export const authPaswordServiceFactory = ({ * Email password reset flow via email. Step 1 send email */ const sendPasswordResetEmail = async (email: string) => { - const user = await userDAL.findUserByUsername(email); - // ignore as user is not found to avoid an outside entity to identify infisical registered accounts - if (!user || (user && !user.isAccepted)) return; + const sendEmail = async () => { + const user = await userDAL.findUserByUsername(email); - const cfg = getConfig(); - const token = await tokenService.createTokenForUser({ - type: TokenType.TOKEN_EMAIL_PASSWORD_RESET, - userId: user.id - }); + if (user && user.isAccepted) { + const cfg = getConfig(); + const token = await tokenService.createTokenForUser({ + type: TokenType.TOKEN_EMAIL_PASSWORD_RESET, + userId: user.id + }); - await smtpService.sendMail({ - template: SmtpTemplates.ResetPassword, - recipients: [email], - subjectLine: "Infisical password reset", - substitutions: { - email, - token, - callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : "" + await smtpService.sendMail({ + template: SmtpTemplates.ResetPassword, + recipients: [email], + subjectLine: "Infisical password reset", + substitutions: { + email, + token, + callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : "" + } + }); } - }); + }; + + // note(daniel): run in background to prevent timing attacks + void sendEmail().catch((err) => logger.error(err, "Failed to send password reset email")); }; /* @@ -132,6 +153,11 @@ export const authPaswordServiceFactory = ({ const verifyPasswordResetEmail = async (email: string, code: string) => { const cfg = getConfig(); const user = await userDAL.findUserByUsername(email); + + const userEnc = await userDAL.findUserEncKeyByUserId(user.id); + + if (!userEnc) throw new BadRequestError({ message: "Failed to find user encryption data" }); + // ignore as user is not found to avoid an outside entity to identify infisical registered accounts if (!user || (user && !user.isAccepted)) { throw new Error("Failed email verification for pass reset"); @@ -152,8 +178,91 @@ export const authPaswordServiceFactory = ({ { expiresIn: cfg.JWT_SIGNUP_LIFETIME } ); - return { token, user }; + return { token, user, userEncryptionVersion: userEnc.encryptionVersion as UserEncryption }; }; + + const resetPasswordV2 = async ({ userId, newPassword, type, oldPassword }: TResetPasswordV2DTO) => { + const cfg = getConfig(); + + const user = await userDAL.findUserEncKeyByUserId(userId); + if (!user) { + throw new BadRequestError({ message: `User encryption key not found for user with ID '${userId}'` }); + } + + if (!user.hashedPassword) { + throw new BadRequestError({ message: "Unable to reset password, no password is set" }); + } + + if (!user.authMethods?.includes(AuthMethod.EMAIL)) { + throw new BadRequestError({ message: "Unable to reset password, no email authentication method is configured" }); + } + + // we check the old password if the user is resetting their password while logged in + if (type === ResetPasswordV2Type.LoggedInReset) { + if (!oldPassword) { + throw new BadRequestError({ message: "Current password is required." }); + } + + const isValid = await bcrypt.compare(oldPassword, user.hashedPassword); + if (!isValid) { + throw new BadRequestError({ message: "Incorrect current password." }); + } + } + + const newHashedPassword = await bcrypt.hash(newPassword, cfg.BCRYPT_SALT_ROUND); + + // we need to get the original private key first for v2 + let privateKey: string; + if ( + user.serverEncryptedPrivateKey && + user.serverEncryptedPrivateKeyTag && + user.serverEncryptedPrivateKeyIV && + user.serverEncryptedPrivateKeyEncoding && + user.encryptionVersion === UserEncryption.V2 + ) { + privateKey = infisicalSymmetricDecrypt({ + iv: user.serverEncryptedPrivateKeyIV, + tag: user.serverEncryptedPrivateKeyTag, + ciphertext: user.serverEncryptedPrivateKey, + keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding + }); + } else { + throw new BadRequestError({ + message: "Cannot reset password without current credentials or recovery method", + name: "Reset password" + }); + } + + const encKeys = await generateUserSrpKeys(user.username, newPassword, { + publicKey: user.publicKey, + privateKey + }); + + const { tag, iv, ciphertext, encoding } = infisicalSymmetricEncypt(privateKey); + + await userDAL.updateUserEncryptionByUserId(userId, { + hashedPassword: newHashedPassword, + + // srp params + salt: encKeys.salt, + verifier: encKeys.verifier, + + protectedKey: encKeys.protectedKey, + protectedKeyIV: encKeys.protectedKeyIV, + protectedKeyTag: encKeys.protectedKeyTag, + encryptedPrivateKey: encKeys.encryptedPrivateKey, + iv: encKeys.encryptedPrivateKeyIV, + tag: encKeys.encryptedPrivateKeyTag, + + serverEncryptedPrivateKey: ciphertext, + serverEncryptedPrivateKeyIV: iv, + serverEncryptedPrivateKeyTag: tag, + serverEncryptedPrivateKeyEncoding: encoding + }); + + await tokenService.revokeAllMySessions(userId); + }; + /* * Reset password of a user via backup key * */ @@ -166,8 +275,13 @@ export const authPaswordServiceFactory = ({ verifier, encryptedPrivateKeyIV, encryptedPrivateKeyTag, - userId + userId, + password }: TResetPasswordViaBackupKeyDTO) => { + const cfg = getConfig(); + + const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND); + await userDAL.updateUserEncryptionByUserId(userId, { encryptionVersion: 2, protectedKey, @@ -177,7 +291,8 @@ export const authPaswordServiceFactory = ({ iv: encryptedPrivateKeyIV, tag: encryptedPrivateKeyTag, salt, - verifier + verifier, + hashedPassword }); await userDAL.updateById(userId, { @@ -185,6 +300,12 @@ export const authPaswordServiceFactory = ({ temporaryLockDateEnd: null, consecutiveFailedMfaAttempts: 0 }); + + /* we reset the mobile authenticator configs of the user + because we want this to be one of the recovery modes from account lockout */ + await totpConfigDAL.delete({ + userId + }); }; /* @@ -258,6 +379,108 @@ export const authPaswordServiceFactory = ({ return backupKey; }; + const sendPasswordSetupEmail = async (actor: OrgServiceActor) => { + if (actor.type !== ActorType.USER) + throw new BadRequestError({ message: `Actor of type ${actor.type} cannot set password` }); + + const user = await userDAL.findById(actor.id); + + if (!user) throw new BadRequestError({ message: `Could not find user with ID ${actor.id}` }); + + if (!user.isAccepted || !user.authMethods) + throw new BadRequestError({ message: `You must complete signup to set a password` }); + + const cfg = getConfig(); + + const token = await tokenService.createTokenForUser({ + type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP, + userId: user.id + }); + + const email = user.email ?? user.username; + + await smtpService.sendMail({ + template: SmtpTemplates.SetupPassword, + recipients: [email], + subjectLine: "Infisical Password Setup", + substitutions: { + email, + token, + callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-setup` : "" + } + }); + }; + + const setupPassword = async ( + { + encryptedPrivateKey, + protectedKeyTag, + protectedKey, + protectedKeyIV, + salt, + verifier, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + password, + token + }: TSetupPasswordViaBackupKeyDTO, + actor: OrgServiceActor + ) => { + try { + await tokenService.validateTokenForUser({ + type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP, + userId: actor.id, + code: token + }); + } catch (e) { + throw new BadRequestError({ message: "Expired or invalid token. Please try again." }); + } + + await userDAL.transaction(async (tx) => { + const user = await userDAL.findById(actor.id, tx); + + if (!user) throw new BadRequestError({ message: `Could not find user with ID ${actor.id}` }); + + if (!user.isAccepted || !user.authMethods) + throw new BadRequestError({ message: `You must complete signup to set a password` }); + + if (!user.authMethods.includes(AuthMethod.EMAIL)) { + await userDAL.updateById( + actor.id, + { + authMethods: [...user.authMethods, AuthMethod.EMAIL] + }, + tx + ); + } + + const cfg = getConfig(); + + const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND); + + await userDAL.updateUserEncryptionByUserId( + actor.id, + { + encryptionVersion: 2, + protectedKey, + protectedKeyIV, + protectedKeyTag, + encryptedPrivateKey, + iv: encryptedPrivateKeyIV, + tag: encryptedPrivateKeyTag, + salt, + verifier, + hashedPassword, + serverPrivateKey: null, + clientPublicKey: null + }, + tx + ); + }); + + await tokenService.revokeAllMySessions(actor.id); + }; + return { generateServerPubKey, changePassword, @@ -265,6 +488,9 @@ export const authPaswordServiceFactory = ({ sendPasswordResetEmail, verifyPasswordResetEmail, createBackupPrivateKey, - getBackupPrivateKeyOfUser + getBackupPrivateKeyOfUser, + sendPasswordSetupEmail, + setupPassword, + resetPasswordV2 }; }; diff --git a/backend/src/services/auth/auth-password-type.ts b/backend/src/services/auth/auth-password-type.ts index a52374506..b3b14c3b4 100644 --- a/backend/src/services/auth/auth-password-type.ts +++ b/backend/src/services/auth/auth-password-type.ts @@ -13,6 +13,18 @@ export type TChangePasswordDTO = { password: string; }; +export enum ResetPasswordV2Type { + Recovery = "recovery", + LoggedInReset = "logged-in-reset" +} + +export type TResetPasswordV2DTO = { + type: ResetPasswordV2Type; + userId: string; + newPassword: string; + oldPassword?: string; +}; + export type TResetPasswordViaBackupKeyDTO = { userId: string; protectedKey: string; @@ -23,6 +35,20 @@ export type TResetPasswordViaBackupKeyDTO = { encryptedPrivateKeyTag: string; salt: string; verifier: string; + password: string; +}; + +export type TSetupPasswordViaBackupKeyDTO = { + protectedKey: string; + protectedKeyIV: string; + protectedKeyTag: string; + encryptedPrivateKey: string; + encryptedPrivateKeyIV: string; + encryptedPrivateKeyTag: string; + salt: string; + verifier: string; + password: string; + token: string; }; export type TCreateBackupPrivateKeyDTO = { diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts index b55d01308..a652c2a5b 100644 --- a/backend/src/services/auth/auth-signup-service.ts +++ b/backend/src/services/auth/auth-signup-service.ts @@ -9,7 +9,7 @@ import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { getConfig } from "@app/lib/config/env"; import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp"; -import { NotFoundError } from "@app/lib/errors"; +import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { isDisposableEmail } from "@app/lib/validator"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; @@ -23,6 +23,7 @@ import { TOrgServiceFactory } from "../org/org-service"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; +import { getServerCfg } from "../super-admin/super-admin-service"; import { TUserDALFactory } from "../user/user-dal"; import { UserEncryption } from "../user/user-types"; import { TAuthDALFactory } from "./auth-dal"; @@ -151,6 +152,8 @@ export const authSignupServiceFactory = ({ authorization }: TCompleteAccountSignupDTO) => { const appCfg = getConfig(); + const serverCfg = await getServerCfg(); + const user = await userDAL.findOne({ username: email }); if (!user || (user && user.isAccepted)) { throw new Error("Failed to complete account for complete user"); @@ -163,6 +166,12 @@ export const authSignupServiceFactory = ({ authMethod = userAuthMethod; organizationId = orgId; } else { + // disallow signup if disabled. we are not doing this for providerAuthToken because we allow signups via saml or sso + if (!serverCfg.allowSignUp) { + throw new ForbiddenRequestError({ + message: "Signup's are disabled" + }); + } validateSignUpAuthorization(authorization, user.id); } diff --git a/backend/src/services/auth/auth-type.ts b/backend/src/services/auth/auth-type.ts index 44b775945..497414a60 100644 --- a/backend/src/services/auth/auth-type.ts +++ b/backend/src/services/auth/auth-type.ts @@ -35,11 +35,13 @@ export enum AuthMode { export enum ActorType { // would extend to AWS, Azure, ... PLATFORM = "platform", // Useful for when we want to perform logging on automated actions such as integration syncs. + KMIP_CLIENT = "kmipClient", USER = "user", // userIdentity SERVICE = "service", IDENTITY = "identity", Machine = "machine", - SCIM_CLIENT = "scimClient" + SCIM_CLIENT = "scimClient", + UNKNOWN_USER = "unknownUser" } // This will be null unless the token-type is JWT @@ -53,6 +55,7 @@ export type AuthModeJwtTokenPayload = { accessVersion: number; organizationId?: string; isMfaVerified?: boolean; + mfaMethod?: MfaMethod; }; export type AuthModeMfaJwtTokenPayload = { @@ -71,6 +74,7 @@ export type AuthModeRefreshJwtTokenPayload = { refreshVersion: number; organizationId?: string; isMfaVerified?: boolean; + mfaMethod?: MfaMethod; }; export type AuthModeProviderJwtTokenPayload = { @@ -85,3 +89,8 @@ export type AuthModeProviderSignUpTokenPayload = { authTokenType: AuthTokenType.SIGNUP_TOKEN; userId: string; }; + +export enum MfaMethod { + EMAIL = "email", + TOTP = "totp" +} diff --git a/backend/src/services/certificate-authority/certificate-authority-fns.ts b/backend/src/services/certificate-authority/certificate-authority-fns.ts index efb582d88..d2c87e772 100644 --- a/backend/src/services/certificate-authority/certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/certificate-authority-fns.ts @@ -15,7 +15,7 @@ import { /* eslint-disable no-bitwise */ export const createSerialNumber = () => { - const randomBytes = crypto.randomBytes(20); + const randomBytes = crypto.randomBytes(20); // 20 bytes = 160 bits randomBytes[0] &= 0x7f; // ensure the first bit is 0 return randomBytes.toString("hex"); }; diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 06efcf9e3..499a25741 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -2,14 +2,16 @@ import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; import crypto, { KeyObject } from "crypto"; -import ms from "ms"; import { z } from "zod"; -import { TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas"; +import { ActionProjectType, ProjectType, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { ms } from "@app/lib/ms"; +import { isFQDN } from "@app/lib/validator/validate-url"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; @@ -58,7 +60,6 @@ import { TSignIntermediateDTO, TUpdateCaDTO } from "./certificate-authority-types"; -import { hostnameRegex } from "./certificate-authority-validators"; type TCertificateAuthorityServiceFactoryDep = { certificateAuthorityDAL: Pick< @@ -77,7 +78,10 @@ type TCertificateAuthorityServiceFactoryDep = { certificateBodyDAL: Pick; pkiCollectionDAL: Pick; pkiCollectionItemDAL: Pick; - projectDAL: Pick; + projectDAL: Pick< + TProjectDALFactory, + "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction" | "getProjectFromSplitId" + >; kmsService: Pick; permissionService: Pick; }; @@ -123,14 +127,24 @@ export const certificateAuthorityServiceFactory = ({ }: TCreateCaDTO) => { const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); + let projectId = project.id; - const { permission } = await permissionService.getProjectPermission( + const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId( + projectId, + ProjectType.CertificateManager + ); + if (certManagerProjectFromSplit) { + projectId = certManagerProjectFromSplit.id; + } + + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - project.id, + projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, @@ -161,7 +175,7 @@ export const certificateAuthorityServiceFactory = ({ const ca = await certificateAuthorityDAL.create( { - projectId: project.id, + projectId, type, organization, ou, @@ -185,7 +199,7 @@ export const certificateAuthorityServiceFactory = ({ ); const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ - projectId: project.id, + projectId, projectDAL, kmsService }); @@ -292,13 +306,14 @@ export const certificateAuthorityServiceFactory = ({ const ca = await certificateAuthorityDAL.findById(caId); if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - ca.projectId, + projectId: ca.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities @@ -323,13 +338,14 @@ export const certificateAuthorityServiceFactory = ({ const ca = await certificateAuthorityDAL.findById(caId); if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - ca.projectId, + projectId: ca.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, @@ -348,13 +364,14 @@ export const certificateAuthorityServiceFactory = ({ const ca = await certificateAuthorityDAL.findById(caId); if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - ca.projectId, + projectId: ca.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, @@ -373,13 +390,14 @@ export const certificateAuthorityServiceFactory = ({ const ca = await certificateAuthorityDAL.findById(caId); if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - ca.projectId, + projectId: ca.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, @@ -434,13 +452,14 @@ export const certificateAuthorityServiceFactory = ({ if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - ca.projectId, + projectId: ca.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, @@ -704,13 +723,14 @@ export const certificateAuthorityServiceFactory = ({ const ca = await certificateAuthorityDAL.findById(caId); if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - ca.projectId, + projectId: ca.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, @@ -739,13 +759,14 @@ export const certificateAuthorityServiceFactory = ({ if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - ca.projectId, + projectId: ca.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, @@ -819,13 +840,14 @@ export const certificateAuthorityServiceFactory = ({ const ca = await certificateAuthorityDAL.findById(caId); if (!ca) throw new NotFoundError({ message: "CA not found" }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - ca.projectId, + projectId: ca.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, @@ -965,13 +987,14 @@ export const certificateAuthorityServiceFactory = ({ const ca = await certificateAuthorityDAL.findById(caId); if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - ca.projectId, + projectId: ca.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, @@ -995,9 +1018,7 @@ export const certificateAuthorityServiceFactory = ({ const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength; // validate imported certificate and certificate chain - const certificates = certificateChain - .match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g) - ?.map((cert) => new x509.X509Certificate(cert)); + const certificates = extractX509CertFromChain(certificateChain)?.map((cert) => new x509.X509Certificate(cert)); if (!certificates) throw new BadRequestError({ message: "Failed to parse certificate chain" }); @@ -1127,13 +1148,14 @@ export const certificateAuthorityServiceFactory = ({ throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - ca.projectId, + projectId: ca.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates); @@ -1302,7 +1324,7 @@ export const certificateAuthorityServiceFactory = ({ } // check if the altName is a valid hostname - if (hostnameRegex.test(altName)) { + if (isFQDN(altName, { allow_wildcard: true })) { return { type: "dns", value: altName @@ -1455,13 +1477,14 @@ export const certificateAuthorityServiceFactory = ({ } if (!dto.isInternal) { - const { permission } = await permissionService.getProjectPermission( - dto.actor, - dto.actorId, - ca.projectId, - dto.actorAuthMethod, - dto.actorOrgId - ); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.actor, + actorId: dto.actorId, + projectId: ca.projectId, + actorAuthMethod: dto.actorAuthMethod, + actorOrgId: dto.actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, @@ -1678,7 +1701,7 @@ export const certificateAuthorityServiceFactory = ({ } // check if the altName is a valid hostname - if (hostnameRegex.test(altName)) { + if (isFQDN(altName, { allow_wildcard: true })) { return { type: "dns", value: altName @@ -1795,7 +1818,8 @@ export const certificateAuthorityServiceFactory = ({ certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(), issuingCaCertificate, serialNumber, - ca + ca, + commonName: cn }; }; @@ -1812,13 +1836,14 @@ export const certificateAuthorityServiceFactory = ({ const ca = await certificateAuthorityDAL.findById(caId); if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - ca.projectId, + projectId: ca.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, diff --git a/backend/src/services/certificate-authority/certificate-authority-validators.ts b/backend/src/services/certificate-authority/certificate-authority-validators.ts index 16e7dcf49..979a3b9c5 100644 --- a/backend/src/services/certificate-authority/certificate-authority-validators.ts +++ b/backend/src/services/certificate-authority/certificate-authority-validators.ts @@ -1,5 +1,8 @@ import { z } from "zod"; +import { isValidIp } from "@app/lib/ip"; +import { isFQDN } from "@app/lib/validator/validate-url"; + const isValidDate = (dateString: string) => { const date = new Date(dateString); return !Number.isNaN(date.getTime()); @@ -7,7 +10,6 @@ const isValidDate = (dateString: string) => { export const validateCaDateField = z.string().trim().refine(isValidDate, { message: "Invalid date format" }); -export const hostnameRegex = /^(?!:\/\/)(\*\.)?([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/; export const validateAltNamesField = z .string() .trim() @@ -25,7 +27,7 @@ export const validateAltNamesField = z if (data === "") return true; // Split and validate each alt name return data.split(", ").every((name) => { - return hostnameRegex.test(name) || z.string().email().safeParse(name).success; + return isFQDN(name, { allow_wildcard: true }) || z.string().email().safeParse(name).success || isValidIp(name); }); }, { diff --git a/backend/src/services/certificate-template/certificate-template-fns.ts b/backend/src/services/certificate-template/certificate-template-fns.ts index 597be7eb2..b6ec8d755 100644 --- a/backend/src/services/certificate-template/certificate-template-fns.ts +++ b/backend/src/services/certificate-template/certificate-template-fns.ts @@ -1,7 +1,8 @@ -import ms from "ms"; +import RE2 from "re2"; import { TCertificateTemplates } from "@app/db/schemas"; import { BadRequestError } from "@app/lib/errors"; +import { ms } from "@app/lib/ms"; export const validateCertificateDetailsAgainstTemplate = ( cert: { @@ -12,7 +13,8 @@ export const validateCertificateDetailsAgainstTemplate = ( }, template: TCertificateTemplates ) => { - const commonNameRegex = new RegExp(template.commonName); + // these are validated in router using validateTemplateRegexField + const commonNameRegex = new RE2(template.commonName); if (!commonNameRegex.test(cert.commonName)) { throw new BadRequestError({ message: "Invalid common name based on template policy" @@ -25,7 +27,7 @@ export const validateCertificateDetailsAgainstTemplate = ( }); } - const subjectAlternativeNameRegex = new RegExp(template.subjectAlternativeName); + const subjectAlternativeNameRegex = new RE2(template.subjectAlternativeName); cert.altNames.forEach((altName) => { if (!subjectAlternativeNameRegex.test(altName)) { throw new BadRequestError({ diff --git a/backend/src/services/certificate-template/certificate-template-service.ts b/backend/src/services/certificate-template/certificate-template-service.ts index cbe893719..04bf76f5c 100644 --- a/backend/src/services/certificate-template/certificate-template-service.ts +++ b/backend/src/services/certificate-template/certificate-template-service.ts @@ -2,10 +2,11 @@ import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; import bcrypt from "bcrypt"; -import { TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas"; +import { ActionProjectType, TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -67,13 +68,14 @@ export const certificateTemplateServiceFactory = ({ message: `CA with ID ${caId} not found` }); } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - ca.projectId, + projectId: ca.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, @@ -128,13 +130,14 @@ export const certificateTemplateServiceFactory = ({ }); } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - certTemplate.projectId, + projectId: certTemplate.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, @@ -185,13 +188,14 @@ export const certificateTemplateServiceFactory = ({ }); } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - certTemplate.projectId, + projectId: certTemplate.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, @@ -211,13 +215,14 @@ export const certificateTemplateServiceFactory = ({ }); } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - certTemplate.projectId, + projectId: certTemplate.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, @@ -235,7 +240,8 @@ export const certificateTemplateServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + disableBootstrapCertValidation }: TCreateEstConfigurationDTO) => { const plan = await licenseService.getPlan(actorOrgId); if (!plan.pkiEst) { @@ -251,13 +257,14 @@ export const certificateTemplateServiceFactory = ({ }); } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - certTemplate.projectId, + projectId: certTemplate.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, @@ -266,39 +273,43 @@ export const certificateTemplateServiceFactory = ({ const appCfg = getConfig(); - const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ - projectId: certTemplate.projectId, - projectDAL, - kmsService - }); + let encryptedCaChain: Buffer | undefined; + if (caChain) { + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ + projectId: certTemplate.projectId, + projectDAL, + kmsService + }); - // validate CA chain - const certificates = caChain - .match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g) - ?.map((cert) => new x509.X509Certificate(cert)); + // validate CA chain + const certificates = extractX509CertFromChain(caChain)?.map((cert) => new x509.X509Certificate(cert)); - if (!certificates) { - throw new BadRequestError({ message: "Failed to parse certificate chain" }); + if (!certificates) { + throw new BadRequestError({ message: "Failed to parse certificate chain" }); + } + + if (!(await isCertChainValid(certificates))) { + throw new BadRequestError({ message: "Invalid certificate chain" }); + } + + const kmsEncryptor = await kmsService.encryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); + + const { cipherTextBlob } = await kmsEncryptor({ + plainText: Buffer.from(caChain) + }); + + encryptedCaChain = cipherTextBlob; } - if (!(await isCertChainValid(certificates))) { - throw new BadRequestError({ message: "Invalid certificate chain" }); - } - - const kmsEncryptor = await kmsService.encryptWithKmsKey({ - kmsId: certificateManagerKmsId - }); - - const { cipherTextBlob: encryptedCaChain } = await kmsEncryptor({ - plainText: Buffer.from(caChain) - }); - const hashedPassphrase = await bcrypt.hash(passphrase, appCfg.SALT_ROUNDS); const estConfig = await certificateTemplateEstConfigDAL.create({ certificateTemplateId, hashedPassphrase, encryptedCaChain, - isEnabled + isEnabled, + disableBootstrapCertValidation }); return { ...estConfig, projectId: certTemplate.projectId }; @@ -312,7 +323,8 @@ export const certificateTemplateServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + disableBootstrapCertValidation }: TUpdateEstConfigurationDTO) => { const plan = await licenseService.getPlan(actorOrgId); if (!plan.pkiEst) { @@ -328,13 +340,14 @@ export const certificateTemplateServiceFactory = ({ }); } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - certTemplate.projectId, + projectId: certTemplate.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, @@ -360,13 +373,12 @@ export const certificateTemplateServiceFactory = ({ }); const updatedData: TCertificateTemplateEstConfigsUpdate = { - isEnabled + isEnabled, + disableBootstrapCertValidation }; if (caChain) { - const certificates = caChain - .match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g) - ?.map((cert) => new x509.X509Certificate(cert)); + const certificates = extractX509CertFromChain(caChain)?.map((cert) => new x509.X509Certificate(cert)); if (!certificates) { throw new BadRequestError({ message: "Failed to parse certificate chain" }); @@ -408,13 +420,14 @@ export const certificateTemplateServiceFactory = ({ } if (!dto.isInternal) { - const { permission } = await permissionService.getProjectPermission( - dto.actor, - dto.actorId, - certTemplate.projectId, - dto.actorAuthMethod, - dto.actorOrgId - ); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.actor, + actorId: dto.actorId, + projectId: certTemplate.projectId, + actorAuthMethod: dto.actorAuthMethod, + actorOrgId: dto.actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, @@ -442,18 +455,24 @@ export const certificateTemplateServiceFactory = ({ kmsId: certificateManagerKmsId }); - const decryptedCaChain = await kmsDecryptor({ - cipherTextBlob: estConfig.encryptedCaChain - }); + let decryptedCaChain = ""; + if (estConfig.encryptedCaChain) { + decryptedCaChain = ( + await kmsDecryptor({ + cipherTextBlob: estConfig.encryptedCaChain + }) + ).toString(); + } return { certificateTemplateId, id: estConfig.id, isEnabled: estConfig.isEnabled, - caChain: decryptedCaChain.toString(), + caChain: decryptedCaChain, hashedPassphrase: estConfig.hashedPassphrase, projectId: certTemplate.projectId, - orgId: certTemplate.orgId + orgId: certTemplate.orgId, + disableBootstrapCertValidation: estConfig.disableBootstrapCertValidation }; }; diff --git a/backend/src/services/certificate-template/certificate-template-types.ts b/backend/src/services/certificate-template/certificate-template-types.ts index 6d6488f2c..cdccb6a2d 100644 --- a/backend/src/services/certificate-template/certificate-template-types.ts +++ b/backend/src/services/certificate-template/certificate-template-types.ts @@ -34,9 +34,10 @@ export type TDeleteCertTemplateDTO = { export type TCreateEstConfigurationDTO = { certificateTemplateId: string; - caChain: string; + caChain?: string; passphrase: string; isEnabled: boolean; + disableBootstrapCertValidation: boolean; } & Omit; export type TUpdateEstConfigurationDTO = { @@ -44,6 +45,7 @@ export type TUpdateEstConfigurationDTO = { caChain?: string; passphrase?: string; isEnabled?: boolean; + disableBootstrapCertValidation?: boolean; } & Omit; export type TGetEstConfigurationDTO = diff --git a/backend/src/services/certificate-template/certificate-template-validators.ts b/backend/src/services/certificate-template/certificate-template-validators.ts index 41a06b05d..60694b598 100644 --- a/backend/src/services/certificate-template/certificate-template-validators.ts +++ b/backend/src/services/certificate-template/certificate-template-validators.ts @@ -1,13 +1,27 @@ import safe from "safe-regex"; import z from "zod"; +import { CharacterType, characterValidator } from "@app/lib/validator/validate-string"; + export const validateTemplateRegexField = z .string() .min(1) .max(100) - .regex(/^[a-zA-Z0-9 *@\-\\.\\]+$/, { - message: "Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed." - }) + .refine( + (val) => + characterValidator([ + CharacterType.AlphaNumeric, + CharacterType.Spaces, // (space) + CharacterType.Asterisk, // * + CharacterType.At, // @ + CharacterType.Hyphen, // - + CharacterType.Period, // . + CharacterType.Backslash // \ + ])(val), + { + message: "Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed." + } + ) // we ensure that the inputted pattern is computationally safe by limiting star height to 1 .refine((v) => safe(v), { message: "Unsafe REGEX pattern" diff --git a/backend/src/services/certificate/certificate-fns.ts b/backend/src/services/certificate/certificate-fns.ts index 1768a5011..45ad5963c 100644 --- a/backend/src/services/certificate/certificate-fns.ts +++ b/backend/src/services/certificate/certificate-fns.ts @@ -40,3 +40,9 @@ export const isCertChainValid = async (certificates: x509.X509Certificate[]) => // chain.build() implicitly verifies the chain return chainItems.length === certificates.length; }; + +export const constructPemChainFromCerts = (certificates: x509.X509Certificate[]) => + certificates + .map((cert) => cert.toString("pem")) + .join("\n") + .trim(); diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index 8dc2de901..0ca0d64c6 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; +import { ActionProjectType } from "@app/db/schemas"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -49,13 +50,14 @@ export const certificateServiceFactory = ({ const cert = await certificateDAL.findOne({ serialNumber }); const ca = await certificateAuthorityDAL.findById(cert.caId); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - ca.projectId, + projectId: ca.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); @@ -72,13 +74,14 @@ export const certificateServiceFactory = ({ const cert = await certificateDAL.findOne({ serialNumber }); const ca = await certificateAuthorityDAL.findById(cert.caId); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - ca.projectId, + projectId: ca.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates); @@ -106,13 +109,14 @@ export const certificateServiceFactory = ({ const cert = await certificateDAL.findOne({ serialNumber }); const ca = await certificateAuthorityDAL.findById(cert.caId); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - ca.projectId, + projectId: ca.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates); @@ -152,13 +156,14 @@ export const certificateServiceFactory = ({ const cert = await certificateDAL.findOne({ serialNumber }); const ca = await certificateAuthorityDAL.findById(cert.caId); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - ca.projectId, + projectId: ca.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); diff --git a/backend/src/services/cmek/cmek-service.ts b/backend/src/services/cmek/cmek-service.ts index c8e1b932a..b968a8951 100644 --- a/backend/src/services/cmek/cmek-service.ts +++ b/backend/src/services/cmek/cmek-service.ts @@ -1,12 +1,20 @@ import { ForbiddenError } from "@casl/ability"; +import { ActionProjectType, ProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { SigningAlgorithm } from "@app/lib/crypto/sign"; +import { DatabaseErrorCode } from "@app/lib/error-codes"; +import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; import { TCmekDecryptDTO, TCmekEncryptDTO, + TCmekGetPublicKeyDTO, + TCmekKeyEncryptionAlgorithm, + TCmekListSigningAlgorithmsDTO, + TCmekSignDTO, + TCmekVerifyDTO, TCreateCmekDTO, TListCmeksByProjectIdDTO, TUpdabteCmekByIdDTO @@ -14,111 +22,205 @@ import { import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsKeyUsage } from "../kms/kms-types"; +import { TProjectDALFactory } from "../project/project-dal"; + type TCmekServiceFactoryDep = { kmsService: TKmsServiceFactory; kmsDAL: TKmsKeyDALFactory; permissionService: TPermissionServiceFactory; + projectDAL: Pick; }; export type TCmekServiceFactory = ReturnType; -export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TCmekServiceFactoryDep) => { - const createCmek = async ({ projectId, ...dto }: TCreateCmekDTO, actor: OrgServiceActor) => { - const { permission } = await permissionService.getProjectPermission( - actor.type, - actor.id, - projectId, - actor.authMethod, - actor.orgId - ); +export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, projectDAL }: TCmekServiceFactoryDep) => { + const createCmek = async ({ projectId: preSplitProjectId, ...dto }: TCreateCmekDTO, actor: OrgServiceActor) => { + let projectId = preSplitProjectId; + const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(projectId, ProjectType.KMS); + if (cmekProjectFromSplit) { + projectId = cmekProjectFromSplit.id; + } + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Create, ProjectPermissionSub.Cmek); - const cmek = await kmsService.generateKmsKey({ - ...dto, - projectId, - isReserved: false - }); + try { + const cmek = await kmsService.generateKmsKey({ + ...dto, + projectId, + isReserved: false + }); - return cmek; + return { + ...cmek, + version: 1, + encryptionAlgorithm: dto.encryptionAlgorithm + }; + } catch (err) { + if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) { + throw new BadRequestError({ + message: `A KMS key with the name "${dto.name}" already exists for the project with ID "${projectId}"` + }); + } + + throw err; + } }; const updateCmekById = async ({ keyId, ...data }: TUpdabteCmekByIdDTO, actor: OrgServiceActor) => { - const key = await kmsDAL.findById(keyId); + const key = await kmsDAL.findCmekById(keyId); if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` }); if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" }); - const { permission } = await permissionService.getProjectPermission( - actor.type, - actor.id, - key.projectId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: key.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Edit, ProjectPermissionSub.Cmek); - const cmek = await kmsDAL.updateById(keyId, data); + try { + const cmek = await kmsDAL.updateById(keyId, data); - return cmek; + return { + ...cmek, + version: key.version, + encryptionAlgorithm: key.encryptionAlgorithm + }; + } catch (err) { + if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) { + throw new BadRequestError({ + message: `A KMS key with the name "${data.name!}" already exists for the project with ID "${key.projectId}"` + }); + } + + throw err; + } }; const deleteCmekById = async (keyId: string, actor: OrgServiceActor) => { - const key = await kmsDAL.findById(keyId); + const key = await kmsDAL.findCmekById(keyId); if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` }); if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" }); - const { permission } = await permissionService.getProjectPermission( - actor.type, - actor.id, - key.projectId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: key.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Delete, ProjectPermissionSub.Cmek); - const cmek = kmsDAL.deleteById(keyId); + await kmsDAL.deleteById(keyId); - return cmek; + return key; }; - const listCmeksByProjectId = async ({ projectId, ...filters }: TListCmeksByProjectIdDTO, actor: OrgServiceActor) => { - const { permission } = await permissionService.getProjectPermission( - actor.type, - actor.id, + const listCmeksByProjectId = async ( + { projectId: preSplitProjectId, ...filters }: TListCmeksByProjectIdDTO, + actor: OrgServiceActor + ) => { + let projectId = preSplitProjectId; + const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(preSplitProjectId, ProjectType.KMS); + if (cmekProjectFromSplit) { + projectId = cmekProjectFromSplit.id; + } + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, projectId, - actor.authMethod, - actor.orgId - ); + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); - const { keys: cmeks, totalCount } = await kmsDAL.findKmsKeysByProjectId({ projectId, ...filters }); + const { keys: cmeks, totalCount } = await kmsDAL.listCmeksByProjectId({ projectId, ...filters }); return { cmeks, totalCount }; }; + const findCmekById = async (keyId: string, actor: OrgServiceActor) => { + const key = await kmsDAL.findCmekById(keyId); + + if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` }); + + if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: key.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); + + return key; + }; + + const findCmekByName = async (keyName: string, projectId: string, actor: OrgServiceActor) => { + const key = await kmsDAL.findCmekByName(keyName, projectId); + + if (!key) + throw new NotFoundError({ message: `Key with name "${keyName}" not found for project with ID "${projectId}"` }); + + if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: key.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); + + return key; + }; + const cmekEncrypt = async ({ keyId, plaintext }: TCmekEncryptDTO, actor: OrgServiceActor) => { const key = await kmsDAL.findById(keyId); - if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` }); + if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` }); if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" }); if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" }); - const { permission } = await permissionService.getProjectPermission( - actor.type, - actor.id, - key.projectId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: key.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Encrypt, ProjectPermissionSub.Cmek); @@ -126,25 +228,170 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC const { cipherTextBlob } = await encrypt({ plainText: Buffer.from(plaintext, "base64") }); - return cipherTextBlob.toString("base64"); + return { + ciphertext: cipherTextBlob.toString("base64"), + projectId: key.projectId + }; }; - const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: OrgServiceActor) => { - const key = await kmsDAL.findById(keyId); + const listSigningAlgorithms = async ({ keyId }: TCmekListSigningAlgorithmsDTO, actor: OrgServiceActor) => { + const key = await kmsDAL.findCmekById(keyId); - if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` }); + if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` }); + if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" }); + if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: key.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); + + if (key.keyUsage !== KmsKeyUsage.SIGN_VERIFY) { + throw new BadRequestError({ message: `Key with ID '${keyId}' is not intended for signing` }); + } + + const encryptionAlgorithm = key.encryptionAlgorithm as TCmekKeyEncryptionAlgorithm; + + const algos = [ + { + keyAlgorithm: "rsa", + signingAlgorithms: Object.values(SigningAlgorithm).filter((algorithm) => + algorithm.toLowerCase().startsWith("rsa") + ) + }, + { + keyAlgorithm: "ecc", + signingAlgorithms: Object.values(SigningAlgorithm).filter((algorithm) => + algorithm.toLowerCase().startsWith("ecdsa") + ) + } + ]; + + const selectedAlgorithm = algos.find((algo) => encryptionAlgorithm.toLowerCase().startsWith(algo.keyAlgorithm)); + + if (!selectedAlgorithm) { + throw new BadRequestError({ message: `Unsupported encryption algorithm: ${encryptionAlgorithm}` }); + } + + return { signingAlgorithms: selectedAlgorithm.signingAlgorithms, projectId: key.projectId }; + }; + + const getPublicKey = async ({ keyId }: TCmekGetPublicKeyDTO, actor: OrgServiceActor) => { + const key = await kmsDAL.findCmekById(keyId); + + if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` }); + if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" }); + if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: key.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); + + const publicKey = await kmsService.getPublicKey({ kmsId: keyId }); + return { publicKey: publicKey.toString("base64"), projectId: key.projectId }; + }; + + const cmekSign = async ({ keyId, data, signingAlgorithm, isDigest }: TCmekSignDTO, actor: OrgServiceActor) => { + const key = await kmsDAL.findCmekById(keyId); + + if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` }); if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" }); if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" }); - const { permission } = await permissionService.getProjectPermission( - actor.type, - actor.id, - key.projectId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: key.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Sign, ProjectPermissionSub.Cmek); + + const sign = await kmsService.signWithKmsKey({ kmsId: keyId }); + + const { signature, algorithm } = await sign({ data: Buffer.from(data, "base64"), signingAlgorithm, isDigest }); + + return { + signature: signature.toString("base64"), + keyId: key.id, + projectId: key.projectId, + signingAlgorithm: algorithm + }; + }; + + const cmekVerify = async ( + { keyId, data, signature, signingAlgorithm, isDigest }: TCmekVerifyDTO, + actor: OrgServiceActor + ) => { + const key = await kmsDAL.findCmekById(keyId); + + if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` }); + + if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" }); + + if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: key.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Verify, ProjectPermissionSub.Cmek); + + const verify = await kmsService.verifyWithKmsKey({ kmsId: keyId, signingAlgorithm }); + + const { signatureValid, algorithm } = await verify({ + isDigest, + data: Buffer.from(data, "base64"), + signature: Buffer.from(signature, "base64") + }); + + return { + signatureValid, + keyId: key.id, + projectId: key.projectId, + signingAlgorithm: algorithm + }; + }; + + const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: OrgServiceActor) => { + const key = await kmsDAL.findById(keyId); + + if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` }); + + if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" }); + + if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: key.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.KMS + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Decrypt, ProjectPermissionSub.Cmek); @@ -152,7 +399,10 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC const plaintextBlob = await decrypt({ cipherTextBlob: Buffer.from(ciphertext, "base64") }); - return plaintextBlob.toString("base64"); + return { + plaintext: plaintextBlob.toString("base64"), + projectId: key.projectId + }; }; return { @@ -161,6 +411,12 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC deleteCmekById, listCmeksByProjectId, cmekEncrypt, - cmekDecrypt + cmekDecrypt, + findCmekById, + findCmekByName, + cmekSign, + cmekVerify, + listSigningAlgorithms, + getPublicKey }; }; diff --git a/backend/src/services/cmek/cmek-types.ts b/backend/src/services/cmek/cmek-types.ts index b99ff1d6e..0421bce0e 100644 --- a/backend/src/services/cmek/cmek-types.ts +++ b/backend/src/services/cmek/cmek-types.ts @@ -1,12 +1,18 @@ -import { SymmetricEncryption } from "@app/lib/crypto/cipher"; +import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher"; +import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign"; import { OrderByDirection } from "@app/lib/types"; +import { KmsKeyUsage } from "../kms/kms-types"; + +export type TCmekKeyEncryptionAlgorithm = SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm; + export type TCreateCmekDTO = { orgId: string; projectId: string; name: string; description?: string; - encryptionAlgorithm: SymmetricEncryption; + encryptionAlgorithm: TCmekKeyEncryptionAlgorithm; + keyUsage: KmsKeyUsage; }; export type TUpdabteCmekByIdDTO = { @@ -38,3 +44,26 @@ export type TCmekDecryptDTO = { export enum CmekOrderBy { Name = "name" } + +export type TCmekListSigningAlgorithmsDTO = { + keyId: string; +}; + +export type TCmekGetPublicKeyDTO = { + keyId: string; +}; + +export type TCmekSignDTO = { + keyId: string; + data: string; + signingAlgorithm: SigningAlgorithm; + isDigest: boolean; +}; + +export type TCmekVerifyDTO = { + keyId: string; + data: string; + signature: string; + signingAlgorithm: SigningAlgorithm; + isDigest: boolean; +}; diff --git a/backend/src/services/external-migration/external-migration-fns.ts b/backend/src/services/external-migration/external-migration-fns.ts index 7ae0d0aad..856b39012 100644 --- a/backend/src/services/external-migration/external-migration-fns.ts +++ b/backend/src/services/external-migration/external-migration-fns.ts @@ -16,6 +16,7 @@ import { TProjectDALFactory } from "../project/project-dal"; import { TProjectServiceFactory } from "../project/project-service"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvServiceFactory } from "../project-env/project-env-service"; +import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; @@ -30,11 +31,13 @@ export type TImportDataIntoInfisicalDTO = { projectEnvDAL: Pick; kmsService: Pick; - secretDAL: Pick; + secretDAL: Pick; secretVersionDAL: Pick; - secretTagDAL: Pick; + secretTagDAL: Pick; secretVersionTagDAL: Pick; + resourceMetadataDAL: Pick; + folderDAL: Pick; projectService: Pick; projectEnvService: Pick; @@ -503,6 +506,7 @@ export const importDataIntoInfisicalFn = async ({ secretTagDAL, secretVersionTagDAL, folderDAL, + resourceMetadataDAL, input: { data, actor, actorId, actorOrgId, actorAuthMethod } }: TImportDataIntoInfisicalDTO) => { // Import data to infisical @@ -762,10 +766,16 @@ export const importDataIntoInfisicalFn = async ({ }; }), folderId: selectedFolder.id, + orgId: actorOrgId, + resourceMetadataDAL, secretDAL, secretVersionDAL, secretTagDAL, secretVersionTagDAL, + actor: { + type: actor, + actorId + }, tx }); } diff --git a/backend/src/services/external-migration/external-migration-queue.ts b/backend/src/services/external-migration/external-migration-queue.ts index 3cbe8b616..8aa46b94c 100644 --- a/backend/src/services/external-migration/external-migration-queue.ts +++ b/backend/src/services/external-migration/external-migration-queue.ts @@ -8,6 +8,7 @@ import { TProjectDALFactory } from "../project/project-dal"; import { TProjectServiceFactory } from "../project/project-service"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvServiceFactory } from "../project-env/project-env-service"; +import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; @@ -26,15 +27,17 @@ export type TExternalMigrationQueueFactoryDep = { projectEnvDAL: Pick; kmsService: Pick; - secretDAL: Pick; + secretDAL: Pick; secretVersionDAL: Pick; - secretTagDAL: Pick; + secretTagDAL: Pick; secretVersionTagDAL: Pick; folderDAL: Pick; projectService: Pick; projectEnvService: Pick; secretV2BridgeService: Pick; + + resourceMetadataDAL: Pick; }; export type TExternalMigrationQueueFactory = ReturnType; @@ -52,7 +55,8 @@ export const externalMigrationQueueFactory = ({ secretVersionDAL, secretTagDAL, secretVersionTagDAL, - folderDAL + folderDAL, + resourceMetadataDAL }: TExternalMigrationQueueFactoryDep) => { const startImport = async (dto: { actorEmail: string; @@ -109,7 +113,8 @@ export const externalMigrationQueueFactory = ({ kmsService, projectService, projectEnvService, - secretV2BridgeService + secretV2BridgeService, + resourceMetadataDAL }); if (projectsNotImported.length) { diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index 896afe93d..1ff2c78d2 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -1,14 +1,18 @@ import { ForbiddenError } from "@casl/ability"; -import ms from "ms"; -import { ProjectMembershipRole, SecretKeyEncoding } from "@app/db/schemas"; +import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; +import { ProjectPermissionGroupActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto"; import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; -import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; +import { ms } from "@app/lib/ms"; +import { isUuidV4 } from "@app/lib/validator"; import { TGroupDALFactory } from "../../ee/services/group/group-dal"; import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal"; @@ -62,29 +66,37 @@ export const groupProjectServiceFactory = ({ actorAuthMethod, roles, projectId, - groupId + groupIdOrName }: TCreateProjectGroupDTO) => { const project = await projectDAL.findById(projectId); if (!project) throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); if (project.version < 2) throw new BadRequestError({ message: `Failed to add group to E2EE project` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, membership } = await permissionService.getProjectPermission({ actor, actorId, - project.id, + projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Groups); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Create, ProjectPermissionSub.Groups); - const group = await groupDAL.findOne({ orgId: actorOrgId, id: groupId }); - if (!group) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` }); + let group: TGroups | null = null; + if (isUuidV4(groupIdOrName)) { + group = await groupDAL.findOne({ orgId: actorOrgId, id: groupIdOrName }); + } + if (!group) { + group = await groupDAL.findOne({ orgId: actorOrgId, name: groupIdOrName }); + } + + if (!group) throw new NotFoundError({ message: `Failed to find group with ID or name ${groupIdOrName}` }); const existingGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id }); if (existingGroup) throw new BadRequestError({ - message: `Group with ID ${groupId} already exists in project with id ${project.id}` + message: `Group with ID ${group.id} already exists in project with id ${project.id}` }); for await (const { role: requestedRoleChange } of roles) { @@ -93,11 +105,23 @@ export const groupProjectServiceFactory = ({ project.id ); - const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, rolePermission); - - if (!hasRequiredPrivileges) { - throw new ForbiddenRequestError({ message: "Failed to assign group to a more privileged role" }); - } + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionGroupActions.GrantPrivileges, + ProjectPermissionSub.Groups, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to assign group to role", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionGroupActions.GrantPrivileges, + ProjectPermissionSub.Groups + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); } // validate custom roles input @@ -127,7 +151,7 @@ export const groupProjectServiceFactory = ({ const projectGroup = await groupProjectDAL.transaction(async (tx) => { const groupProjectMembership = await groupProjectDAL.create( { - groupId: group.id, + groupId: group!.id, projectId: project.id }, tx @@ -162,7 +186,7 @@ export const groupProjectServiceFactory = ({ // share project key with users in group that have not // individually been added to the project and that are not part of // other groups that are in the project - const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, project.id, tx); + const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group!.id, project.id, tx); if (groupMembers.length) { const ghostUser = await projectDAL.findProjectGhostUser(project.id, tx); @@ -237,14 +261,15 @@ export const groupProjectServiceFactory = ({ if (!project) throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, membership } = await permissionService.getProjectPermission({ actor, actorId, - project.id, + projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Groups); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Edit, ProjectPermissionSub.Groups); const group = await groupDAL.findOne({ orgId: actorOrgId, id: groupId }); if (!group) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` }); @@ -257,12 +282,23 @@ export const groupProjectServiceFactory = ({ requestedRoleChange, project.id ); - - const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, rolePermission); - - if (!hasRequiredPrivileges) { - throw new ForbiddenRequestError({ message: "Failed to assign group to a more privileged role" }); - } + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionGroupActions.GrantPrivileges, + ProjectPermissionSub.Groups, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to assign group to role", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionGroupActions.GrantPrivileges, + ProjectPermissionSub.Groups + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); } // validate custom roles input @@ -339,14 +375,15 @@ export const groupProjectServiceFactory = ({ const groupProjectMembership = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id }); if (!groupProjectMembership) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - project.id, + projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Groups); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Delete, ProjectPermissionSub.Groups); const deletedProjectGroup = await groupProjectDAL.transaction(async (tx) => { const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, project.id, tx); @@ -383,14 +420,15 @@ export const groupProjectServiceFactory = ({ throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - project.id, + projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Groups); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups); const groupMemberships = await groupProjectDAL.findByProjectId(project.id); return groupMemberships; @@ -410,14 +448,15 @@ export const groupProjectServiceFactory = ({ throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - project.id, + projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Groups); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups); const [groupMembership] = await groupProjectDAL.findByProjectId(project.id, { groupId diff --git a/backend/src/services/group-project/group-project-types.ts b/backend/src/services/group-project/group-project-types.ts index 1e1794963..f77615d2e 100644 --- a/backend/src/services/group-project/group-project-types.ts +++ b/backend/src/services/group-project/group-project-types.ts @@ -3,7 +3,7 @@ import { TProjectPermission } from "@app/lib/types"; import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types"; export type TCreateProjectGroupDTO = { - groupId: string; + groupIdOrName: string; roles: ( | { role: string; diff --git a/backend/src/services/identity-access-token/identity-access-token-dal.ts b/backend/src/services/identity-access-token/identity-access-token-dal.ts index f12bd8c15..57517c706 100644 --- a/backend/src/services/identity-access-token/identity-access-token-dal.ts +++ b/backend/src/services/identity-access-token/identity-access-token-dal.ts @@ -37,7 +37,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { ) .leftJoin(TableName.IdentityOidcAuth, `${TableName.Identity}.id`, `${TableName.IdentityOidcAuth}.identityId`) .leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`) - + .leftJoin(TableName.IdentityJwtAuth, `${TableName.Identity}.id`, `${TableName.IdentityJwtAuth}.identityId`) .select(selectAllTableCols(TableName.IdentityAccessToken)) .select( db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"), @@ -47,6 +47,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityKubernetesAuth).as("accessTokenTrustedIpsK8s"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOidcAuth).as("accessTokenTrustedIpsOidc"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTokenAuth).as("accessTokenTrustedIpsToken"), + db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityJwtAuth).as("accessTokenTrustedIpsJwt"), db.ref("name").withSchema(TableName.Identity) ) .first(); @@ -61,7 +62,8 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { trustedIpsAzureAuth: doc.accessTokenTrustedIpsAzure, trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s, trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc, - trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken + trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken, + trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt }; } catch (error) { throw new DatabaseError({ error, name: "IdAccessTokenFindOne" }); diff --git a/backend/src/services/identity-access-token/identity-access-token-service.ts b/backend/src/services/identity-access-token/identity-access-token-service.ts index a59d1e959..a51d80e41 100644 --- a/backend/src/services/identity-access-token/identity-access-token-service.ts +++ b/backend/src/services/identity-access-token/identity-access-token-service.ts @@ -78,9 +78,7 @@ export const identityAccessTokenServiceFactory = ({ const renewAccessToken = async ({ accessToken }: TRenewAccessTokenDTO) => { const appCfg = getConfig(); - const decodedToken = jwt.verify(accessToken, appCfg.AUTH_SECRET) as JwtPayload & { - identityAccessTokenId: string; - }; + const decodedToken = jwt.verify(accessToken, appCfg.AUTH_SECRET) as TIdentityAccessTokenJwtPayload; if (decodedToken.authTokenType !== AuthTokenType.IDENTITY_ACCESS_TOKEN) { throw new BadRequestError({ message: "Only identity access tokens can be renewed" }); } @@ -127,7 +125,23 @@ export const identityAccessTokenServiceFactory = ({ accessTokenLastRenewedAt: new Date() }); - return { accessToken, identityAccessToken: updatedIdentityAccessToken }; + const renewedToken = jwt.sign( + { + identityId: decodedToken.identityId, + clientSecretId: decodedToken.clientSecretId, + identityAccessTokenId: decodedToken.identityAccessTokenId, + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + } as TIdentityAccessTokenJwtPayload, + appCfg.AUTH_SECRET, + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } + ); + + return { accessToken: renewedToken, identityAccessToken: updatedIdentityAccessToken }; }; const revokeAccessToken = async (accessToken: string) => { @@ -171,7 +185,8 @@ export const identityAccessTokenServiceFactory = ({ [IdentityAuthMethod.AZURE_AUTH]: identityAccessToken.trustedIpsAzureAuth, [IdentityAuthMethod.KUBERNETES_AUTH]: identityAccessToken.trustedIpsKubernetesAuth, [IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth, - [IdentityAuthMethod.TOKEN_AUTH]: identityAccessToken.trustedIpsAccessTokenAuth + [IdentityAuthMethod.TOKEN_AUTH]: identityAccessToken.trustedIpsAccessTokenAuth, + [IdentityAuthMethod.JWT_AUTH]: identityAccessToken.trustedIpsAccessJwtAuth }; const trustedIps = trustedIpsMap[identityAccessToken.authMethod as IdentityAuthMethod]; diff --git a/backend/src/services/identity-access-token/identity-access-token-types.ts b/backend/src/services/identity-access-token/identity-access-token-types.ts index 86967df76..c97d2f40a 100644 --- a/backend/src/services/identity-access-token/identity-access-token-types.ts +++ b/backend/src/services/identity-access-token/identity-access-token-types.ts @@ -7,4 +7,9 @@ export type TIdentityAccessTokenJwtPayload = { clientSecretId: string; identityAccessTokenId: string; authTokenType: string; + identityAuth: { + oidc?: { + claims: Record; + }; + }; }; diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts index 6295446bd..fe7b24783 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts @@ -2,20 +2,25 @@ import { ForbiddenError } from "@casl/ability"; import axios from "axios"; import jwt from "jsonwebtoken"; +import RE2 from "re2"; import { IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; -import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityAwsAuthDALFactory } from "./identity-aws-auth-dal"; import { extractPrincipalArn } from "./identity-aws-auth-fns"; import { @@ -29,7 +34,7 @@ import { } from "./identity-aws-auth-types"; type TIdentityAwsAuthServiceFactoryDep = { - identityAccessTokenDAL: Pick; + identityAccessTokenDAL: Pick; identityAwsAuthDAL: Pick; identityOrgMembershipDAL: Pick; licenseService: Pick; @@ -38,6 +43,40 @@ type TIdentityAwsAuthServiceFactoryDep = { export type TIdentityAwsAuthServiceFactory = ReturnType; +const awsRegionFromHeader = (authorizationHeader: string): string | null => { + // https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html + // The Authorization header takes the following form. + // Authorization: AWS4-HMAC-SHA256 + // Credential=AKIAIOSFODNN7EXAMPLE/20230719/us-east-1/sts/aws4_request, + // SignedHeaders=content-length;content-type;host;x-amz-date, + // Signature=fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024 + // + // The credential is in the form of "////aws4_request" + try { + const fields = authorizationHeader.split(" "); + for (const field of fields) { + if (field.startsWith("Credential=")) { + const parts = field.split("/"); + if (parts.length >= 3) { + return parts[2]; + } + } + } + } catch { + return null; + } + return null; +}; + +function isValidAwsRegion(region: string | null): boolean { + const validRegionPattern = new RE2("^[a-z0-9-]+$"); + if (typeof region !== "string" || region.length === 0 || region.length > 20) { + return false; + } + + return validRegionPattern.test(region); +} + export const identityAwsAuthServiceFactory = ({ identityAccessTokenDAL, identityAwsAuthDAL, @@ -55,6 +94,13 @@ export const identityAwsAuthServiceFactory = ({ const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString()); const body: string = Buffer.from(iamRequestBody, "base64").toString(); + const region = headers.Authorization ? awsRegionFromHeader(headers.Authorization) : null; + + if (!isValidAwsRegion(region)) { + throw new BadRequestError({ message: "Invalid AWS region" }); + } + + const url = region ? `https://sts.${region}.amazonaws.com` : identityAwsAuth.stsEndpoint; const { data: { @@ -64,7 +110,7 @@ export const identityAwsAuthServiceFactory = ({ } }: { data: TGetCallerIdentityResponse } = await axios({ method: iamHttpRequestMethod, - url: headers?.Host ? `https://${headers.Host}` : identityAwsAuth.stsEndpoint, + url, headers, data: body }); @@ -92,7 +138,8 @@ export const identityAwsAuthServiceFactory = ({ .some((principalArn) => { // convert wildcard ARN to a regular expression: "arn:aws:iam::123456789012:*" -> "^arn:aws:iam::123456789012:.*$" // considers exact matches + wildcard matches - const regex = new RegExp(`^${principalArn.replace(/\*/g, ".*")}$`); + // heavily validated in router + const regex = new RE2(`^${principalArn.replaceAll("*", ".*")}$`); return regex.test(extractPrincipalArn(Arn)); }); @@ -126,12 +173,12 @@ export const identityAwsAuthServiceFactory = ({ authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN } as TIdentityAccessTokenJwtPayload, appCfg.AUTH_SECRET, - { - expiresIn: - Number(identityAccessToken.accessTokenMaxTTL) === 0 - ? undefined - : Number(identityAccessToken.accessTokenMaxTTL) - } + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } ); return { accessToken, identityAwsAuth, identityAccessToken, identityMembershipOrg }; @@ -149,8 +196,11 @@ export const identityAwsAuthServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TAttachAwsAuthDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); @@ -171,7 +221,7 @@ export const identityAwsAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { @@ -250,7 +300,7 @@ export const identityAwsAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { @@ -304,7 +354,7 @@ export const identityAwsAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...awsIdentityAuth, orgId: identityMembershipOrg.orgId }; }; @@ -322,14 +372,14 @@ export const identityAwsAuthServiceFactory = ({ message: "The identity does not have aws auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, identityMembershipOrg.orgId, actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, @@ -339,13 +389,29 @@ export const identityAwsAuthServiceFactory = ({ actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) - throw new ForbiddenRequestError({ - message: "Failed to revoke aws auth of identity with more privileged role" + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to revoke aws auth of identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } }); const revokedIdentityAwsAuth = await identityAwsAuthDAL.transaction(async (tx) => { const deletedAwsAuth = await identityAwsAuthDAL.delete({ identityId }, tx); + await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.AWS_AUTH }, tx); + return { ...deletedAwsAuth?.[0], orgId: identityMembershipOrg.orgId }; }); return revokedIdentityAwsAuth; diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-types.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-types.ts index c24186ee0..785b37bbc 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-types.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-types.ts @@ -16,6 +16,7 @@ export type TAttachAwsAuthDTO = { accessTokenMaxTTL: number; accessTokenNumUsesLimit: number; accessTokenTrustedIps: { ipAddress: string }[]; + isActorSuperAdmin?: boolean; } & Omit; export type TUpdateAwsAuthDTO = { diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts index 2cb7b4ea4..1a6e5cbd6 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts @@ -1,7 +1,10 @@ +import RE2 from "re2"; +import safe from "safe-regex"; import { z } from "zod"; -const twelveDigitRegex = /^\d{12}$/; -const arnRegex = /^arn:aws:iam::\d{12}:(user\/[\w-]+|role\/[\w-]+|\*)$/; +const twelveDigitRegex = new RE2(/^\d{12}$/); +// akhilmhdh: change this to a normal function later. Checked no redosable at the moment +const arnRegex = new RE2(/^arn:aws:iam::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/[a-zA-Z0-9_.@+*/-]+|\*)$/); export const validateAccountIds = z .string() @@ -42,7 +45,8 @@ export const validatePrincipalArns = z // Split the string by commas to check each supposed ARN const arns = data.split(","); // Return true only if every item matches one of the allowed ARN formats - return arns.every((arn) => arnRegex.test(arn.trim())); + // and checks whether the provided regex is safe + return arns.map((el) => el.trim()).every((arn) => safe(`^${arn.replaceAll("*", ".*")}$`) && arnRegex.test(arn)); }, { message: diff --git a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts index cc61df65f..5baa00652 100644 --- a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts +++ b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts @@ -3,17 +3,21 @@ import jwt from "jsonwebtoken"; import { IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; -import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityAzureAuthDALFactory } from "./identity-azure-auth-dal"; import { validateAzureIdentity } from "./identity-azure-auth-fns"; import { @@ -30,7 +34,7 @@ type TIdentityAzureAuthServiceFactoryDep = { "findOne" | "transaction" | "create" | "updateById" | "delete" >; identityOrgMembershipDAL: Pick; - identityAccessTokenDAL: Pick; + identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; }; @@ -70,7 +74,9 @@ export const identityAzureAuthServiceFactory = ({ .map((servicePrincipalId) => servicePrincipalId.trim()) .some((servicePrincipalId) => servicePrincipalId === azureIdentity.oid); - if (!isServicePrincipalAllowed) throw new UnauthorizedError({ message: "Service principal not allowed" }); + if (!isServicePrincipalAllowed) { + throw new UnauthorizedError({ message: `Service principal '${azureIdentity.oid}' not allowed` }); + } } const identityAccessToken = await identityAzureAuthDAL.transaction(async (tx) => { @@ -97,12 +103,12 @@ export const identityAzureAuthServiceFactory = ({ authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN } as TIdentityAccessTokenJwtPayload, appCfg.AUTH_SECRET, - { - expiresIn: - Number(identityAccessToken.accessTokenMaxTTL) === 0 - ? undefined - : Number(identityAccessToken.accessTokenMaxTTL) - } + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } ); return { accessToken, identityAzureAuth, identityAccessToken, identityMembershipOrg }; @@ -120,8 +126,11 @@ export const identityAzureAuthServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TAttachAzureAuthDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); @@ -141,7 +150,7 @@ export const identityAzureAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { @@ -219,7 +228,7 @@ export const identityAzureAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { @@ -275,7 +284,7 @@ export const identityAzureAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...identityAzureAuth, orgId: identityMembershipOrg.orgId }; }; @@ -294,14 +303,14 @@ export const identityAzureAuthServiceFactory = ({ message: "The identity does not have azure auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, identityMembershipOrg.orgId, actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, @@ -310,13 +319,28 @@ export const identityAzureAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) - throw new ForbiddenRequestError({ - message: "Failed to revoke azure auth of identity with more privileged role" + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to revoke azure auth of identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } }); const revokedIdentityAzureAuth = await identityAzureAuthDAL.transaction(async (tx) => { const deletedAzureAuth = await identityAzureAuthDAL.delete({ identityId }, tx); + await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.AZURE_AUTH }, tx); + return { ...deletedAzureAuth?.[0], orgId: identityMembershipOrg.orgId }; }); return revokedIdentityAzureAuth; diff --git a/backend/src/services/identity-azure-auth/identity-azure-auth-types.ts b/backend/src/services/identity-azure-auth/identity-azure-auth-types.ts index ec03451db..485753b6f 100644 --- a/backend/src/services/identity-azure-auth/identity-azure-auth-types.ts +++ b/backend/src/services/identity-azure-auth/identity-azure-auth-types.ts @@ -14,6 +14,7 @@ export type TAttachAzureAuthDTO = { accessTokenMaxTTL: number; accessTokenNumUsesLimit: number; accessTokenTrustedIps: { ipAddress: string }[]; + isActorSuperAdmin?: boolean; } & Omit; export type TUpdateAzureAuthDTO = { diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts index a2a395f63..7420b61cf 100644 --- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts @@ -3,17 +3,21 @@ import jwt from "jsonwebtoken"; import { IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; -import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityGcpAuthDALFactory } from "./identity-gcp-auth-dal"; import { validateIamIdentity, validateIdTokenIdentity } from "./identity-gcp-auth-fns"; import { @@ -28,7 +32,7 @@ import { type TIdentityGcpAuthServiceFactoryDep = { identityGcpAuthDAL: Pick; identityOrgMembershipDAL: Pick; - identityAccessTokenDAL: Pick; + identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; }; @@ -138,12 +142,12 @@ export const identityGcpAuthServiceFactory = ({ authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN } as TIdentityAccessTokenJwtPayload, appCfg.AUTH_SECRET, - { - expiresIn: - Number(identityAccessToken.accessTokenMaxTTL) === 0 - ? undefined - : Number(identityAccessToken.accessTokenMaxTTL) - } + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } ); return { accessToken, identityGcpAuth, identityAccessToken, identityMembershipOrg }; @@ -162,8 +166,11 @@ export const identityGcpAuthServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TAttachGcpAuthDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); @@ -184,7 +191,7 @@ export const identityGcpAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { @@ -264,7 +271,7 @@ export const identityGcpAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { @@ -322,7 +329,7 @@ export const identityGcpAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...identityGcpAuth, orgId: identityMembershipOrg.orgId }; }; @@ -342,14 +349,14 @@ export const identityGcpAuthServiceFactory = ({ message: "The identity does not have gcp auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, identityMembershipOrg.orgId, actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, @@ -358,13 +365,28 @@ export const identityGcpAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) - throw new ForbiddenRequestError({ - message: "Failed to revoke gcp auth of identity with more privileged role" + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to revoke gcp auth of identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } }); const revokedIdentityGcpAuth = await identityGcpAuthDAL.transaction(async (tx) => { const deletedGcpAuth = await identityGcpAuthDAL.delete({ identityId }, tx); + await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.GCP_AUTH }, tx); + return { ...deletedGcpAuth?.[0], orgId: identityMembershipOrg.orgId }; }); return revokedIdentityGcpAuth; diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts index 45e64b24b..063630c73 100644 --- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts @@ -15,6 +15,7 @@ export type TAttachGcpAuthDTO = { accessTokenMaxTTL: number; accessTokenNumUsesLimit: number; accessTokenTrustedIps: { ipAddress: string }[]; + isActorSuperAdmin?: boolean; } & Omit; export type TUpdateGcpAuthDTO = { diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-dal.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-dal.ts new file mode 100644 index 000000000..5e6d13be6 --- /dev/null +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TIdentityJwtAuthDALFactory = ReturnType; + +export const identityJwtAuthDALFactory = (db: TDbClient) => { + const jwtAuthOrm = ormify(db, TableName.IdentityJwtAuth); + + return jwtAuthOrm; +}; diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-fns.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-fns.ts new file mode 100644 index 000000000..57aa933d6 --- /dev/null +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-fns.ts @@ -0,0 +1,13 @@ +import picomatch from "picomatch"; + +export const doesFieldValueMatchJwtPolicy = (fieldValue: string | boolean | number, policyValue: string) => { + if (typeof fieldValue === "boolean") { + return fieldValue === (policyValue === "true"); + } + + if (typeof fieldValue === "number") { + return fieldValue === parseInt(policyValue, 10); + } + + return policyValue === fieldValue || picomatch.isMatch(fieldValue, policyValue); +}; diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts new file mode 100644 index 000000000..39fc28ad2 --- /dev/null +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts @@ -0,0 +1,569 @@ +import { ForbiddenError } from "@casl/ability"; +import https from "https"; +import jwt from "jsonwebtoken"; +import { JwksClient } from "jwks-rsa"; + +import { IdentityAuthMethod, TIdentityJwtAuthsUpdate } from "@app/db/schemas"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { getConfig } from "@app/lib/config/env"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; +import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; +import { getStringValueByDot } from "@app/lib/template/dot-access"; + +import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; +import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; +import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { TKmsServiceFactory } from "../kms/kms-service"; +import { KmsDataKey } from "../kms/kms-types"; +import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; +import { TIdentityJwtAuthDALFactory } from "./identity-jwt-auth-dal"; +import { doesFieldValueMatchJwtPolicy } from "./identity-jwt-auth-fns"; +import { + JwtConfigurationType, + TAttachJwtAuthDTO, + TGetJwtAuthDTO, + TLoginJwtAuthDTO, + TRevokeJwtAuthDTO, + TUpdateJwtAuthDTO +} from "./identity-jwt-auth-types"; + +type TIdentityJwtAuthServiceFactoryDep = { + identityJwtAuthDAL: TIdentityJwtAuthDALFactory; + identityOrgMembershipDAL: Pick; + identityAccessTokenDAL: Pick; + permissionService: Pick; + licenseService: Pick; + kmsService: Pick; +}; + +export type TIdentityJwtAuthServiceFactory = ReturnType; + +export const identityJwtAuthServiceFactory = ({ + identityJwtAuthDAL, + identityOrgMembershipDAL, + permissionService, + licenseService, + identityAccessTokenDAL, + kmsService +}: TIdentityJwtAuthServiceFactoryDep) => { + const login = async ({ identityId, jwt: jwtValue }: TLoginJwtAuthDTO) => { + const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId }); + if (!identityJwtAuth) { + throw new NotFoundError({ message: "JWT auth method not found for identity, did you configure JWT auth?" }); + } + + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ + identityId: identityJwtAuth.identityId + }); + if (!identityMembershipOrg) { + throw new NotFoundError({ + message: `Identity organization membership for identity with ID '${identityJwtAuth.identityId}' not found` + }); + } + + const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId + }); + + const decodedToken = jwt.decode(jwtValue, { complete: true }); + if (!decodedToken) { + throw new UnauthorizedError({ + message: "Invalid JWT" + }); + } + + let tokenData: Record = {}; + + if (identityJwtAuth.configurationType === JwtConfigurationType.JWKS) { + let client: JwksClient; + if (identityJwtAuth.jwksUrl.includes("https:")) { + const decryptedJwksCaCert = orgDataKeyDecryptor({ + cipherTextBlob: identityJwtAuth.encryptedJwksCaCert + }).toString(); + + const requestAgent = new https.Agent({ ca: decryptedJwksCaCert, rejectUnauthorized: !!decryptedJwksCaCert }); + client = new JwksClient({ + jwksUri: identityJwtAuth.jwksUrl, + requestAgent + }); + } else { + client = new JwksClient({ + jwksUri: identityJwtAuth.jwksUrl + }); + } + + const { kid } = decodedToken.header; + const jwtSigningKey = await client.getSigningKey(kid); + + try { + tokenData = jwt.verify(jwtValue, jwtSigningKey.getPublicKey()) as Record; + } catch (error) { + if (error instanceof jwt.JsonWebTokenError) { + throw new UnauthorizedError({ + message: `Access denied: ${error.message}` + }); + } + + throw error; + } + } else { + const decryptedPublicKeys = orgDataKeyDecryptor({ cipherTextBlob: identityJwtAuth.encryptedPublicKeys }) + .toString() + .split(","); + + const errors: string[] = []; + let isMatchAnyKey = false; + for (const publicKey of decryptedPublicKeys) { + try { + tokenData = jwt.verify(jwtValue, publicKey) as Record; + isMatchAnyKey = true; + } catch (error) { + if (error instanceof jwt.JsonWebTokenError) { + errors.push(error.message); + } + } + } + + if (!isMatchAnyKey) { + throw new UnauthorizedError({ + message: `Access denied: JWT verification failed with all keys. Errors - ${errors.join("; ")}` + }); + } + } + + if (identityJwtAuth.boundIssuer) { + if (tokenData.iss !== identityJwtAuth.boundIssuer) { + throw new ForbiddenRequestError({ + message: "Access denied: issuer mismatch" + }); + } + } + + if (identityJwtAuth.boundSubject) { + if (!tokenData.sub) { + throw new UnauthorizedError({ + message: "Access denied: token has no subject field" + }); + } + + if (!doesFieldValueMatchJwtPolicy(tokenData.sub, identityJwtAuth.boundSubject)) { + throw new ForbiddenRequestError({ + message: "Access denied: subject not allowed" + }); + } + } + + if (identityJwtAuth.boundAudiences) { + if (!tokenData.aud) { + throw new UnauthorizedError({ + message: "Access denied: token has no audience field" + }); + } + + if ( + !identityJwtAuth.boundAudiences + .split(", ") + .some((policyValue) => doesFieldValueMatchJwtPolicy(tokenData.aud, policyValue)) + ) { + throw new UnauthorizedError({ + message: "Access denied: token audience not allowed" + }); + } + } + + if (identityJwtAuth.boundClaims) { + Object.keys(identityJwtAuth.boundClaims).forEach((claimKey) => { + const claimValue = (identityJwtAuth.boundClaims as Record)[claimKey]; + const value = getStringValueByDot(tokenData, claimKey) || ""; + + if (!value) { + throw new UnauthorizedError({ + message: `Access denied: token has no ${claimKey} field` + }); + } + + // handle both single and multi-valued claims + if ( + !claimValue.split(", ").some((claimEntry) => doesFieldValueMatchJwtPolicy(tokenData[claimKey], claimEntry)) + ) { + throw new UnauthorizedError({ + message: `Access denied: claim mismatch for field ${claimKey}` + }); + } + }); + } + + const identityAccessToken = await identityJwtAuthDAL.transaction(async (tx) => { + const newToken = await identityAccessTokenDAL.create( + { + identityId: identityJwtAuth.identityId, + isAccessTokenRevoked: false, + accessTokenTTL: identityJwtAuth.accessTokenTTL, + accessTokenMaxTTL: identityJwtAuth.accessTokenMaxTTL, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: identityJwtAuth.accessTokenNumUsesLimit, + authMethod: IdentityAuthMethod.JWT_AUTH + }, + tx + ); + + return newToken; + }); + + const appCfg = getConfig(); + const accessToken = jwt.sign( + { + identityId: identityJwtAuth.identityId, + identityAccessTokenId: identityAccessToken.id, + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + } as TIdentityAccessTokenJwtPayload, + appCfg.AUTH_SECRET, + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } + ); + + return { accessToken, identityJwtAuth, identityAccessToken, identityMembershipOrg }; + }; + + const attachJwtAuth = async ({ + identityId, + configurationType, + jwksUrl, + jwksCaCert, + publicKeys, + boundIssuer, + boundAudiences, + boundClaims, + boundSubject, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + actorId, + actorAuthMethod, + actor, + actorOrgId, + isActorSuperAdmin + }: TAttachJwtAuthDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) { + if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + } + if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) { + throw new BadRequestError({ + message: "Failed to add JWT Auth to already configured identity" + }); + } + + if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { + throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + + const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { + if ( + !plan.ipAllowlisting && + accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" && + accessTokenTrustedIp.ipAddress !== "::/0" + ) + throw new BadRequestError({ + message: + "Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range." + }); + if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress)) + throw new BadRequestError({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + return extractIPDetails(accessTokenTrustedIp.ipAddress); + }); + + const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: actorOrgId + }); + + const { cipherTextBlob: encryptedJwksCaCert } = orgDataKeyEncryptor({ + plainText: Buffer.from(jwksCaCert) + }); + + const { cipherTextBlob: encryptedPublicKeys } = orgDataKeyEncryptor({ + plainText: Buffer.from(publicKeys.join(",")) + }); + + const identityJwtAuth = await identityJwtAuthDAL.transaction(async (tx) => { + const doc = await identityJwtAuthDAL.create( + { + identityId: identityMembershipOrg.identityId, + configurationType, + jwksUrl, + encryptedJwksCaCert, + encryptedPublicKeys, + boundIssuer, + boundAudiences, + boundClaims, + boundSubject, + accessTokenMaxTTL, + accessTokenTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps) + }, + tx + ); + + return doc; + }); + return { ...identityJwtAuth, orgId: identityMembershipOrg.orgId, jwksCaCert, publicKeys }; + }; + + const updateJwtAuth = async ({ + identityId, + configurationType, + jwksUrl, + jwksCaCert, + publicKeys, + boundIssuer, + boundAudiences, + boundClaims, + boundSubject, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TUpdateJwtAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + + if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) { + throw new BadRequestError({ + message: "Failed to update JWT Auth" + }); + } + + const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId }); + + if ( + (accessTokenMaxTTL || identityJwtAuth.accessTokenMaxTTL) > 0 && + (accessTokenTTL || identityJwtAuth.accessTokenMaxTTL) > (accessTokenMaxTTL || identityJwtAuth.accessTokenMaxTTL) + ) { + throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + + const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { + if ( + !plan.ipAllowlisting && + accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" && + accessTokenTrustedIp.ipAddress !== "::/0" + ) + throw new BadRequestError({ + message: + "Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range." + }); + if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress)) + throw new BadRequestError({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + return extractIPDetails(accessTokenTrustedIp.ipAddress); + }); + + const updateQuery: TIdentityJwtAuthsUpdate = { + boundIssuer, + configurationType, + jwksUrl, + boundAudiences, + boundClaims, + boundSubject, + accessTokenMaxTTL, + accessTokenTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: reformattedAccessTokenTrustedIps + ? JSON.stringify(reformattedAccessTokenTrustedIps) + : undefined + }; + + const { encryptor: orgDataKeyEncryptor, decryptor: orgDataKeyDecryptor } = + await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: actorOrgId + }); + + if (jwksCaCert !== undefined) { + const { cipherTextBlob: encryptedJwksCaCert } = orgDataKeyEncryptor({ + plainText: Buffer.from(jwksCaCert) + }); + + updateQuery.encryptedJwksCaCert = encryptedJwksCaCert; + } + + if (publicKeys) { + const { cipherTextBlob: encryptedPublicKeys } = orgDataKeyEncryptor({ + plainText: Buffer.from(publicKeys.join(",")) + }); + + updateQuery.encryptedPublicKeys = encryptedPublicKeys; + } + + const updatedJwtAuth = await identityJwtAuthDAL.updateById(identityJwtAuth.id, updateQuery); + const decryptedJwksCaCert = orgDataKeyDecryptor({ cipherTextBlob: updatedJwtAuth.encryptedJwksCaCert }).toString(); + const decryptedPublicKeys = orgDataKeyDecryptor({ cipherTextBlob: updatedJwtAuth.encryptedPublicKeys }) + .toString() + .split(","); + + return { + ...updatedJwtAuth, + orgId: identityMembershipOrg.orgId, + jwksCaCert: decryptedJwksCaCert, + publicKeys: decryptedPublicKeys + }; + }; + + const getJwtAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetJwtAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + + if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) { + throw new BadRequestError({ + message: "The identity does not have JWT Auth attached" + }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + + const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId }); + + const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: actorOrgId + }); + + const decryptedJwksCaCert = orgDataKeyDecryptor({ cipherTextBlob: identityJwtAuth.encryptedJwksCaCert }).toString(); + const decryptedPublicKeys = orgDataKeyDecryptor({ cipherTextBlob: identityJwtAuth.encryptedPublicKeys }) + .toString() + .split(","); + + return { + ...identityJwtAuth, + orgId: identityMembershipOrg.orgId, + jwksCaCert: decryptedJwksCaCert, + publicKeys: decryptedPublicKeys + }; + }; + + const revokeJwtAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TRevokeJwtAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) { + throw new NotFoundError({ message: "Failed to find identity" }); + } + + if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) { + throw new BadRequestError({ + message: "The identity does not have JWT auth" + }); + } + + const { permission, membership } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission( + ActorType.IDENTITY, + identityMembershipOrg.identityId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to revoke jwt auth of identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + + const revokedIdentityJwtAuth = await identityJwtAuthDAL.transaction(async (tx) => { + const deletedJwtAuth = await identityJwtAuthDAL.delete({ identityId }, tx); + await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.JWT_AUTH }, tx); + + return { ...deletedJwtAuth?.[0], orgId: identityMembershipOrg.orgId }; + }); + + return revokedIdentityJwtAuth; + }; + + return { + login, + attachJwtAuth, + updateJwtAuth, + getJwtAuth, + revokeJwtAuth + }; +}; diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-types.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-types.ts new file mode 100644 index 000000000..bc19aba83 --- /dev/null +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-types.ts @@ -0,0 +1,52 @@ +import { TProjectPermission } from "@app/lib/types"; + +export enum JwtConfigurationType { + JWKS = "jwks", + STATIC = "static" +} + +export type TAttachJwtAuthDTO = { + identityId: string; + configurationType: JwtConfigurationType; + jwksUrl: string; + jwksCaCert: string; + publicKeys: string[]; + boundIssuer: string; + boundAudiences: string; + boundClaims: Record; + boundSubject: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: { ipAddress: string }[]; + isActorSuperAdmin?: boolean; +} & Omit; + +export type TUpdateJwtAuthDTO = { + identityId: string; + configurationType?: JwtConfigurationType; + jwksUrl?: string; + jwksCaCert?: string; + publicKeys?: string[]; + boundIssuer?: string; + boundAudiences?: string; + boundClaims?: Record; + boundSubject?: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: { ipAddress: string }[]; +} & Omit; + +export type TGetJwtAuthDTO = { + identityId: string; +} & Omit; + +export type TRevokeJwtAuthDTO = { + identityId: string; +} & Omit; + +export type TLoginJwtAuthDTO = { + identityId: string; + jwt: string; +}; diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-validators.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-validators.ts new file mode 100644 index 000000000..515c2ac7e --- /dev/null +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-validators.ts @@ -0,0 +1,25 @@ +import { z } from "zod"; + +export const validateJwtAuthAudiencesField = z + .string() + .trim() + .default("") + .transform((data) => { + if (data === "") return ""; + return data + .split(",") + .map((id) => id.trim()) + .join(", "); + }); + +export const validateJwtBoundClaimsField = z.record(z.string()).transform((data) => { + const formattedClaims: Record = {}; + Object.keys(data).forEach((key) => { + formattedClaims[key] = data[key] + .split(",") + .map((id) => id.trim()) + .join(", "); + }); + + return formattedClaims; +}); diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index a99ae7c18..9c0e8d2dd 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -3,28 +3,25 @@ import axios, { AxiosError } from "axios"; import https from "https"; import jwt from "jsonwebtoken"; -import { IdentityAuthMethod, SecretKeyEncoding, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; +import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; -import { getConfig } from "@app/lib/config/env"; +import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { - decryptSymmetric, - encryptSymmetric, - generateAsymmetricKeyPair, - generateSymmetricKey, - infisicalSymmetricDecrypt, - infisicalSymmetricEncypt -} from "@app/lib/crypto/encryption"; -import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; -import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { TKmsServiceFactory } from "../kms/kms-service"; +import { KmsDataKey } from "../kms/kms-types"; +import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityKubernetesAuthDALFactory } from "./identity-kubernetes-auth-dal"; import { extractK8sUsername } from "./identity-kubernetes-auth-fns"; import { @@ -41,11 +38,11 @@ type TIdentityKubernetesAuthServiceFactoryDep = { TIdentityKubernetesAuthDALFactory, "create" | "findOne" | "transaction" | "updateById" | "delete" >; - identityAccessTokenDAL: Pick; + identityAccessTokenDAL: Pick; identityOrgMembershipDAL: Pick; - orgBotDAL: Pick; permissionService: Pick; licenseService: Pick; + kmsService: Pick; }; export type TIdentityKubernetesAuthServiceFactory = ReturnType; @@ -54,9 +51,9 @@ export const identityKubernetesAuthServiceFactory = ({ identityKubernetesAuthDAL, identityOrgMembershipDAL, identityAccessTokenDAL, - orgBotDAL, permissionService, - licenseService + licenseService, + kmsService }: TIdentityKubernetesAuthServiceFactoryDep) => { const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => { const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId }); @@ -75,42 +72,24 @@ export const identityKubernetesAuthServiceFactory = ({ }); } - const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }); - if (!orgBot) { - throw new NotFoundError({ - message: `Organization bot not found for organization with ID ${identityMembershipOrg.orgId}`, - name: "OrgBotNotFound" - }); - } - - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId }); - const { encryptedCaCert, caCertIV, caCertTag, encryptedTokenReviewerJwt, tokenReviewerJwtIV, tokenReviewerJwtTag } = - identityKubernetesAuth; - let caCert = ""; - if (encryptedCaCert && caCertIV && caCertTag) { - caCert = decryptSymmetric({ - ciphertext: encryptedCaCert, - iv: caCertIV, - tag: caCertTag, - key - }); + if (identityKubernetesAuth.encryptedKubernetesCaCertificate) { + caCert = decryptor({ cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate }).toString(); } let tokenReviewerJwt = ""; - if (encryptedTokenReviewerJwt && tokenReviewerJwtIV && tokenReviewerJwtTag) { - tokenReviewerJwt = decryptSymmetric({ - ciphertext: encryptedTokenReviewerJwt, - iv: tokenReviewerJwtIV, - tag: tokenReviewerJwtTag, - key - }); + if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) { + tokenReviewerJwt = decryptor({ + cipherTextBlob: identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt + }).toString(); + } else { + // if no token reviewer is provided means the incoming token has to act as reviewer + tokenReviewerJwt = serviceAccountJwt; } const { data } = await axios @@ -120,7 +99,8 @@ export const identityKubernetesAuthServiceFactory = ({ apiVersion: "authentication.k8s.io/v1", kind: "TokenReview", spec: { - token: serviceAccountJwt + token: serviceAccountJwt, + ...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {}) } }, { @@ -128,7 +108,8 @@ export const identityKubernetesAuthServiceFactory = ({ "Content-Type": "application/json", Authorization: `Bearer ${tokenReviewerJwt}` }, - + signal: AbortSignal.timeout(10000), + timeout: 10000, // if ca cert, rejectUnauthorized: true httpsAgent: new https.Agent({ ca: caCert, @@ -228,12 +209,12 @@ export const identityKubernetesAuthServiceFactory = ({ authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN } as TIdentityAccessTokenJwtPayload, appCfg.AUTH_SECRET, - { - expiresIn: - Number(identityAccessToken.accessTokenMaxTTL) === 0 - ? undefined - : Number(identityAccessToken.accessTokenMaxTTL) - } + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } ); return { accessToken, identityKubernetesAuth, identityAccessToken, identityMembershipOrg }; @@ -254,8 +235,11 @@ export const identityKubernetesAuthServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TAttachKubernetesAuthDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); @@ -276,7 +260,7 @@ export const identityKubernetesAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { @@ -296,79 +280,27 @@ export const identityKubernetesAuthServiceFactory = ({ return extractIPDetails(accessTokenTrustedIp.ipAddress); }); - const orgBot = await orgBotDAL.transaction(async (tx) => { - const doc = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }, tx); - if (doc) return doc; - - const { privateKey, publicKey } = generateAsymmetricKeyPair(); - const key = generateSymmetricKey(); - const { - ciphertext: encryptedPrivateKey, - iv: privateKeyIV, - tag: privateKeyTag, - encoding: privateKeyKeyEncoding, - algorithm: privateKeyAlgorithm - } = infisicalSymmetricEncypt(privateKey); - const { - ciphertext: encryptedSymmetricKey, - iv: symmetricKeyIV, - tag: symmetricKeyTag, - encoding: symmetricKeyKeyEncoding, - algorithm: symmetricKeyAlgorithm - } = infisicalSymmetricEncypt(key); - - return orgBotDAL.create( - { - name: "Infisical org bot", - publicKey, - privateKeyIV, - encryptedPrivateKey, - symmetricKeyIV, - symmetricKeyTag, - encryptedSymmetricKey, - symmetricKeyAlgorithm, - orgId: identityMembershipOrg.orgId, - privateKeyTag, - privateKeyAlgorithm, - privateKeyKeyEncoding, - symmetricKeyKeyEncoding - }, - tx - ); + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId }); - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding - }); - - const { ciphertext: encryptedCaCert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); - const { - ciphertext: encryptedTokenReviewerJwt, - iv: tokenReviewerJwtIV, - tag: tokenReviewerJwtTag - } = encryptSymmetric(tokenReviewerJwt, key); - const identityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => { const doc = await identityKubernetesAuthDAL.create( { identityId: identityMembershipOrg.identityId, kubernetesHost, - encryptedCaCert, - caCertIV, - caCertTag, - encryptedTokenReviewerJwt, - tokenReviewerJwtIV, - tokenReviewerJwtTag, allowedNamespaces, allowedNames, allowedAudience, accessTokenMaxTTL, accessTokenTTL, accessTokenNumUsesLimit, - accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps) + accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps), + encryptedKubernetesTokenReviewerJwt: tokenReviewerJwt + ? encryptor({ plainText: Buffer.from(tokenReviewerJwt) }).cipherTextBlob + : null, + encryptedKubernetesCaCertificate: encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob }, tx ); @@ -421,7 +353,7 @@ export const identityKubernetesAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { @@ -454,61 +386,36 @@ export const identityKubernetesAuthServiceFactory = ({ : undefined }; - const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }); - if (!orgBot) { - throw new NotFoundError({ - message: `Organization bot not found for organization with ID ${identityMembershipOrg.orgId}`, - name: "OrgBotNotFound" - }); - } - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId }); if (caCert !== undefined) { - const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); - updateQuery.encryptedCaCert = encryptedCACert; - updateQuery.caCertIV = caCertIV; - updateQuery.caCertTag = caCertTag; + updateQuery.encryptedKubernetesCaCertificate = encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob; } - if (tokenReviewerJwt !== undefined) { - const { - ciphertext: encryptedTokenReviewerJwt, - iv: tokenReviewerJwtIV, - tag: tokenReviewerJwtTag - } = encryptSymmetric(tokenReviewerJwt, key); - updateQuery.encryptedTokenReviewerJwt = encryptedTokenReviewerJwt; - updateQuery.tokenReviewerJwtIV = tokenReviewerJwtIV; - updateQuery.tokenReviewerJwtTag = tokenReviewerJwtTag; + if (tokenReviewerJwt) { + updateQuery.encryptedKubernetesTokenReviewerJwt = encryptor({ + plainText: Buffer.from(tokenReviewerJwt) + }).cipherTextBlob; + } else if (tokenReviewerJwt === null) { + updateQuery.encryptedKubernetesTokenReviewerJwt = null; } const updatedKubernetesAuth = await identityKubernetesAuthDAL.updateById(identityKubernetesAuth.id, updateQuery); - const updatedCACert = - updatedKubernetesAuth.encryptedCaCert && updatedKubernetesAuth.caCertIV && updatedKubernetesAuth.caCertTag - ? decryptSymmetric({ - ciphertext: updatedKubernetesAuth.encryptedCaCert, - iv: updatedKubernetesAuth.caCertIV, - tag: updatedKubernetesAuth.caCertTag, - key - }) - : ""; + const updatedCACert = updatedKubernetesAuth.encryptedKubernetesCaCertificate + ? decryptor({ + cipherTextBlob: updatedKubernetesAuth.encryptedKubernetesCaCertificate + }).toString() + : ""; - const updatedTokenReviewerJwt = - updatedKubernetesAuth.encryptedTokenReviewerJwt && - updatedKubernetesAuth.tokenReviewerJwtIV && - updatedKubernetesAuth.tokenReviewerJwtTag - ? decryptSymmetric({ - ciphertext: updatedKubernetesAuth.encryptedTokenReviewerJwt, - iv: updatedKubernetesAuth.tokenReviewerJwtIV, - tag: updatedKubernetesAuth.tokenReviewerJwtTag, - key - }) - : ""; + const updatedTokenReviewerJwt = updatedKubernetesAuth.encryptedKubernetesTokenReviewerJwt + ? decryptor({ + cipherTextBlob: updatedKubernetesAuth.encryptedKubernetesTokenReviewerJwt + }).toString() + : ""; return { ...updatedKubernetesAuth, @@ -528,12 +435,16 @@ export const identityKubernetesAuthServiceFactory = ({ const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId }); + if (!identityKubernetesAuth) { + throw new NotFoundError({ message: `Failed to find Kubernetes Auth for identity with ID ${identityId}` }); + } + if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) { throw new BadRequestError({ message: "The identity does not have Kubernetes Auth attached" }); } - const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId }); const { permission } = await permissionService.getOrgPermission( actor, @@ -542,43 +453,23 @@ export const identityKubernetesAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }); - if (!orgBot) - throw new NotFoundError({ - message: `Organization bot not found for organization with ID ${identityMembershipOrg.orgId}`, - name: "OrgBotNotFound" - }); - - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId }); - const { encryptedCaCert, caCertIV, caCertTag, encryptedTokenReviewerJwt, tokenReviewerJwtIV, tokenReviewerJwtTag } = - identityKubernetesAuth; - let caCert = ""; - if (encryptedCaCert && caCertIV && caCertTag) { - caCert = decryptSymmetric({ - ciphertext: encryptedCaCert, - iv: caCertIV, - tag: caCertTag, - key - }); + if (identityKubernetesAuth.encryptedKubernetesCaCertificate) { + caCert = decryptor({ cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate }).toString(); } let tokenReviewerJwt = ""; - if (encryptedTokenReviewerJwt && tokenReviewerJwtIV && tokenReviewerJwtTag) { - tokenReviewerJwt = decryptSymmetric({ - ciphertext: encryptedTokenReviewerJwt, - iv: tokenReviewerJwtIV, - tag: tokenReviewerJwtTag, - key - }); + if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) { + tokenReviewerJwt = decryptor({ + cipherTextBlob: identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt + }).toString(); } return { ...identityKubernetesAuth, caCert, tokenReviewerJwt, orgId: identityMembershipOrg.orgId }; @@ -599,14 +490,14 @@ export const identityKubernetesAuthServiceFactory = ({ message: "The identity does not have kubernetes auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, identityMembershipOrg.orgId, actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, @@ -615,13 +506,27 @@ export const identityKubernetesAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) - throw new ForbiddenRequestError({ - message: "Failed to revoke kubernetes auth of identity with more privileged role" + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to revoke kubernetes auth of identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } }); const revokedIdentityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => { const deletedKubernetesAuth = await identityKubernetesAuthDAL.delete({ identityId }, tx); + await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.KUBERNETES_AUTH }, tx); return { ...deletedKubernetesAuth?.[0], orgId: identityMembershipOrg.orgId }; }); return revokedIdentityKubernetesAuth; diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts index f1cde2be9..b3bbcb49e 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts @@ -9,7 +9,7 @@ export type TAttachKubernetesAuthDTO = { identityId: string; kubernetesHost: string; caCert: string; - tokenReviewerJwt: string; + tokenReviewerJwt?: string; allowedNamespaces: string; allowedNames: string; allowedAudience: string; @@ -17,13 +17,14 @@ export type TAttachKubernetesAuthDTO = { accessTokenMaxTTL: number; accessTokenNumUsesLimit: number; accessTokenTrustedIps: { ipAddress: string }[]; + isActorSuperAdmin?: boolean; } & Omit; export type TUpdateKubernetesAuthDTO = { identityId: string; kubernetesHost?: string; caCert?: string; - tokenReviewerJwt?: string; + tokenReviewerJwt?: string | null; allowedNamespaces?: string; allowedNames?: string; allowedAudience?: string; diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-fns.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-fns.ts index c6d65d836..7d386afcb 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-fns.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-fns.ts @@ -2,3 +2,11 @@ import picomatch from "picomatch"; export const doesFieldValueMatchOidcPolicy = (fieldValue: string, policyValue: string) => policyValue === fieldValue || picomatch.isMatch(fieldValue, policyValue); + +export const doesAudValueMatchOidcPolicy = (fieldValue: string | string[], policyValue: string) => { + if (Array.isArray(fieldValue)) { + return fieldValue.some((entry) => entry === policyValue || picomatch.isMatch(entry, policyValue)); + } + + return policyValue === fieldValue || picomatch.isMatch(fieldValue, policyValue); +}; diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts index 02440ebe7..d54a49f38 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts @@ -4,30 +4,34 @@ import https from "https"; import jwt from "jsonwebtoken"; import { JwksClient } from "jwks-rsa"; -import { IdentityAuthMethod, SecretKeyEncoding, TIdentityOidcAuthsUpdate } from "@app/db/schemas"; +import { IdentityAuthMethod, TIdentityOidcAuthsUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; -import { getConfig } from "@app/lib/config/env"; -import { generateAsymmetricKeyPair } from "@app/lib/crypto"; +import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { - decryptSymmetric, - encryptSymmetric, - generateSymmetricKey, - infisicalSymmetricDecrypt, - infisicalSymmetricEncypt -} from "@app/lib/crypto/encryption"; -import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { getConfig } from "@app/lib/config/env"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; +import { getStringValueByDot } from "@app/lib/template/dot-access"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; -import { TOrgBotDALFactory } from "../org/org-bot-dal"; +import { TKmsServiceFactory } from "../kms/kms-service"; +import { KmsDataKey } from "../kms/kms-types"; +import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityOidcAuthDALFactory } from "./identity-oidc-auth-dal"; -import { doesFieldValueMatchOidcPolicy } from "./identity-oidc-auth-fns"; +import { doesAudValueMatchOidcPolicy, doesFieldValueMatchOidcPolicy } from "./identity-oidc-auth-fns"; import { TAttachOidcAuthDTO, TGetOidcAuthDTO, @@ -39,10 +43,10 @@ import { type TIdentityOidcAuthServiceFactoryDep = { identityOidcAuthDAL: TIdentityOidcAuthDALFactory; identityOrgMembershipDAL: Pick; - identityAccessTokenDAL: Pick; + identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; - orgBotDAL: Pick; + kmsService: Pick; }; export type TIdentityOidcAuthServiceFactory = ReturnType; @@ -53,7 +57,7 @@ export const identityOidcAuthServiceFactory = ({ permissionService, licenseService, identityAccessTokenDAL, - orgBotDAL + kmsService }: TIdentityOidcAuthServiceFactoryDep) => { const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => { const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId }); @@ -70,38 +74,21 @@ export const identityOidcAuthServiceFactory = ({ }); } - const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }); - if (!orgBot) { - throw new NotFoundError({ - message: `Organization bot not found for organization with ID '${identityMembershipOrg.orgId}'`, - name: "OrgBotNotFound" - }); - } - - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId }); - const { encryptedCaCert, caCertIV, caCertTag } = identityOidcAuth; - let caCert = ""; - if (encryptedCaCert && caCertIV && caCertTag) { - caCert = decryptSymmetric({ - ciphertext: encryptedCaCert, - iv: caCertIV, - tag: caCertTag, - key - }); + if (identityOidcAuth.encryptedCaCertificate) { + caCert = decryptor({ cipherTextBlob: identityOidcAuth.encryptedCaCertificate }).toString(); } const requestAgent = new https.Agent({ ca: caCert, rejectUnauthorized: !!caCert }); const { data: discoveryDoc } = await axios.get<{ jwks_uri: string }>( `${identityOidcAuth.oidcDiscoveryUrl}/.well-known/openid-configuration`, { - httpsAgent: requestAgent + httpsAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined } ); const jwksUri = discoveryDoc.jwks_uri; @@ -115,7 +102,7 @@ export const identityOidcAuthServiceFactory = ({ const client = new JwksClient({ jwksUri, - requestAgent + requestAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined }); const { kid } = decodedToken.header; @@ -132,7 +119,6 @@ export const identityOidcAuthServiceFactory = ({ message: `Access denied: ${error.message}` }); } - throw error; } @@ -148,7 +134,7 @@ export const identityOidcAuthServiceFactory = ({ if ( !identityOidcAuth.boundAudiences .split(", ") - .some((policyValue) => doesFieldValueMatchOidcPolicy(tokenData.aud, policyValue)) + .some((policyValue) => doesAudValueMatchOidcPolicy(tokenData.aud, policyValue)) ) { throw new UnauthorizedError({ message: "Access denied: OIDC audience not allowed." @@ -159,10 +145,16 @@ export const identityOidcAuthServiceFactory = ({ if (identityOidcAuth.boundClaims) { Object.keys(identityOidcAuth.boundClaims).forEach((claimKey) => { const claimValue = (identityOidcAuth.boundClaims as Record)[claimKey]; + const value = getStringValueByDot(tokenData, claimKey) || ""; + + if (!value) { + throw new UnauthorizedError({ + message: `Access denied: token has no ${claimKey} field` + }); + } + // handle both single and multi-valued claims - if ( - !claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(tokenData[claimKey], claimEntry)) - ) { + if (!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(value, claimEntry))) { throw new UnauthorizedError({ message: "Access denied: OIDC claim not allowed." }); @@ -170,6 +162,20 @@ export const identityOidcAuthServiceFactory = ({ }); } + const filteredClaims: Record = {}; + if (identityOidcAuth.claimMetadataMapping) { + Object.keys(identityOidcAuth.claimMetadataMapping).forEach((permissionKey) => { + const claimKey = (identityOidcAuth.claimMetadataMapping as Record)[permissionKey]; + const value = getStringValueByDot(tokenData, claimKey) || ""; + if (!value) { + throw new UnauthorizedError({ + message: `Access denied: token has no ${claimKey} field` + }); + } + filteredClaims[permissionKey] = value; + }); + } + const identityAccessToken = await identityOidcAuthDAL.transaction(async (tx) => { const newToken = await identityAccessTokenDAL.create( { @@ -191,18 +197,23 @@ export const identityOidcAuthServiceFactory = ({ { identityId: identityOidcAuth.identityId, identityAccessTokenId: identityAccessToken.id, - authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN, + identityAuth: { + oidc: { + claims: filteredClaims + } + } } as TIdentityAccessTokenJwtPayload, appCfg.AUTH_SECRET, - { - expiresIn: - Number(identityAccessToken.accessTokenMaxTTL) === 0 - ? undefined - : Number(identityAccessToken.accessTokenMaxTTL) - } + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } ); - return { accessToken, identityOidcAuth, identityAccessToken, identityMembershipOrg }; + return { accessToken, identityOidcAuth, identityAccessToken, identityMembershipOrg, oidcTokenData: tokenData }; }; const attachOidcAuth = async ({ @@ -212,6 +223,7 @@ export const identityOidcAuthServiceFactory = ({ boundIssuer, boundAudiences, boundClaims, + claimMetadataMapping, boundSubject, accessTokenTTL, accessTokenMaxTTL, @@ -220,8 +232,10 @@ export const identityOidcAuthServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TAttachOidcAuthDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) { if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); @@ -244,7 +258,7 @@ export const identityOidcAuthServiceFactory = ({ actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { @@ -264,67 +278,21 @@ export const identityOidcAuthServiceFactory = ({ return extractIPDetails(accessTokenTrustedIp.ipAddress); }); - const orgBot = await orgBotDAL.transaction(async (tx) => { - const doc = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }, tx); - if (doc) return doc; - - const { privateKey, publicKey } = generateAsymmetricKeyPair(); - const key = generateSymmetricKey(); - const { - ciphertext: encryptedPrivateKey, - iv: privateKeyIV, - tag: privateKeyTag, - encoding: privateKeyKeyEncoding, - algorithm: privateKeyAlgorithm - } = infisicalSymmetricEncypt(privateKey); - const { - ciphertext: encryptedSymmetricKey, - iv: symmetricKeyIV, - tag: symmetricKeyTag, - encoding: symmetricKeyKeyEncoding, - algorithm: symmetricKeyAlgorithm - } = infisicalSymmetricEncypt(key); - - return orgBotDAL.create( - { - name: "Infisical org bot", - publicKey, - privateKeyIV, - encryptedPrivateKey, - symmetricKeyIV, - symmetricKeyTag, - encryptedSymmetricKey, - symmetricKeyAlgorithm, - orgId: identityMembershipOrg.orgId, - privateKeyTag, - privateKeyAlgorithm, - privateKeyKeyEncoding, - symmetricKeyKeyEncoding - }, - tx - ); + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId }); - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding - }); - - const { ciphertext: encryptedCaCert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); - const identityOidcAuth = await identityOidcAuthDAL.transaction(async (tx) => { const doc = await identityOidcAuthDAL.create( { identityId: identityMembershipOrg.identityId, oidcDiscoveryUrl, - encryptedCaCert, - caCertIV, - caCertTag, + encryptedCaCertificate: encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob, boundIssuer, boundAudiences, boundClaims, + claimMetadataMapping, boundSubject, accessTokenMaxTTL, accessTokenTTL, @@ -345,6 +313,7 @@ export const identityOidcAuthServiceFactory = ({ boundIssuer, boundAudiences, boundClaims, + claimMetadataMapping, boundSubject, accessTokenTTL, accessTokenMaxTTL, @@ -381,7 +350,7 @@ export const identityOidcAuthServiceFactory = ({ actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { @@ -406,6 +375,7 @@ export const identityOidcAuthServiceFactory = ({ boundIssuer, boundAudiences, boundClaims, + claimMetadataMapping, boundSubject, accessTokenMaxTTL, accessTokenTTL, @@ -415,38 +385,19 @@ export const identityOidcAuthServiceFactory = ({ : undefined }; - const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }); - if (!orgBot) { - throw new NotFoundError({ - message: `Organization bot not found for organization with ID '${identityMembershipOrg.orgId}'`, - name: "OrgBotNotFound" - }); - } - - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId }); if (caCert !== undefined) { - const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); - updateQuery.encryptedCaCert = encryptedCACert; - updateQuery.caCertIV = caCertIV; - updateQuery.caCertTag = caCertTag; + updateQuery.encryptedCaCertificate = encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob; } const updatedOidcAuth = await identityOidcAuthDAL.updateById(identityOidcAuth.id, updateQuery); - const updatedCACert = - updatedOidcAuth.encryptedCaCert && updatedOidcAuth.caCertIV && updatedOidcAuth.caCertTag - ? decryptSymmetric({ - ciphertext: updatedOidcAuth.encryptedCaCert, - iv: updatedOidcAuth.caCertIV, - tag: updatedOidcAuth.caCertTag, - key - }) - : ""; + const updatedCACert = updatedOidcAuth.encryptedCaCertificate + ? decryptor({ cipherTextBlob: updatedOidcAuth.encryptedCaCertificate }).toString() + : ""; return { ...updatedOidcAuth, @@ -472,31 +423,18 @@ export const identityOidcAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId }); - const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }); - if (!orgBot) { - throw new NotFoundError({ - message: `Organization bot not found for organization with ID ${identityMembershipOrg.orgId}`, - name: "OrgBotNotFound" - }); - } - - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId }); - const caCert = decryptSymmetric({ - ciphertext: identityOidcAuth.encryptedCaCert, - iv: identityOidcAuth.caCertIV, - tag: identityOidcAuth.caCertTag, - key - }); + const caCert = identityOidcAuth.encryptedCaCertificate + ? decryptor({ cipherTextBlob: identityOidcAuth.encryptedCaCertificate }).toString() + : ""; return { ...identityOidcAuth, orgId: identityMembershipOrg.orgId, caCert }; }; @@ -513,7 +451,7 @@ export const identityOidcAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, identityMembershipOrg.orgId, @@ -521,7 +459,7 @@ export const identityOidcAuthServiceFactory = ({ actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, @@ -531,14 +469,29 @@ export const identityOidcAuthServiceFactory = ({ actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) { - throw new ForbiddenRequestError({ - message: "Failed to revoke OIDC auth of identity with more privileged role" + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to revoke oidc auth of identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } }); - } const revokedIdentityOidcAuth = await identityOidcAuthDAL.transaction(async (tx) => { const deletedOidcAuth = await identityOidcAuthDAL.delete({ identityId }, tx); + await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.OIDC_AUTH }, tx); + return { ...deletedOidcAuth?.[0], orgId: identityMembershipOrg.orgId }; }); diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts index 761f68aa7..fc5da3e27 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts @@ -7,11 +7,13 @@ export type TAttachOidcAuthDTO = { boundIssuer: string; boundAudiences: string; boundClaims: Record; + claimMetadataMapping?: Record; boundSubject: string; accessTokenTTL: number; accessTokenMaxTTL: number; accessTokenNumUsesLimit: number; accessTokenTrustedIps: { ipAddress: string }[]; + isActorSuperAdmin?: boolean; } & Omit; export type TUpdateOidcAuthDTO = { @@ -21,6 +23,7 @@ export type TUpdateOidcAuthDTO = { boundIssuer?: string; boundAudiences?: string; boundClaims?: Record; + claimMetadataMapping?: Record; boundSubject?: string; accessTokenTTL?: number; accessTokenMaxTTL?: number; diff --git a/backend/src/services/identity-project/identity-project-dal.ts b/backend/src/services/identity-project/identity-project-dal.ts index fd8eaa15d..bc4f4a303 100644 --- a/backend/src/services/identity-project/identity-project-dal.ts +++ b/backend/src/services/identity-project/identity-project-dal.ts @@ -102,6 +102,7 @@ export const identityProjectDALFactory = (db: TDbClient) => { db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole), db.ref("projectId").withSchema(TableName.IdentityProjectMembership), db.ref("name").as("projectName").withSchema(TableName.Project), + db.ref("type").as("projectType").withSchema(TableName.Project), db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), @@ -126,7 +127,8 @@ export const identityProjectDALFactory = (db: TDbClient) => { createdAt, updatedAt, projectId, - projectName + projectName, + projectType }) => ({ id, identityId, @@ -147,7 +149,8 @@ export const identityProjectDALFactory = (db: TDbClient) => { }, project: { id: projectId, - name: projectName + name: projectName, + type: projectType } }), key: "id", diff --git a/backend/src/services/identity-project/identity-project-service.ts b/backend/src/services/identity-project/identity-project-service.ts index a49b15c1b..14df0cd4a 100644 --- a/backend/src/services/identity-project/identity-project-service.ts +++ b/backend/src/services/identity-project/identity-project-service.ts @@ -1,14 +1,16 @@ -import { ForbiddenError } from "@casl/ability"; -import ms from "ms"; +import { ForbiddenError, subject } from "@casl/ability"; -import { ProjectMembershipRole } from "@app/db/schemas"; +import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; -import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; +import { ms } from "@app/lib/ms"; -import { ActorType } from "../auth/auth-type"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TProjectDALFactory } from "../project/project-dal"; import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types"; @@ -19,6 +21,7 @@ import { TCreateProjectIdentityDTO, TDeleteProjectIdentityDTO, TGetProjectIdentityByIdentityIdDTO, + TGetProjectIdentityByMembershipIdDTO, TListProjectIdentityDTO, TUpdateProjectIdentityDTO } from "./identity-project-types"; @@ -54,14 +57,20 @@ export const identityProjectServiceFactory = ({ projectId, roles }: TCreateProjectIdentityDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, membership } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Create, + subject(ProjectPermissionSub.Identity, { + identityId + }) ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Identity); const existingIdentity = await identityProjectDAL.findOne({ identityId, projectId }); if (existingIdentity) @@ -85,11 +94,23 @@ export const identityProjectServiceFactory = ({ projectId ); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission); - - if (!hasRequiredPriviledges) { - throw new ForbiddenRequestError({ message: "Failed to change to a more privileged role" }); - } + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionSub.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to assign to role", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionSub.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); } // validate custom roles input @@ -154,14 +175,18 @@ export const identityProjectServiceFactory = ({ actorAuthMethod, actorOrgId }: TUpdateProjectIdentityDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, membership } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Edit, + subject(ProjectPermissionSub.Identity, { identityId }) ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); const projectIdentity = await identityProjectDAL.findOne({ identityId, projectId }); if (!projectIdentity) @@ -175,14 +200,34 @@ export const identityProjectServiceFactory = ({ projectId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) { - throw new ForbiddenRequestError({ message: "Failed to change to a more privileged role" }); - } + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionSub.Identity, + permission, + rolePermission + ); + + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to change role", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionSub.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); } // validate custom roles input const customInputRoles = roles.filter( - ({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole) + ({ role }) => + !Object.values(ProjectMembershipRole) + // we don't want to include custom in this check; + // this unintentionally enables setting slug to custom which is reserved + .filter((r) => r !== ProjectMembershipRole.Custom) + .includes(role as ProjectMembershipRole) ); const hasCustomRole = Boolean(customInputRoles.length); const customRoles = hasCustomRole @@ -241,23 +286,18 @@ export const identityProjectServiceFactory = ({ throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - identityProjectMembership.projectId, + projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Delete, + subject(ProjectPermissionSub.Identity, { identityId }) ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity); - const { permission: identityRolePermission } = await permissionService.getProjectPermission( - ActorType.IDENTITY, - identityId, - identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId - ); - if (!isAtLeastAsPrivileged(permission, identityRolePermission)) - throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" }); const [deletedIdentity] = await identityProjectDAL.delete({ identityId, projectId }); return deletedIdentity; @@ -275,14 +315,18 @@ export const identityProjectServiceFactory = ({ orderDirection, search }: TListProjectIdentityDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + ProjectPermissionSub.Identity ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); const identityMemberships = await identityProjectDAL.findByProjectId(projectId, { limit, @@ -305,14 +349,19 @@ export const identityProjectServiceFactory = ({ actorOrgId, identityId }: TGetProjectIdentityByIdentityIdDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + subject(ProjectPermissionSub.Identity, { identityId }) ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); const [identityMembership] = await identityProjectDAL.findByProjectId(projectId, { identityId }); if (!identityMembership) @@ -322,11 +371,48 @@ export const identityProjectServiceFactory = ({ return identityMembership; }; + const getProjectIdentityByMembershipId = async ({ + identityMembershipId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TGetProjectIdentityByMembershipIdDTO) => { + const membership = await identityProjectDAL.findOne({ id: identityMembershipId }); + + if (!membership) { + throw new NotFoundError({ + message: `Project membership with ID '${identityMembershipId}' not found` + }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: membership.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + subject(ProjectPermissionSub.Identity, { identityId: membership.identityId }) + ); + + const [identityMembership] = await identityProjectDAL.findByProjectId(membership.projectId, { + identityId: membership.identityId + }); + + return identityMembership; + }; + return { createProjectIdentity, updateProjectIdentity, deleteProjectIdentity, listProjectIdentities, - getProjectIdentityByIdentityId + getProjectIdentityByIdentityId, + getProjectIdentityByMembershipId }; }; diff --git a/backend/src/services/identity-project/identity-project-types.ts b/backend/src/services/identity-project/identity-project-types.ts index 607fd4823..bc85ca398 100644 --- a/backend/src/services/identity-project/identity-project-types.ts +++ b/backend/src/services/identity-project/identity-project-types.ts @@ -52,6 +52,10 @@ export type TGetProjectIdentityByIdentityIdDTO = { identityId: string; } & TProjectPermission; +export type TGetProjectIdentityByMembershipIdDTO = { + identityMembershipId: string; +} & Omit; + export enum ProjectIdentityOrderBy { Name = "name" } diff --git a/backend/src/services/identity-token-auth/identity-token-auth-service.ts b/backend/src/services/identity-token-auth/identity-token-auth-service.ts index 39f2f6589..549512452 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-service.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-service.ts @@ -3,17 +3,21 @@ import jwt from "jsonwebtoken"; import { IdentityAuthMethod, TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; -import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityTokenAuthDALFactory } from "./identity-token-auth-dal"; import { TAttachTokenAuthDTO, @@ -59,8 +63,11 @@ export const identityTokenAuthServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TAttachTokenAuthDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); @@ -81,7 +88,7 @@ export const identityTokenAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { @@ -126,8 +133,11 @@ export const identityTokenAuthServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TUpdateTokenAuthDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); @@ -154,7 +164,7 @@ export const identityTokenAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { @@ -208,7 +218,7 @@ export const identityTokenAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...identityTokenAuth, orgId: identityMembershipOrg.orgId }; }; @@ -218,8 +228,11 @@ export const identityTokenAuthServiceFactory = ({ actorId, actor, actorAuthMethod, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TRevokeTokenAuthDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); @@ -235,9 +248,9 @@ export const identityTokenAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( + const { permission: rolePermission, membership } = await permissionService.getOrgPermission( ActorType.IDENTITY, identityMembershipOrg.identityId, identityMembershipOrg.orgId, @@ -245,11 +258,23 @@ export const identityTokenAuthServiceFactory = ({ actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) { - throw new ForbiddenRequestError({ - message: "Failed to revoke Token Auth of identity with more privileged role" + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to revoke token auth of identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } }); - } const revokedIdentityTokenAuth = await identityTokenAuthDAL.transaction(async (tx) => { const deletedTokenAuth = await identityTokenAuthDAL.delete({ identityId }, tx); @@ -269,8 +294,11 @@ export const identityTokenAuthServiceFactory = ({ actor, actorAuthMethod, actorOrgId, - name + name, + isActorSuperAdmin }: TCreateTokenAuthTokenDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); @@ -286,19 +314,32 @@ export const identityTokenAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( + const { permission: rolePermission, membership } = await permissionService.getOrgPermission( ActorType.IDENTITY, identityMembershipOrg.identityId, identityMembershipOrg.orgId, actorAuthMethod, actorOrgId ); - const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); - if (!hasPriviledge) - throw new ForbiddenRequestError({ - message: "Failed to create token for identity with more privileged role" + + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.CreateToken, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to create token for identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.CreateToken, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } }); const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId }); @@ -328,12 +369,12 @@ export const identityTokenAuthServiceFactory = ({ authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN } as TIdentityAccessTokenJwtPayload, appCfg.AUTH_SECRET, - { - expiresIn: - Number(identityAccessToken.accessTokenMaxTTL) === 0 - ? undefined - : Number(identityAccessToken.accessTokenMaxTTL) - } + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } ); return { accessToken, identityTokenAuth, identityAccessToken, identityMembershipOrg }; @@ -346,8 +387,11 @@ export const identityTokenAuthServiceFactory = ({ actorId, actor, actorAuthMethod, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TGetTokenAuthTokensDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); @@ -363,7 +407,7 @@ export const identityTokenAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const tokens = await identityAccessTokenDAL.find( { @@ -382,11 +426,12 @@ export const identityTokenAuthServiceFactory = ({ actorId, actor, actorAuthMethod, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TUpdateTokenAuthTokenDTO) => { const foundToken = await identityAccessTokenDAL.findOne({ - id: tokenId, - authMethod: IdentityAuthMethod.TOKEN_AUTH + [`${TableName.IdentityAccessToken}.id` as "id"]: tokenId, + [`${TableName.IdentityAccessToken}.authMethod` as "authMethod"]: IdentityAuthMethod.TOKEN_AUTH }); if (!foundToken) throw new NotFoundError({ message: `Token with ID ${tokenId} not found` }); @@ -394,6 +439,8 @@ export const identityTokenAuthServiceFactory = ({ if (!identityMembershipOrg) { throw new NotFoundError({ message: `Failed to find identity with ID ${foundToken.identityId}` }); } + + await validateIdentityUpdateForSuperAdminPrivileges(foundToken.identityId, isActorSuperAdmin); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { throw new BadRequestError({ message: "The identity does not have Token Auth" @@ -406,19 +453,31 @@ export const identityTokenAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( + const { permission: rolePermission, membership } = await permissionService.getOrgPermission( ActorType.IDENTITY, identityMembershipOrg.identityId, identityMembershipOrg.orgId, actorAuthMethod, actorOrgId ); - const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); - if (!hasPriviledge) - throw new ForbiddenRequestError({ - message: "Failed to update token for identity with more privileged role" + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.CreateToken, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update token for identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.CreateToken, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } }); const [token] = await identityAccessTokenDAL.update( @@ -440,18 +499,22 @@ export const identityTokenAuthServiceFactory = ({ actorId, actor, actorAuthMethod, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TRevokeTokenAuthTokenDTO) => { const identityAccessToken = await identityAccessTokenDAL.findOne({ [`${TableName.IdentityAccessToken}.id` as "id"]: tokenId, - isAccessTokenRevoked: false, - authMethod: IdentityAuthMethod.TOKEN_AUTH + [`${TableName.IdentityAccessToken}.isAccessTokenRevoked` as "isAccessTokenRevoked"]: false, + [`${TableName.IdentityAccessToken}.authMethod` as "authMethod"]: IdentityAuthMethod.TOKEN_AUTH }); + if (!identityAccessToken) throw new NotFoundError({ message: `Token with ID ${tokenId} not found or already revoked` }); + await validateIdentityUpdateForSuperAdminPrivileges(identityAccessToken.identityId, isActorSuperAdmin); + const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: identityAccessToken.identityId }); @@ -467,7 +530,7 @@ export const identityTokenAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const [revokedToken] = await identityAccessTokenDAL.update( { diff --git a/backend/src/services/identity-token-auth/identity-token-auth-types.ts b/backend/src/services/identity-token-auth/identity-token-auth-types.ts index 12c689728..16cd60db7 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-types.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-types.ts @@ -6,6 +6,7 @@ export type TAttachTokenAuthDTO = { accessTokenMaxTTL: number; accessTokenNumUsesLimit: number; accessTokenTrustedIps: { ipAddress: string }[]; + isActorSuperAdmin?: boolean; } & Omit; export type TUpdateTokenAuthDTO = { @@ -14,6 +15,7 @@ export type TUpdateTokenAuthDTO = { accessTokenMaxTTL?: number; accessTokenNumUsesLimit?: number; accessTokenTrustedIps?: { ipAddress: string }[]; + isActorSuperAdmin?: boolean; } & Omit; export type TGetTokenAuthDTO = { @@ -22,24 +24,29 @@ export type TGetTokenAuthDTO = { export type TRevokeTokenAuthDTO = { identityId: string; + isActorSuperAdmin?: boolean; } & Omit; export type TCreateTokenAuthTokenDTO = { identityId: string; name?: string; + isActorSuperAdmin?: boolean; } & Omit; export type TGetTokenAuthTokensDTO = { identityId: string; offset: number; limit: number; + isActorSuperAdmin?: boolean; } & Omit; export type TUpdateTokenAuthTokenDTO = { tokenId: string; name?: string; + isActorSuperAdmin?: boolean; } & Omit; export type TRevokeTokenAuthTokenDTO = { tokenId: string; + isActorSuperAdmin?: boolean; } & Omit; diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index b456c1647..8ab499e65 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -6,17 +6,21 @@ import jwt from "jsonwebtoken"; import { IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; -import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityUaClientSecretDALFactory } from "./identity-ua-client-secret-dal"; import { TIdentityUaDALFactory } from "./identity-ua-dal"; import { @@ -63,14 +67,22 @@ export const identityUaServiceFactory = ({ ipAddress: ip, trustedIps: identityUa.clientSecretTrustedIps as TIp[] }); + const clientSecretPrefix = clientSecret.slice(0, 4); const clientSecrtInfo = await identityUaClientSecretDAL.find({ identityUAId: identityUa.id, - isClientSecretRevoked: false + isClientSecretRevoked: false, + clientSecretPrefix }); - const validClientSecretInfo = clientSecrtInfo.find(({ clientSecretHash }) => - bcrypt.compareSync(clientSecret, clientSecretHash) - ); + let validClientSecretInfo: (typeof clientSecrtInfo)[0] | null = null; + for await (const info of clientSecrtInfo) { + const isMatch = await bcrypt.compare(clientSecret, info.clientSecretHash); + if (isMatch) { + validClientSecretInfo = info; + break; + } + } + if (!validClientSecretInfo) throw new UnauthorizedError({ message: "Invalid credentials" }); const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo; @@ -103,7 +115,7 @@ export const identityUaServiceFactory = ({ } const identityAccessToken = await identityUaDAL.transaction(async (tx) => { - const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo.id, tx); + const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx); const newToken = await identityAccessTokenDAL.create( { identityId: identityUa.identityId, @@ -129,12 +141,12 @@ export const identityUaServiceFactory = ({ authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN } as TIdentityAccessTokenJwtPayload, appCfg.AUTH_SECRET, - { - expiresIn: - Number(identityAccessToken.accessTokenMaxTTL) === 0 - ? undefined - : Number(identityAccessToken.accessTokenMaxTTL) - } + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } ); return { accessToken, identityUa, validClientSecretInfo, identityAccessToken, identityMembershipOrg }; @@ -150,8 +162,11 @@ export const identityUaServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TAttachUaDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); @@ -172,7 +187,7 @@ export const identityUaServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedClientSecretTrustedIps = clientSecretTrustedIps.map((clientSecretTrustedIp) => { @@ -241,14 +256,17 @@ export const identityUaServiceFactory = ({ const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + const uaIdentityAuth = await identityUaDAL.findOne({ identityId }); + if (!uaIdentityAuth) { + throw new NotFoundError({ message: `Failed to find universal auth for identity with ID ${identityId}` }); + } + if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) { throw new BadRequestError({ message: "The identity does not have universal auth" }); } - const uaIdentityAuth = await identityUaDAL.findOne({ identityId }); - if ( (accessTokenMaxTTL || uaIdentityAuth.accessTokenMaxTTL) > 0 && (accessTokenTTL || uaIdentityAuth.accessTokenMaxTTL) > (accessTokenMaxTTL || uaIdentityAuth.accessTokenMaxTTL) @@ -263,7 +281,7 @@ export const identityUaServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedClientSecretTrustedIps = clientSecretTrustedIps?.map((clientSecretTrustedIp) => { @@ -317,14 +335,17 @@ export const identityUaServiceFactory = ({ const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + const uaIdentityAuth = await identityUaDAL.findOne({ identityId }); + if (!uaIdentityAuth) { + throw new NotFoundError({ message: `Failed to find universal auth for identity with ID ${identityId}` }); + } + if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) { throw new BadRequestError({ message: "The identity does not have universal auth" }); } - const uaIdentityAuth = await identityUaDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission( actor, actorId, @@ -332,7 +353,7 @@ export const identityUaServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...uaIdentityAuth, orgId: identityMembershipOrg.orgId }; }; @@ -358,18 +379,31 @@ export const identityUaServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( + const { permission: rolePermission, membership } = await permissionService.getOrgPermission( ActorType.IDENTITY, identityMembershipOrg.identityId, identityMembershipOrg.orgId, actorAuthMethod, actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) - throw new ForbiddenRequestError({ - message: "Failed to revoke universal auth of identity with more privileged role" + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to revoke universal auth of identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } }); const revokedIdentityUniversalAuth = await identityUaDAL.transaction(async (tx) => { @@ -398,14 +432,14 @@ export const identityUaServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, identityMembershipOrg.orgId, actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, @@ -414,10 +448,22 @@ export const identityUaServiceFactory = ({ actorAuthMethod, actorOrgId ); - const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); - if (!hasPriviledge) - throw new ForbiddenRequestError({ - message: "Failed to add identity to project with more privileged role" + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.CreateToken, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to create client secret for identity.", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.CreateToken, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } }); const appCfg = getConfig(); @@ -425,6 +471,7 @@ export const identityUaServiceFactory = ({ const clientSecretHash = await bcrypt.hash(clientSecret, appCfg.SALT_ROUNDS); const identityUaAuth = await identityUaDAL.findOne({ identityId: identityMembershipOrg.identityId }); + if (!identityUaAuth) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); const identityUaClientSecret = await identityUaClientSecretDAL.create({ identityUAId: identityUaAuth.id, @@ -458,14 +505,14 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, identityMembershipOrg.orgId, actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, @@ -475,9 +522,22 @@ export const identityUaServiceFactory = ({ actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) - throw new ForbiddenRequestError({ - message: "Failed to add identity to project with more privileged role" + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.GetToken, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to get identity client secret with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.GetToken, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } }); const identityUniversalAuth = await identityUaDAL.findOne({ @@ -508,14 +568,20 @@ export const identityUaServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const identityUa = await identityUaDAL.findOne({ identityId }); + if (!identityUa) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + + const clientSecret = await identityUaClientSecretDAL.findOne({ id: clientSecretId, identityUAId: identityUa.id }); + if (!clientSecret) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, identityMembershipOrg.orgId, actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, @@ -524,12 +590,24 @@ export const identityUaServiceFactory = ({ actorAuthMethod, actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) - throw new ForbiddenRequestError({ - message: "Failed to read identity client secret of project with more privileged role" + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.GetToken, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to read identity client secret of identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.GetToken, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } }); - const clientSecret = await identityUaClientSecretDAL.findById(clientSecretId); return { ...clientSecret, identityId, orgId: identityMembershipOrg.orgId }; }; @@ -550,14 +628,20 @@ export const identityUaServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const identityUa = await identityUaDAL.findOne({ identityId }); + if (!identityUa) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + + const clientSecret = await identityUaClientSecretDAL.findOne({ id: clientSecretId, identityUAId: identityUa.id }); + if (!clientSecret) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, identityMembershipOrg.orgId, actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, @@ -567,15 +651,30 @@ export const identityUaServiceFactory = ({ actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) - throw new ForbiddenRequestError({ - message: "Failed to revoke identity client secret with more privileged role" + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.DeleteToken, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) { + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to revoke identity client secret with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.DeleteToken, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } }); + } - const clientSecret = await identityUaClientSecretDAL.updateById(clientSecretId, { + const updatedClientSecret = await identityUaClientSecretDAL.updateById(clientSecretId, { isClientSecretRevoked: true }); - return { ...clientSecret, identityId, orgId: identityMembershipOrg.orgId }; + + return { ...updatedClientSecret, identityId, orgId: identityMembershipOrg.orgId }; }; return { diff --git a/backend/src/services/identity-ua/identity-ua-types.ts b/backend/src/services/identity-ua/identity-ua-types.ts index 2045c2143..07b6a4810 100644 --- a/backend/src/services/identity-ua/identity-ua-types.ts +++ b/backend/src/services/identity-ua/identity-ua-types.ts @@ -7,6 +7,7 @@ export type TAttachUaDTO = { accessTokenNumUsesLimit: number; clientSecretTrustedIps: { ipAddress: string }[]; accessTokenTrustedIps: { ipAddress: string }[]; + isActorSuperAdmin?: boolean; } & Omit; export type TUpdateUaDTO = { diff --git a/backend/src/services/identity/identity-dal.ts b/backend/src/services/identity/identity-dal.ts index a74a84ce3..c4d0b6307 100644 --- a/backend/src/services/identity/identity-dal.ts +++ b/backend/src/services/identity/identity-dal.ts @@ -1,10 +1,42 @@ import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; +import { TableName, TIdentities } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; export type TIdentityDALFactory = ReturnType; export const identityDALFactory = (db: TDbClient) => { const identityOrm = ormify(db, TableName.Identity); - return identityOrm; + + const getIdentitiesByFilter = async ({ + limit, + offset, + searchTerm, + sortBy + }: { + limit: number; + offset: number; + searchTerm: string; + sortBy?: keyof TIdentities; + }) => { + try { + let query = db.replicaNode()(TableName.Identity); + + if (searchTerm) { + query = query.where((qb) => { + void qb.whereILike("name", `%${searchTerm}%`); + }); + } + + if (sortBy) { + query = query.orderBy(sortBy); + } + + return await query.limit(limit).offset(offset).select(selectAllTableCols(TableName.Identity)); + } catch (error) { + throw new DatabaseError({ error, name: "Get identities by filter" }); + } + }; + + return { ...identityOrm, getIdentitiesByFilter }; }; diff --git a/backend/src/services/identity/identity-fns.ts b/backend/src/services/identity/identity-fns.ts index 49cf4d119..2d77e6544 100644 --- a/backend/src/services/identity/identity-fns.ts +++ b/backend/src/services/identity/identity-fns.ts @@ -7,7 +7,8 @@ export const buildAuthMethods = ({ kubernetesId, oidcId, azureId, - tokenId + tokenId, + jwtId }: { uaId?: string; gcpId?: string; @@ -16,6 +17,7 @@ export const buildAuthMethods = ({ oidcId?: string; azureId?: string; tokenId?: string; + jwtId?: string; }) => { return [ ...[uaId ? IdentityAuthMethod.UNIVERSAL_AUTH : null], @@ -24,6 +26,7 @@ export const buildAuthMethods = ({ ...[kubernetesId ? IdentityAuthMethod.KUBERNETES_AUTH : null], ...[oidcId ? IdentityAuthMethod.OIDC_AUTH : null], ...[azureId ? IdentityAuthMethod.AZURE_AUTH : null], - ...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null] + ...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null], + ...[jwtId ? IdentityAuthMethod.JWT_AUTH : null] ].filter((authMethod) => authMethod) as IdentityAuthMethod[]; }; diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts index bbdf96a2b..dbae59bbe 100644 --- a/backend/src/services/identity/identity-org-dal.ts +++ b/backend/src/services/identity/identity-org-dal.ts @@ -6,6 +6,7 @@ import { TIdentityAwsAuths, TIdentityAzureAuths, TIdentityGcpAuths, + TIdentityJwtAuths, TIdentityKubernetesAuths, TIdentityOidcAuths, TIdentityOrgMemberships, @@ -13,10 +14,15 @@ import { TIdentityUniversalAuths, TOrgRoles } from "@app/db/schemas"; -import { DatabaseError } from "@app/lib/errors"; +import { BadRequestError, DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; +import { buildKnexFilterForSearchResource } from "@app/lib/search-resource/db"; import { OrderByDirection } from "@app/lib/types"; -import { OrgIdentityOrderBy, TListOrgIdentitiesByOrgIdDTO } from "@app/services/identity/identity-types"; +import { + OrgIdentityOrderBy, + TListOrgIdentitiesByOrgIdDTO, + TSearchOrgIdentitiesByOrgIdDAL +} from "@app/services/identity/identity-types"; import { buildAuthMethods } from "./identity-fns"; @@ -70,6 +76,11 @@ export const identityOrgDALFactory = (db: TDbClient) => { `${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityTokenAuth}.identityId` ) + .leftJoin( + TableName.IdentityJwtAuth, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityJwtAuth}.identityId` + ) .select( selectAllTableCols(TableName.IdentityOrgMembership), @@ -81,6 +92,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth), db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth), + db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth), db.ref("name").withSchema(TableName.Identity) ); @@ -183,7 +195,11 @@ export const identityOrgDALFactory = (db: TDbClient) => { "paginatedIdentity.identityId", `${TableName.IdentityTokenAuth}.identityId` ) - + .leftJoin( + TableName.IdentityJwtAuth, + "paginatedIdentity.identityId", + `${TableName.IdentityJwtAuth}.identityId` + ) .select( db.ref("id").withSchema("paginatedIdentity"), db.ref("role").withSchema("paginatedIdentity"), @@ -200,7 +216,8 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth), db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), - db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth) + db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth), + db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth) ) // cr stands for custom role .select(db.ref("id").as("crId").withSchema(TableName.OrgRoles)) @@ -237,6 +254,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { uaId, awsId, gcpId, + jwtId, kubernetesId, oidcId, azureId, @@ -271,7 +289,8 @@ export const identityOrgDALFactory = (db: TDbClient) => { kubernetesId, oidcId, azureId, - tokenId + tokenId, + jwtId }) } }), @@ -294,6 +313,214 @@ export const identityOrgDALFactory = (db: TDbClient) => { } }; + const searchIdentities = async ( + { + limit, + offset = 0, + orderBy = OrgIdentityOrderBy.Name, + orderDirection = OrderByDirection.ASC, + searchFilter, + orgId + }: TSearchOrgIdentitiesByOrgIdDAL, + tx?: Knex + ) => { + try { + const searchQuery = (tx || db.replicaNode())(TableName.IdentityOrgMembership) + .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityOrgMembership}.identityId`) + .where(`${TableName.IdentityOrgMembership}.orgId`, orgId) + .leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`) + .orderBy(`${TableName.Identity}.${orderBy}`, orderDirection) + .select(`${TableName.IdentityOrgMembership}.id`) + .select<{ id: string; total_count: string }>( + db.raw( + `count(${TableName.IdentityOrgMembership}."identityId") OVER(PARTITION BY ${TableName.IdentityOrgMembership}."orgId") as total_count` + ) + ) + .as("searchedIdentities"); + + if (searchFilter) { + buildKnexFilterForSearchResource(searchQuery, searchFilter, (attr) => { + switch (attr) { + case "role": + return [`${TableName.OrgRoles}.slug`, `${TableName.IdentityOrgMembership}.role`]; + case "name": + return `${TableName.Identity}.name`; + default: + throw new BadRequestError({ message: `Invalid ${String(attr)} provided` }); + } + }); + } + + if (limit) { + void searchQuery.offset(offset).limit(limit); + } + + type TSubquery = Awaited; + const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership) + .where(`${TableName.IdentityOrgMembership}.orgId`, orgId) + .join(searchQuery, `${TableName.IdentityOrgMembership}.id`, "searchedIdentities.id") + .join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`) + .leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`) + .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { + void queryBuilder + .on(`${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityMetadata}.identityId`) + .andOn(`${TableName.IdentityOrgMembership}.orgId`, `${TableName.IdentityMetadata}.orgId`); + }) + .leftJoin( + TableName.IdentityUniversalAuth, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityUniversalAuth}.identityId` + ) + .leftJoin( + TableName.IdentityGcpAuth, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityGcpAuth}.identityId` + ) + .leftJoin( + TableName.IdentityAwsAuth, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityAwsAuth}.identityId` + ) + .leftJoin( + TableName.IdentityKubernetesAuth, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityKubernetesAuth}.identityId` + ) + .leftJoin( + TableName.IdentityOidcAuth, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityOidcAuth}.identityId` + ) + .leftJoin( + TableName.IdentityAzureAuth, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityAzureAuth}.identityId` + ) + .leftJoin( + TableName.IdentityTokenAuth, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityTokenAuth}.identityId` + ) + .leftJoin( + TableName.IdentityJwtAuth, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityJwtAuth}.identityId` + ) + .select( + db.ref("id").withSchema(TableName.IdentityOrgMembership), + db.ref("total_count").withSchema("searchedIdentities"), + db.ref("role").withSchema(TableName.IdentityOrgMembership), + db.ref("roleId").withSchema(TableName.IdentityOrgMembership), + db.ref("orgId").withSchema(TableName.IdentityOrgMembership), + db.ref("createdAt").withSchema(TableName.IdentityOrgMembership), + db.ref("updatedAt").withSchema(TableName.IdentityOrgMembership), + db.ref("identityId").withSchema(TableName.IdentityOrgMembership).as("identityId"), + db.ref("name").withSchema(TableName.Identity).as("identityName"), + + db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), + db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), + db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), + db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), + db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth), + db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), + db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth), + db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth) + ) + // cr stands for custom role + .select(db.ref("id").as("crId").withSchema(TableName.OrgRoles)) + .select(db.ref("name").as("crName").withSchema(TableName.OrgRoles)) + .select(db.ref("slug").as("crSlug").withSchema(TableName.OrgRoles)) + .select(db.ref("description").as("crDescription").withSchema(TableName.OrgRoles)) + .select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles)) + .select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles)) + .select( + db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue") + ); + + if (orderBy === OrgIdentityOrderBy.Name) { + void query.orderBy("identityName", orderDirection); + } + + const docs = await query; + const formattedDocs = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: ({ + crId, + crDescription, + crSlug, + crPermission, + crName, + identityId, + identityName, + role, + roleId, + total_count, + id, + uaId, + awsId, + gcpId, + jwtId, + kubernetesId, + oidcId, + azureId, + tokenId, + createdAt, + updatedAt + }) => ({ + role, + roleId, + identityId, + id, + total_count: total_count as string, + orgId, + createdAt, + updatedAt, + customRole: roleId + ? { + id: crId, + name: crName, + slug: crSlug, + permissions: crPermission, + description: crDescription + } + : undefined, + identity: { + id: identityId, + name: identityName, + authMethods: buildAuthMethods({ + uaId, + awsId, + gcpId, + kubernetesId, + oidcId, + azureId, + tokenId, + jwtId + }) + } + }), + childrenMapper: [ + { + key: "metadataId", + label: "metadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) + } + ] + }); + + return { docs: formattedDocs, totalCount: Number(formattedDocs?.[0]?.total_count ?? 0) }; + } catch (error) { + throw new DatabaseError({ error, name: "FindByOrgId" }); + } + }; + const countAllOrgIdentities = async ( { search, ...filter }: Partial & Pick, tx?: Knex @@ -316,5 +543,5 @@ export const identityOrgDALFactory = (db: TDbClient) => { } }; - return { ...identityOrgOrm, find, findOne, countAllOrgIdentities }; + return { ...identityOrgOrm, find, findOne, countAllOrgIdentities, searchIdentities }; }; diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index fffcbacc2..6f72b3c6e 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -2,13 +2,16 @@ import { ForbiddenError } from "@casl/ability"; import { OrgMembershipRole, TableName, TOrgRoles } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; -import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; -import { ActorType } from "../auth/auth-type"; +import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityDALFactory } from "./identity-dal"; import { TIdentityMetadataDALFactory } from "./identity-metadata-dal"; import { TIdentityOrgDALFactory } from "./identity-org-dal"; @@ -18,6 +21,7 @@ import { TGetIdentityByIdDTO, TListOrgIdentitiesByOrgIdDTO, TListProjectIdentitiesByIdentityIdDTO, + TSearchOrgIdentitiesByOrgIdDTO, TUpdateIdentityDTO } from "./identity-types"; @@ -50,17 +54,37 @@ export const identityServiceFactory = ({ actorOrgId, metadata }: TCreateIdentityDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + const { permission, membership } = await permissionService.getOrgPermission( + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole( role, orgId ); const isCustomRole = Boolean(customRole); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission); - if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to create a more privileged identity" }); + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.GrantPrivileges, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to create identity", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.GrantPrivileges, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); const plan = await licenseService.getPlan(orgId); @@ -108,30 +132,22 @@ export const identityServiceFactory = ({ actorId, actorAuthMethod, actorOrgId, - metadata + metadata, + isActorSuperAdmin }: TUpdateIdentityDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(id, isActorSuperAdmin); + const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id }); if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` }); - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, identityOrgMembership.orgId, actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); - - const { permission: identityRolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - id, - identityOrgMembership.orgId, - actorAuthMethod, - actorOrgId - ); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); - if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); let customRole: TOrgRoles | undefined; if (role) { @@ -141,9 +157,23 @@ export const identityServiceFactory = ({ ); const isCustomRole = Boolean(customOrgRole); - const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission); - if (!hasRequiredNewRolePermission) - throw new ForbiddenRequestError({ message: "Failed to create a more privileged identity" }); + const appliedRolePermissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.GrantPrivileges, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!appliedRolePermissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update identity", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.GrantPrivileges, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: appliedRolePermissionBoundary.missingPermissions } + }); if (isCustomRole) customRole = customOrgRole; } @@ -193,11 +223,20 @@ export const identityServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return identity; }; - const deleteIdentity = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TDeleteIdentityDTO) => { + const deleteIdentity = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + id, + isActorSuperAdmin + }: TDeleteIdentityDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(id, isActorSuperAdmin); + const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id }); if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` }); @@ -208,17 +247,8 @@ export const identityServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity); - const { permission: identityRolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - id, - identityOrgMembership.orgId, - actorAuthMethod, - actorOrgId - ); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); - if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" }); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity); const deletedIdentity = await identityDAL.deleteById(id); @@ -240,7 +270,7 @@ export const identityServiceFactory = ({ search }: TListOrgIdentitiesByOrgIdDTO) => { const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const identityMemberships = await identityOrgMembershipDAL.find({ [`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId, @@ -259,6 +289,33 @@ export const identityServiceFactory = ({ return { identityMemberships, totalCount }; }; + const searchOrgIdentities = async ({ + orgId, + actor, + actorId, + actorAuthMethod, + actorOrgId, + limit, + offset, + orderBy, + orderDirection, + searchFilter = {} + }: TSearchOrgIdentitiesByOrgIdDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + + const { totalCount, docs } = await identityOrgMembershipDAL.searchIdentities({ + orgId, + limit, + offset, + orderBy, + orderDirection, + searchFilter + }); + + return { identityMemberships: docs, totalCount }; + }; + const listProjectIdentitiesByIdentityId = async ({ identityId, actor, @@ -276,7 +333,7 @@ export const identityServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const identityMemberships = await identityProjectDAL.findByIdentityId(identityId); return identityMemberships; @@ -288,6 +345,7 @@ export const identityServiceFactory = ({ deleteIdentity, listOrgIdentities, getIdentityById, + searchOrgIdentities, listProjectIdentitiesByIdentityId }; }; diff --git a/backend/src/services/identity/identity-types.ts b/backend/src/services/identity/identity-types.ts index ceaf3ecfc..363d42a88 100644 --- a/backend/src/services/identity/identity-types.ts +++ b/backend/src/services/identity/identity-types.ts @@ -1,4 +1,5 @@ import { IPType } from "@app/lib/ip"; +import { TSearchResourceOperator } from "@app/lib/search-resource/search"; import { OrderByDirection, TOrgPermission } from "@app/lib/types"; export type TCreateIdentityDTO = { @@ -12,10 +13,12 @@ export type TUpdateIdentityDTO = { role?: string; name?: string; metadata?: { key: string; value: string }[]; + isActorSuperAdmin?: boolean; } & Omit; export type TDeleteIdentityDTO = { id: string; + isActorSuperAdmin?: boolean; } & Omit; export type TGetIdentityByIdDTO = { @@ -44,3 +47,17 @@ export enum OrgIdentityOrderBy { Name = "name" // Role = "role" } + +export type TSearchOrgIdentitiesByOrgIdDAL = { + limit?: number; + offset?: number; + orderBy?: OrgIdentityOrderBy; + orderDirection?: OrderByDirection; + orgId: string; + searchFilter?: Partial<{ + name: Omit; + role: Omit; + }>; +}; + +export type TSearchOrgIdentitiesByOrgIdDTO = TSearchOrgIdentitiesByOrgIdDAL & TOrgPermission; diff --git a/backend/src/services/integration-auth/integration-app-list.ts b/backend/src/services/integration-auth/integration-app-list.ts index 8fe1231bb..4a5fd8231 100644 --- a/backend/src/services/integration-auth/integration-app-list.ts +++ b/backend/src/services/integration-auth/integration-app-list.ts @@ -1,6 +1,7 @@ /* eslint-disable no-await-in-loop */ import { createAppAuth } from "@octokit/auth-app"; import { Octokit } from "@octokit/rest"; +import { Client as OctopusDeployClient, ProjectRepository as OctopusDeployRepository } from "@octopusdeploy/api-client"; import { TIntegrationAuths } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; @@ -131,16 +132,28 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => { /** * Return list of names of apps for Vercel integration + * This is re-used for getting custom environments for Vercel */ -const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null; accessToken: string }) => { - const apps: Array<{ name: string; appId: string }> = []; +export const getAppsVercel = async ({ + accessToken, + teamId, + includeCustomEnvironments +}: { + teamId?: string | null; + accessToken: string; + includeCustomEnvironments?: boolean; +}) => { + const apps: Array<{ name: string; appId: string; customEnvironments: Array<{ slug: string; id: string }> }> = []; const limit = "20"; let hasMorePages = true; let next: number | null = null; interface Response { - projects: { name: string; id: string }[]; + projects: { + name: string; + id: string; + }[]; pagination: { count: number; next: number | null; @@ -148,6 +161,20 @@ const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null; }; } + const getProjectCustomEnvironments = async (projectId: string) => { + const { data } = await request.get<{ environments: { id: string; slug: string }[] }>( + `${IntegrationUrls.VERCEL_API_URL}/v9/projects/${projectId}/custom-environments`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + return data.environments; + }; + while (hasMorePages) { const params: { [key: string]: string } = { limit @@ -169,12 +196,38 @@ const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null; } }); - data.projects.forEach((a) => { - apps.push({ - name: a.name, - appId: a.id + if (includeCustomEnvironments) { + const projectsWithCustomEnvironments = await Promise.all( + data.projects.map(async (a) => { + const customEnvironments = await getProjectCustomEnvironments(a.id); + + return { + ...a, + customEnvironments + }; + }) + ); + + projectsWithCustomEnvironments.forEach((a) => { + apps.push({ + name: a.name, + appId: a.id, + customEnvironments: + a.customEnvironments?.map((env) => ({ + slug: env.slug, + id: env.id + })) ?? [] + }); }); - }); + } else { + data.projects.forEach((a) => { + apps.push({ + name: a.name, + appId: a.id, + customEnvironments: [] + }); + }); + } next = data.pagination.next; @@ -870,16 +923,14 @@ const getAppsCodefresh = async ({ accessToken }: { accessToken: string }) => { /** * Return list of projects for Windmill integration */ -const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => { - const { data } = await request.get<{ id: string; name: string }[]>( - `${IntegrationUrls.WINDMILL_API_URL}/workspaces/list`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json" - } +const getAppsWindmill = async ({ accessToken, url }: { accessToken: string; url?: string | null }) => { + const apiUrl = url ? `${url}/api` : IntegrationUrls.WINDMILL_API_URL; + const { data } = await request.get<{ id: string; name: string }[]>(`${apiUrl}/workspaces/list`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" } - ); + }); // check for write access of secrets in windmill workspaces const writeAccessCheck = data.map(async (app) => { @@ -888,7 +939,7 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => { const folderPath = "f/folder/variable"; const { data: writeUser } = await request.post( - `${IntegrationUrls.WINDMILL_API_URL}/w/${app.id}/variables/create`, + `${apiUrl}/w/${app.id}/variables/create`, { path: userPath, value: "variable", @@ -904,7 +955,7 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => { ); const { data: writeFolder } = await request.post( - `${IntegrationUrls.WINDMILL_API_URL}/w/${app.id}/variables/create`, + `${apiUrl}/w/${app.id}/variables/create`, { path: folderPath, value: "variable", @@ -921,14 +972,14 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => { // is write access is allowed then delete the created secrets from workspace if (writeUser && writeFolder) { - await request.delete(`${IntegrationUrls.WINDMILL_API_URL}/w/${app.id}/variables/delete/${userPath}`, { + await request.delete(`${apiUrl}/w/${app.id}/variables/delete/${userPath}`, { headers: { Authorization: `Bearer ${accessToken}`, "Accept-Encoding": "application/json" } }); - await request.delete(`${IntegrationUrls.WINDMILL_API_URL}/w/${app.id}/variables/delete/${folderPath}`, { + await request.delete(`${apiUrl}/w/${app.id}/variables/delete/${folderPath}`, { headers: { Authorization: `Bearer ${accessToken}`, "Accept-Encoding": "application/json" @@ -1087,6 +1138,33 @@ const getAppsAzureDevOps = async ({ accessToken, orgName }: { accessToken: strin return apps; }; +const getAppsOctopusDeploy = async ({ + apiKey, + instanceURL, + spaceName = "Default" +}: { + apiKey: string; + instanceURL: string; + spaceName?: string; +}) => { + const client = await OctopusDeployClient.create({ + instanceURL, + apiKey, + userAgentApp: "Infisical Integration" + }); + + const repository = new OctopusDeployRepository(client, spaceName); + + const projects = await repository.list({ + take: 1000 + }); + + return projects.Items.map((project) => ({ + name: project.Name, + appId: project.Id + })); +}; + export const getApps = async ({ integration, integrationAuth, @@ -1236,7 +1314,8 @@ export const getApps = async ({ case Integrations.WINDMILL: return getAppsWindmill({ - accessToken + accessToken, + url }); case Integrations.DIGITAL_OCEAN_APP_PLATFORM: @@ -1260,6 +1339,13 @@ export const getApps = async ({ orgName: azureDevOpsOrgName as string }); + case Integrations.OCTOPUS_DEPLOY: + return getAppsOctopusDeploy({ + apiKey: accessToken, + instanceURL: url!, + spaceName: workspaceSlug + }); + default: throw new NotFoundError({ message: `Integration '${integration}' not found` }); } diff --git a/backend/src/services/integration-auth/integration-app-types.ts b/backend/src/services/integration-auth/integration-app-types.ts new file mode 100644 index 000000000..1ddd2e4d2 --- /dev/null +++ b/backend/src/services/integration-auth/integration-app-types.ts @@ -0,0 +1,5 @@ +export type TCircleCIContext = { + id: string; + name: string; + created_at: string; +}; diff --git a/backend/src/services/integration-auth/integration-auth-service.ts b/backend/src/services/integration-auth/integration-auth-service.ts index d7d7c45ab..eb17c05bb 100644 --- a/backend/src/services/integration-auth/integration-auth-service.ts +++ b/backend/src/services/integration-auth/integration-auth-service.ts @@ -1,27 +1,41 @@ import { ForbiddenError } from "@casl/ability"; import { createAppAuth } from "@octokit/auth-app"; import { Octokit } from "@octokit/rest"; +import { Client as OctopusClient, SpaceRepository as OctopusSpaceRepository } from "@octopusdeploy/api-client"; import AWS from "aws-sdk"; -import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas"; +import { + ActionProjectType, + SecretEncryptionAlgo, + SecretKeyEncoding, + TIntegrationAuths, + TIntegrationAuthsInsert +} from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; import { decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; +import { groupBy } from "@app/lib/fn"; +import { logger } from "@app/lib/logger"; import { TGenericPermission, TProjectPermission } from "@app/lib/types"; import { TIntegrationDALFactory } from "../integration/integration-dal"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; -import { getApps } from "./integration-app-list"; +import { getApps, getAppsVercel } from "./integration-app-list"; +import { TCircleCIContext } from "./integration-app-types"; import { TIntegrationAuthDALFactory } from "./integration-auth-dal"; import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema"; import { + GetVercelCustomEnvironmentsDTO, + OctopusDeployScope, + TBitbucketEnvironment, TBitbucketWorkspace, TChecklyGroups, + TCircleCIOrganization, TDeleteIntegrationAuthByIdDTO, TDeleteIntegrationAuthsDTO, TDuplicateGithubIntegrationAuthDTO, @@ -30,12 +44,16 @@ import { THerokuPipelineCoupling, TIntegrationAuthAppsDTO, TIntegrationAuthAwsKmsKeyDTO, + TIntegrationAuthBitbucketEnvironmentsDTO, TIntegrationAuthBitbucketWorkspaceDTO, TIntegrationAuthChecklyGroupsDTO, + TIntegrationAuthCircleCIOrganizationDTO, TIntegrationAuthGithubEnvsDTO, TIntegrationAuthGithubOrgsDTO, TIntegrationAuthHerokuPipelinesDTO, TIntegrationAuthNorthflankSecretGroupDTO, + TIntegrationAuthOctopusDeployProjectScopeValuesDTO, + TIntegrationAuthOctopusDeploySpacesDTO, TIntegrationAuthQoveryEnvironmentsDTO, TIntegrationAuthQoveryOrgsDTO, TIntegrationAuthQoveryProjectDTO, @@ -46,8 +64,10 @@ import { TIntegrationAuthVercelBranchesDTO, TNorthflankSecretGroup, TOauthExchangeDTO, + TOctopusDeployVariableSet, TSaveIntegrationAccessTokenDTO, TTeamCityBuildConfig, + TUpdateIntegrationAuthDTO, TVercelBranches } from "./integration-auth-types"; import { getIntegrationOptions, Integrations, IntegrationUrls } from "./integration-list"; @@ -78,13 +98,14 @@ export const integrationAuthServiceFactory = ({ actorAuthMethod, projectId }: TProjectPermission) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const authorizations = await integrationAuthDAL.find({ projectId }); return authorizations; @@ -93,32 +114,41 @@ export const integrationAuthServiceFactory = ({ const listOrgIntegrationAuth = async ({ actorId, actor, actorOrgId, actorAuthMethod }: TGenericPermission) => { const authorizations = await integrationAuthDAL.getByOrg(actorOrgId as string); - return Promise.all( - authorizations.filter(async (auth) => { - const { permission } = await permissionService.getProjectPermission( - actor, - actorId, - auth.projectId, - actorAuthMethod, - actorOrgId - ); + const filteredAuthorizations = await Promise.all( + authorizations.map(async (auth) => { + try { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: auth.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); - return permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + return permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations) ? auth : null; + } catch (error) { + // user does not belong to the project that the integration auth belongs to + return null; + } }) ); + + return filteredAuthorizations.filter((auth): auth is NonNullable => auth !== null); }; const getIntegrationAuth = async ({ actor, id, actorId, actorAuthMethod, actorOrgId }: TGetIntegrationAuthDTO) => { const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integrationAuth.projectId, + projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); return integrationAuth; }; @@ -137,13 +167,14 @@ export const integrationAuthServiceFactory = ({ if (!Object.values(Integrations).includes(integration as Integrations)) throw new BadRequestError({ message: "Invalid integration" }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); const tokenExchange = await exchangeCode({ integration, code, url, installationId }); @@ -246,13 +277,14 @@ export const integrationAuthServiceFactory = ({ if (!Object.values(Integrations).includes(integration as Integrations)) throw new BadRequestError({ message: "Invalid integration" }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); const updateDoc: TIntegrationAuthsInsert = { @@ -361,6 +393,149 @@ export const integrationAuthServiceFactory = ({ return integrationAuthDAL.create(updateDoc); }; + const updateIntegrationAuth = async ({ + integrationAuthId, + refreshToken, + actorId, + integration: newIntegration, + url, + actor, + actorOrgId, + actorAuthMethod, + accessId, + namespace, + accessToken, + awsAssumeIamRoleArn + }: TUpdateIntegrationAuthDTO) => { + const integrationAuth = await integrationAuthDAL.findById(integrationAuthId); + if (!integrationAuth) { + throw new NotFoundError({ message: `Integration auth with id ${integrationAuthId} not found.` }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: integrationAuth.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations); + + const { projectId } = integrationAuth; + const integration = newIntegration || integrationAuth.integration; + + const updateDoc: TIntegrationAuthsInsert = { + projectId, + integration, + namespace, + url, + algorithm: SecretEncryptionAlgo.AES_256_GCM, + keyEncoding: SecretKeyEncoding.UTF8, + ...(integration === Integrations.GCP_SECRET_MANAGER + ? { + metadata: { + authMethod: "serviceAccount" + } + } + : {}) + }; + + const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(projectId); + if (shouldUseSecretV2Bridge) { + const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + if (refreshToken) { + const tokenDetails = await exchangeRefresh( + integration, + refreshToken, + url, + updateDoc.metadata as Record + ); + const refreshEncToken = secretManagerEncryptor({ + plainText: Buffer.from(tokenDetails.refreshToken) + }).cipherTextBlob; + updateDoc.encryptedRefresh = refreshEncToken; + + const accessEncToken = secretManagerEncryptor({ + plainText: Buffer.from(tokenDetails.accessToken) + }).cipherTextBlob; + updateDoc.encryptedAccess = accessEncToken; + updateDoc.accessExpiresAt = tokenDetails.accessExpiresAt; + } + + if (!refreshToken && (accessId || accessToken || awsAssumeIamRoleArn)) { + if (accessToken) { + const accessEncToken = secretManagerEncryptor({ + plainText: Buffer.from(accessToken) + }).cipherTextBlob; + updateDoc.encryptedAccess = accessEncToken; + updateDoc.encryptedAwsAssumeIamRoleArn = null; + } + if (accessId) { + const accessEncToken = secretManagerEncryptor({ + plainText: Buffer.from(accessId) + }).cipherTextBlob; + updateDoc.encryptedAccessId = accessEncToken; + updateDoc.encryptedAwsAssumeIamRoleArn = null; + } + if (awsAssumeIamRoleArn) { + const awsAssumeIamRoleArnEncrypted = secretManagerEncryptor({ + plainText: Buffer.from(awsAssumeIamRoleArn) + }).cipherTextBlob; + updateDoc.encryptedAwsAssumeIamRoleArn = awsAssumeIamRoleArnEncrypted; + updateDoc.encryptedAccess = null; + updateDoc.encryptedAccessId = null; + } + } + } else { + if (!botKey) throw new NotFoundError({ message: `Project bot key for project with ID '${projectId}' not found` }); + if (refreshToken) { + const tokenDetails = await exchangeRefresh( + integration, + refreshToken, + url, + updateDoc.metadata as Record + ); + const refreshEncToken = encryptSymmetric128BitHexKeyUTF8(tokenDetails.refreshToken, botKey); + updateDoc.refreshIV = refreshEncToken.iv; + updateDoc.refreshTag = refreshEncToken.tag; + updateDoc.refreshCiphertext = refreshEncToken.ciphertext; + const accessEncToken = encryptSymmetric128BitHexKeyUTF8(tokenDetails.accessToken, botKey); + updateDoc.accessIV = accessEncToken.iv; + updateDoc.accessTag = accessEncToken.tag; + updateDoc.accessCiphertext = accessEncToken.ciphertext; + + updateDoc.accessExpiresAt = tokenDetails.accessExpiresAt; + } + + if (!refreshToken && (accessId || accessToken || awsAssumeIamRoleArn)) { + if (accessToken) { + const accessEncToken = encryptSymmetric128BitHexKeyUTF8(accessToken, botKey); + updateDoc.accessIV = accessEncToken.iv; + updateDoc.accessTag = accessEncToken.tag; + updateDoc.accessCiphertext = accessEncToken.ciphertext; + } + if (accessId) { + const accessEncToken = encryptSymmetric128BitHexKeyUTF8(accessId, botKey); + updateDoc.accessIdIV = accessEncToken.iv; + updateDoc.accessIdTag = accessEncToken.tag; + updateDoc.accessIdCiphertext = accessEncToken.ciphertext; + } + if (awsAssumeIamRoleArn) { + const awsAssumeIamRoleArnEnc = encryptSymmetric128BitHexKeyUTF8(awsAssumeIamRoleArn, botKey); + updateDoc.awsAssumeIamRoleArnCipherText = awsAssumeIamRoleArnEnc.ciphertext; + updateDoc.awsAssumeIamRoleArnIV = awsAssumeIamRoleArnEnc.iv; + updateDoc.awsAssumeIamRoleArnTag = awsAssumeIamRoleArnEnc.tag; + } + } + } + + return integrationAuthDAL.updateById(integrationAuthId, updateDoc); + }; + // helper function const getIntegrationAccessToken = async ( integrationAuth: TIntegrationAuths, @@ -499,13 +674,14 @@ export const integrationAuthServiceFactory = ({ const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integrationAuth.projectId, + projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(integrationAuth.projectId); @@ -533,13 +709,14 @@ export const integrationAuthServiceFactory = ({ const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integrationAuth.projectId, + projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); @@ -563,13 +740,14 @@ export const integrationAuthServiceFactory = ({ const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integrationAuth.projectId, + projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); @@ -604,13 +782,14 @@ export const integrationAuthServiceFactory = ({ const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integrationAuth.projectId, + projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); @@ -632,13 +811,14 @@ export const integrationAuthServiceFactory = ({ const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integrationAuth.projectId, + projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); @@ -706,13 +886,14 @@ export const integrationAuthServiceFactory = ({ const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integrationAuth.projectId, + projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); @@ -753,13 +934,14 @@ export const integrationAuthServiceFactory = ({ const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integrationAuth.projectId, + projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); @@ -787,13 +969,14 @@ export const integrationAuthServiceFactory = ({ const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integrationAuth.projectId, + projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); const { accessId, accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); @@ -845,13 +1028,14 @@ export const integrationAuthServiceFactory = ({ const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integrationAuth.projectId, + projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); @@ -881,13 +1065,14 @@ export const integrationAuthServiceFactory = ({ const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integrationAuth.projectId, + projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); @@ -922,13 +1107,14 @@ export const integrationAuthServiceFactory = ({ const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integrationAuth.projectId, + projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); @@ -962,13 +1148,14 @@ export const integrationAuthServiceFactory = ({ const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integrationAuth.projectId, + projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); @@ -1002,13 +1189,14 @@ export const integrationAuthServiceFactory = ({ const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID ${id} not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integrationAuth.projectId, + projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); @@ -1041,13 +1229,14 @@ export const integrationAuthServiceFactory = ({ const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integrationAuth.projectId, + projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); @@ -1081,13 +1270,14 @@ export const integrationAuthServiceFactory = ({ const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integrationAuth.projectId, + projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); @@ -1149,13 +1339,14 @@ export const integrationAuthServiceFactory = ({ const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integrationAuth.projectId, + projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); @@ -1223,13 +1414,14 @@ export const integrationAuthServiceFactory = ({ const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integrationAuth.projectId, + projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); @@ -1261,6 +1453,56 @@ export const integrationAuthServiceFactory = ({ return workspaces; }; + const getBitbucketEnvironments = async ({ + workspaceSlug, + repoSlug, + actorId, + actor, + actorOrgId, + actorAuthMethod, + id + }: TIntegrationAuthBitbucketEnvironmentsDTO) => { + const integrationAuth = await integrationAuthDAL.findById(id); + if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: integrationAuth.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); + const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); + const environments: TBitbucketEnvironment[] = []; + let hasNextPage = true; + + let environmentsUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${workspaceSlug}/${repoSlug}/environments`; + + while (hasNextPage) { + // eslint-disable-next-line + const { data }: { data: { values: TBitbucketEnvironment[]; next: string } } = await request.get(environmentsUrl, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + }); + + if (data?.values.length > 0) { + environments.push(...data.values); + } + + if (data.next) { + environmentsUrl = data.next; + } else { + hasNextPage = false; + } + } + return environments; + }; + const getNorthFlankSecretGroups = async ({ id, actor, @@ -1272,13 +1514,14 @@ export const integrationAuthServiceFactory = ({ const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integrationAuth.projectId, + projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); @@ -1340,13 +1583,14 @@ export const integrationAuthServiceFactory = ({ const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integrationAuth.projectId, + projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); @@ -1371,6 +1615,121 @@ export const integrationAuthServiceFactory = ({ return []; }; + const getCircleCIOrganizations = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + id + }: TIntegrationAuthCircleCIOrganizationDTO) => { + const integrationAuth = await integrationAuthDAL.findById(id); + if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: integrationAuth.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); + const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); + + const { data: organizations }: { data: TCircleCIOrganization[] } = await request.get( + `${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`, + { + headers: { + "Circle-Token": `${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + let projects: { + orgName: string; + projectName: string; + projectId?: string; + }[] = []; + + try { + const projectRes = ( + await request.get<{ reponame: string; username: string; vcs_url: string }[]>( + `${IntegrationUrls.CIRCLECI_API_URL}/v1.1/projects`, + { + headers: { + "Circle-Token": accessToken, + "Accept-Encoding": "application/json" + } + } + ) + ).data; + + projects = projectRes.map((a) => ({ + orgName: a.username, // username maps to unique organization name in CircleCI + projectName: a.reponame, // reponame maps to project name within an organization in CircleCI + projectId: a.vcs_url.split("/").pop() // vcs_url maps to the project id in CircleCI + })); + } catch (error) { + logger.error(error); + } + + const projectsByOrg = groupBy( + projects.map((p) => ({ + orgName: p.orgName, + name: p.projectName, + id: p.projectId as string + })), + (p) => p.orgName + ); + + const getOrgContexts = async (orgSlug: string) => { + type NextPageToken = string | null | undefined; + + try { + const contexts: TCircleCIContext[] = []; + let nextPageToken: NextPageToken; + + while (nextPageToken !== null) { + // eslint-disable-next-line no-await-in-loop + const { data } = await request.get<{ + items: TCircleCIContext[]; + next_page_token: NextPageToken; + }>(`${IntegrationUrls.CIRCLECI_API_URL}/v2/context`, { + headers: { + "Circle-Token": accessToken, + "Accept-Encoding": "application/json" + }, + params: new URLSearchParams({ + "owner-slug": orgSlug, + ...(nextPageToken ? { "page-token": nextPageToken } : {}) + }) + }); + + contexts.push(...data.items); + nextPageToken = data.next_page_token; + } + + return contexts?.map((context) => ({ + name: context.name, + id: context.id + })); + } catch (error) { + logger.error(error); + } + }; + + return Promise.all( + organizations.map(async (org) => ({ + name: org.name, + slug: org.slug, + projects: projectsByOrg[org.name] ?? [], + contexts: (await getOrgContexts(org.slug)) ?? [] + })) + ); + }; + const deleteIntegrationAuths = async ({ projectId, integration, @@ -1379,13 +1738,14 @@ export const integrationAuthServiceFactory = ({ actorAuthMethod, actorOrgId }: TDeleteIntegrationAuthsDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations); const integrations = await integrationAuthDAL.delete({ integration, projectId }); @@ -1402,13 +1762,14 @@ export const integrationAuthServiceFactory = ({ const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integrationAuth.projectId, + projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations); const delIntegrationAuth = await integrationAuthDAL.transaction(async (tx) => { @@ -1435,26 +1796,28 @@ export const integrationAuthServiceFactory = ({ throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); } - const { permission: sourcePermission } = await permissionService.getProjectPermission( + const { permission: sourcePermission } = await permissionService.getProjectPermission({ actor, actorId, - integrationAuth.projectId, + projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(sourcePermission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.Integrations ); - const { permission: targetPermission } = await permissionService.getProjectPermission( + const { permission: targetPermission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(targetPermission).throwUnlessCan( ProjectPermissionActions.Create, @@ -1470,6 +1833,126 @@ export const integrationAuthServiceFactory = ({ return integrationAuthDAL.create(newIntegrationAuth); }; + const getVercelCustomEnvironments = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + teamId, + id + }: GetVercelCustomEnvironmentsDTO) => { + const integrationAuth = await integrationAuthDAL.findById(id); + if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: integrationAuth.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + + const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(integrationAuth.projectId); + const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); + + const vercelApps = await getAppsVercel({ + includeCustomEnvironments: true, + accessToken, + teamId + }); + + return vercelApps.map((app) => ({ + customEnvironments: app.customEnvironments, + appId: app.appId + })); + }; + + const getOctopusDeploySpaces = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + id + }: TIntegrationAuthOctopusDeploySpacesDTO) => { + const integrationAuth = await integrationAuthDAL.findById(id); + if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: integrationAuth.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); + const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); + + const client = await OctopusClient.create({ + apiKey: accessToken, + instanceURL: integrationAuth.url!, + userAgentApp: "Infisical Integration" + }); + + const spaceRepository = new OctopusSpaceRepository(client); + + const spaces = await spaceRepository.list({ + partialName: "", // throws error if no string is present... + take: 1000 + }); + + return spaces.Items; + }; + + const getOctopusDeployScopeValues = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + id, + scope, + spaceId, + resourceId + }: TIntegrationAuthOctopusDeployProjectScopeValuesDTO) => { + const integrationAuth = await integrationAuthDAL.findById(id); + if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: integrationAuth.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); + const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); + + let url: string; + switch (scope) { + case OctopusDeployScope.Project: + url = `${integrationAuth.url}/api/${spaceId}/projects/${resourceId}/variables`; + break; + // future support tenant, variable set etc. + default: + throw new InternalServerError({ message: `Unhandled Octopus Deploy scope` }); + } + + // SDK doesn't support variable set... + const { data: variableSet } = await request.get(url, { + headers: { + "X-NuGet-ApiKey": accessToken, + Accept: "application/json" + } + }); + + return variableSet.ScopeValues; + }; + return { listIntegrationAuthByProjectId, listOrgIntegrationAuth, @@ -1477,6 +1960,7 @@ export const integrationAuthServiceFactory = ({ getIntegrationAuth, oauthExchange, saveIntegrationToken, + updateIntegrationAuth, deleteIntegrationAuthById, deleteIntegrationAuths, getIntegrationAuthTeams, @@ -1499,7 +1983,12 @@ export const integrationAuthServiceFactory = ({ getNorthFlankSecretGroups, getTeamcityBuildConfigs, getBitbucketWorkspaces, + getBitbucketEnvironments, + getCircleCIOrganizations, getIntegrationAccessToken, - duplicateIntegrationAuth + duplicateIntegrationAuth, + getOctopusDeploySpaces, + getOctopusDeployScopeValues, + getVercelCustomEnvironments }; }; diff --git a/backend/src/services/integration-auth/integration-auth-types.ts b/backend/src/services/integration-auth/integration-auth-types.ts index eb8b8044d..8efe6b851 100644 --- a/backend/src/services/integration-auth/integration-auth-types.ts +++ b/backend/src/services/integration-auth/integration-auth-types.ts @@ -22,6 +22,11 @@ export type TSaveIntegrationAccessTokenDTO = { awsAssumeIamRoleArn?: string; } & TProjectPermission; +export type TUpdateIntegrationAuthDTO = Omit & { + integrationAuthId: string; + integration?: string; +}; + export type TDeleteIntegrationAuthsDTO = TProjectPermission & { integration: string; projectId: string; @@ -99,6 +104,12 @@ export type TIntegrationAuthBitbucketWorkspaceDTO = { id: string; } & Omit; +export type TIntegrationAuthBitbucketEnvironmentsDTO = { + workspaceSlug: string; + repoSlug: string; + id: string; +} & Omit; + export type TIntegrationAuthNorthflankSecretGroupDTO = { id: string; appId: string; @@ -117,6 +128,10 @@ export type TGetIntegrationAuthTeamCityBuildConfigDTO = { appId: string; } & Omit; +export type TIntegrationAuthCircleCIOrganizationDTO = { + id: string; +} & Omit; + export type TVercelBranches = { ref: string; lastCommit: string; @@ -148,6 +163,13 @@ export type TBitbucketWorkspace = { updated_on: string; }; +export type TBitbucketEnvironment = { + type: string; + uuid: string; + name: string; + slug: string; +}; + export type TNorthflankSecretGroup = { id: string; name: string; @@ -171,6 +193,14 @@ export type TTeamCityBuildConfig = { webUrl: string; }; +export type TCircleCIOrganization = { + id: string; + vcsType: string; + name: string; + avatarUrl: string; + slug: string; +}; + export type TIntegrationsWithEnvironment = TIntegrations & { environment?: | { @@ -180,3 +210,82 @@ export type TIntegrationsWithEnvironment = TIntegrations & { | null | undefined; }; + +export type TIntegrationAuthOctopusDeploySpacesDTO = { + id: string; +} & Omit; + +export type TIntegrationAuthOctopusDeployProjectScopeValuesDTO = { + id: string; + spaceId: string; + resourceId: string; + scope: OctopusDeployScope; +} & Omit; + +export enum OctopusDeployScope { + Project = "project" + // add tenant, variable set, etc. +} + +export enum CircleCiScope { + Project = "project", + Context = "context" +} + +export type TOctopusDeployVariableSet = { + Id: string; + OwnerId: string; + Version: number; + Variables: { + Id: string; + Name: string; + Value: string; + Description: string; + Scope: { + Environment?: string[]; + Machine?: string[]; + Role?: string[]; + TargetRole?: string[]; + Action?: string[]; + User?: string[]; + Trigger?: string[]; + ParentDeployment?: string[]; + Private?: string[]; + Channel?: string[]; + TenantTag?: string[]; + Tenant?: string[]; + ProcessOwner?: string[]; + }; + IsEditable: boolean; + Prompt: { + Description: string; + DisplaySettings: Record; + Label: string; + Required: boolean; + } | null; + Type: "String"; + IsSensitive: boolean; + }[]; + ScopeValues: { + Environments: { Id: string; Name: string }[]; + Machines: { Id: string; Name: string }[]; + Actions: { Id: string; Name: string }[]; + Roles: { Id: string; Name: string }[]; + Channels: { Id: string; Name: string }[]; + TenantTags: { Id: string; Name: string }[]; + Processes: { + ProcessType: string; + Id: string; + Name: string; + }[]; + }; + SpaceId: string; + Links: { + Self: string; + }; +}; + +export type GetVercelCustomEnvironmentsDTO = { + teamId: string; + id: string; +} & Omit; diff --git a/backend/src/services/integration-auth/integration-delete-secret.ts b/backend/src/services/integration-auth/integration-delete-secret.ts index fdefd0e62..f77becb02 100644 --- a/backend/src/services/integration-auth/integration-delete-secret.ts +++ b/backend/src/services/integration-auth/integration-delete-secret.ts @@ -50,7 +50,7 @@ const getIntegrationSecretsV2 = async ( } // process secrets in current folder - const secrets = await secretV2BridgeDAL.findByFolderId(dto.folderId); + const secrets = await secretV2BridgeDAL.findByFolderId({ folderId: dto.folderId }); secrets.forEach((secret) => { const secretKey = secret.key; @@ -68,7 +68,8 @@ const getIntegrationSecretsV2 = async ( secretDAL: secretV2BridgeDAL, secretImportDAL, secretImports, - hasSecretAccess: () => true + hasSecretAccess: () => true, + viewSecretValue: true }); for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { diff --git a/backend/src/services/integration-auth/integration-list.ts b/backend/src/services/integration-auth/integration-list.ts index b31f70241..e8ef72f7a 100644 --- a/backend/src/services/integration-auth/integration-list.ts +++ b/backend/src/services/integration-auth/integration-list.ts @@ -34,7 +34,8 @@ export enum Integrations { HASURA_CLOUD = "hasura-cloud", RUNDECK = "rundeck", AZURE_DEVOPS = "azure-devops", - AZURE_APP_CONFIGURATION = "azure-app-configuration" + AZURE_APP_CONFIGURATION = "azure-app-configuration", + OCTOPUS_DEPLOY = "octopus-deploy" } export enum IntegrationType { @@ -62,6 +63,7 @@ export enum IntegrationUrls { GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token", GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token", BITBUCKET_TOKEN_URL = "https://bitbucket.org/site/oauth2/access_token", + CAMUNDA_TOKEN_URL = "https://login.cloud.camunda.io/oauth/token", // integration apps endpoints GCP_API_URL = "https://cloudresourcemanager.googleapis.com", @@ -75,7 +77,6 @@ export enum IntegrationUrls { RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2", FLYIO_API_URL = "https://api.fly.io/graphql", CIRCLECI_API_URL = "https://circleci.com/api", - DATABRICKS_API_URL = "https:/xxxx.com/api", TRAVISCI_API_URL = "https://api.travis-ci.com", SUPABASE_API_URL = "https://api.supabase.com", LARAVELFORGE_API_URL = "https://forge.laravel.com", @@ -93,6 +94,8 @@ export enum IntegrationUrls { NORTHFLANK_API_URL = "https://api.northflank.com", HASURA_CLOUD_API_URL = "https://data.pro.hasura.io/v1/graphql", AZURE_DEVOPS_API_URL = "https://dev.azure.com", + HUMANITEC_API_URL = "https://api.humanitec.io", + CAMUNDA_API_URL = "https://api.cloud.camunda.io", GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com", GCP_SECRET_MANAGER_URL = `https://${GCP_SECRET_MANAGER_SERVICE_NAME}`, @@ -192,6 +195,7 @@ export const getIntegrationOptions = async () => { { name: "AWS Secrets Manager", slug: "aws-secret-manager", + syncSlug: "aws-secrets-manager", image: "Amazon Web Services.png", isAvailable: true, type: "custom", @@ -217,9 +221,9 @@ export const getIntegrationOptions = async () => { docsLink: "" }, { - name: "Circle CI", + name: "CircleCI", slug: "circleci", - image: "Circle CI.png", + image: "CircleCI.png", isAvailable: true, type: "pat", clientId: "", @@ -334,7 +338,7 @@ export const getIntegrationOptions = async () => { docsLink: "" }, { - name: "BitBucket", + name: "Bitbucket", slug: "bitbucket", image: "BitBucket.png", isAvailable: true, @@ -413,8 +417,22 @@ export const getIntegrationOptions = async () => { type: "pat", clientId: "", docsLink: "" + }, + { + name: "Octopus Deploy", + slug: "octopus-deploy", + image: "Octopus Deploy.png", + isAvailable: true, + type: "sat", + clientId: "", + docsLink: "" } ]; return INTEGRATION_OPTIONS; }; + +export enum IntegrationMetadataSyncMode { + CUSTOM = "custom", + SECRET_METADATA = "secret-metadata" +} diff --git a/backend/src/services/integration-auth/integration-sync-secret-fns.ts b/backend/src/services/integration-auth/integration-sync-secret-fns.ts new file mode 100644 index 000000000..df8b990af --- /dev/null +++ b/backend/src/services/integration-auth/integration-sync-secret-fns.ts @@ -0,0 +1,35 @@ +export const isAzureKeyVaultReference = (uri: string) => { + const tryJsonDecode = () => { + try { + return (JSON.parse(uri) as { uri: string }).uri || uri; + } catch { + return uri; + } + }; + + const cleanUri = tryJsonDecode(); + + if (!cleanUri.startsWith("https://")) { + return false; + } + + if (!cleanUri.includes(".vault.azure.net/secrets/")) { + return false; + } + + // 3. Check for non-empty string between https:// and .vault.azure.net/secrets/ + const parts = cleanUri.split(".vault.azure.net/secrets/"); + const vaultName = parts[0].replace("https://", ""); + if (!vaultName) { + return false; + } + + // 4. Check for non-empty secret name + const secretParts = parts[1].split("/"); + const secretName = secretParts[0]; + if (!secretName) { + return false; + } + + return true; +}; diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 18c28afac..989a5a88c 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -27,25 +27,34 @@ import { randomUUID } from "crypto"; import https from "https"; import sodium from "libsodium-wrappers"; import isEqual from "lodash.isequal"; +import RE2 from "re2"; import { z } from "zod"; import { SecretType, TIntegrationAuths, TIntegrations } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; -import { BadRequestError } from "@app/lib/errors"; +import { BadRequestError, InternalServerError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/secret/secret-types"; import { TIntegrationDALFactory } from "../integration/integration-dal"; import { IntegrationMetadataSchema } from "../integration/integration-schema"; +import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { IntegrationAuthMetadataSchema } from "./integration-auth-schema"; -import { TIntegrationsWithEnvironment } from "./integration-auth-types"; +import { + CircleCiScope, + OctopusDeployScope, + TIntegrationsWithEnvironment, + TOctopusDeployVariableSet +} from "./integration-auth-types"; import { IntegrationInitialSyncBehavior, IntegrationMappingBehavior, + IntegrationMetadataSyncMode, Integrations, IntegrationUrls } from "./integration-list"; +import { isAzureKeyVaultReference } from "./integration-sync-secret-fns"; const getSecretKeyValuePair = (secrets: Record) => Object.keys(secrets).reduce>((prev, key) => { @@ -299,10 +308,16 @@ const syncSecretsAzureAppConfig = async ({ value: string; } - const getCompleteAzureAppConfigValues = async (url: string) => { + if (!integration.app || !integration.app.endsWith(".azconfig.io")) + throw new BadRequestError({ + message: "Invalid Azure App Configuration URL provided." + }); + + const getCompleteAzureAppConfigValues = async (baseURL: string, url: string) => { let result: AzureAppConfigKeyValue[] = []; while (url) { const res = await request.get(url, { + baseURL, headers: { Authorization: `Bearer ${accessToken}` }, @@ -313,17 +328,20 @@ const syncSecretsAzureAppConfig = async ({ }); result = result.concat(res.data.items); - url = res.data.nextLink; + url = res.data?.["@nextLink"]; } return result; }; const metadata = IntegrationMetadataSchema.parse(integration.metadata); + + const azureAppConfigValuesUrl = `/kv?api-version=2023-11-01&key=${metadata.secretPrefix}*${ + metadata.azureLabel ? `&label=${metadata.azureLabel}` : "&label=%00" + }`; + const azureAppConfigSecrets = ( - await getCompleteAzureAppConfigValues( - `${integration.app}/kv?api-version=2023-11-01&key=${metadata.secretPrefix || ""}*` - ) + await getCompleteAzureAppConfigValues(integration.app, azureAppConfigValuesUrl) ).reduce( (accum, entry) => { accum[entry.key] = entry.value; @@ -405,14 +423,24 @@ const syncSecretsAzureAppConfig = async ({ } // create or update secrets on Azure App Config + for await (const key of Object.keys(secrets)) { if (!(key in azureAppConfigSecrets) || secrets[key]?.value !== azureAppConfigSecrets[key]) { await request.put( `${integration.app}/kv/${key}?api-version=2023-11-01`, { - value: secrets[key]?.value + value: secrets[key]?.value, + ...(isAzureKeyVaultReference(secrets[key]?.value || "") && { + content_type: "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8" + }) }, { + ...(metadata.azureLabel && { + params: { + label: metadata.azureLabel + } + }), + headers: { Authorization: `Bearer ${accessToken}` }, @@ -432,6 +460,11 @@ const syncSecretsAzureAppConfig = async ({ headers: { Authorization: `Bearer ${accessToken}` }, + ...(metadata.azureLabel && { + params: { + label: metadata.azureLabel + } + }), // we force IPV4 because docker setup fails with ipv6 httpsAgent: new https.Agent({ family: 4 @@ -473,7 +506,7 @@ const syncSecretsAzureKeyVault = async ({ id: string; // secret URI value: string; attributes: { - enabled: true; + enabled: boolean; created: number; updated: number; recoveryLevel: string; @@ -509,10 +542,19 @@ const syncSecretsAzureKeyVault = async ({ const getAzureKeyVaultSecrets = await paginateAzureKeyVaultSecrets(`${integration.app}/secrets?api-version=7.3`); + const enabledAzureKeyVaultSecrets = getAzureKeyVaultSecrets.filter((secret) => secret.attributes.enabled); + + // disabled keys to skip sending updates to + const disabledAzureKeyVaultSecretKeys = getAzureKeyVaultSecrets + .filter(({ attributes }) => !attributes.enabled) + .map((getAzureKeyVaultSecret) => { + return getAzureKeyVaultSecret.id.substring(getAzureKeyVaultSecret.id.lastIndexOf("/") + 1); + }); + let lastSlashIndex: number; const res = ( await Promise.all( - getAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => { + enabledAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => { if (!lastSlashIndex) { lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf("/"); } @@ -543,7 +585,7 @@ const syncSecretsAzureKeyVault = async ({ }[] = []; Object.keys(secrets).forEach((key) => { - const hyphenatedKey = key.replace(/_/g, "-"); + const hyphenatedKey = key.replaceAll("_", "-"); if (!(hyphenatedKey in res)) { // case: secret has been created setSecrets.push({ @@ -562,7 +604,7 @@ const syncSecretsAzureKeyVault = async ({ const deleteSecrets: AzureKeyVaultSecret[] = []; Object.keys(res).forEach((key) => { - const underscoredKey = key.replace(/-/g, "_"); + const underscoredKey = key.replaceAll("-", "_"); if (!(underscoredKey in secrets)) { deleteSecrets.push(res[key]); } @@ -576,7 +618,7 @@ const syncSecretsAzureKeyVault = async ({ if (!integration.lastUsed) { Object.keys(res).forEach((key) => { // first time using integration - const underscoredKey = key.replace(/-/g, "_"); + const underscoredKey = key.replaceAll("-", "_"); // -> apply initial sync behavior switch (metadata.initialSyncBehavior) { @@ -658,6 +700,7 @@ const syncSecretsAzureKeyVault = async ({ }) => { let isSecretSet = false; let maxTries = 6; + if (disabledAzureKeyVaultSecretKeys.includes(key)) return; while (!isSecretSet && maxTries > 0) { // try to set secret @@ -890,15 +933,22 @@ const syncSecretsAWSParameterStore = async ({ logger.info( `getIntegrationSecrets: create secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}]` ); - await ssm - .putParameter({ - Name: `${integration.path}${key}`, - Type: "SecureString", - Value: secrets[key].value, - ...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId }), - Overwrite: true - }) - .promise(); + + try { + await ssm + .putParameter({ + Name: `${integration.path}${key}`, + Type: "SecureString", + Value: secrets[key].value, + ...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId }), + Overwrite: true + }) + .promise(); + } catch (error) { + (error as { secretKey: string }).secretKey = key; + throw error; + } + if (metadata.secretAWSTag?.length) { try { await ssm @@ -945,15 +995,20 @@ const syncSecretsAWSParameterStore = async ({ // we ensure that the KMS key configured in the integration is applied for ALL parameters on AWS if (secrets[key].value && (shouldUpdateKms || awsParameterStoreSecretsObj[key].Value !== secrets[key].value)) { - await ssm - .putParameter({ - Name: `${integration.path}${key}`, - Type: "SecureString", - Value: secrets[key].value, - Overwrite: true, - ...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId }) - }) - .promise(); + try { + await ssm + .putParameter({ + Name: `${integration.path}${key}`, + Type: "SecureString", + Value: secrets[key].value, + Overwrite: true, + ...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId }) + }) + .promise(); + } catch (error) { + (error as { secretKey: string }).secretKey = key; + throw error; + } } if (awsParameterStoreSecretsObj[key].Name) { @@ -1042,14 +1097,14 @@ const syncSecretsAWSSecretManager = async ({ projectId }: { integration: TIntegrations; - secrets: Record; + secrets: Record; accessId: string | null; accessToken: string; awsAssumeRoleArn: string | null; projectId?: string; }) => { const appCfg = getConfig(); - const metadata = z.record(z.any()).parse(integration.metadata || {}); + const metadata = IntegrationMetadataSchema.parse(integration.metadata || {}); if (!accessId && !awsAssumeRoleArn) { throw new Error("AWS access ID/AWS Assume Role is required"); @@ -1097,8 +1152,25 @@ const syncSecretsAWSSecretManager = async ({ const processAwsSecret = async ( secretId: string, - secretValue: Record | string + secretValue: Record | string, + secretMetadata?: ResourceMetadataDTO ) => { + const secretAWSTag = metadata.secretAWSTag as { key: string; value: string }[] | undefined; + const shouldTag = + (secretAWSTag && secretAWSTag.length) || + (metadata.metadataSyncMode === IntegrationMetadataSyncMode.SECRET_METADATA && + metadata.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE); + const tagArray = + (metadata.metadataSyncMode === IntegrationMetadataSyncMode.SECRET_METADATA ? secretMetadata : secretAWSTag) ?? []; + + const integrationTagObj = tagArray.reduce( + (acc, item) => { + acc[item.key] = item.value; + return acc; + }, + {} as Record + ); + try { const awsSecretManagerSecret = await secretsManager.send( new GetSecretValueCommand({ @@ -1127,15 +1199,14 @@ const syncSecretsAWSSecretManager = async ({ } else { await secretsManager.send( new DeleteSecretCommand({ - SecretId: secretId + SecretId: secretId, + ForceDeleteWithoutRecovery: true }) ); } } - const secretAWSTag = metadata.secretAWSTag as { key: string; value: string }[] | undefined; - - if (secretAWSTag && secretAWSTag.length) { + if (shouldTag) { const describedSecret = await secretsManager.send( // requires secretsmanager:DescribeSecret policy new DescribeSecretCommand({ @@ -1145,14 +1216,6 @@ const syncSecretsAWSSecretManager = async ({ if (!describedSecret.Tags) return; - const integrationTagObj = secretAWSTag.reduce( - (acc, item) => { - acc[item.key] = item.value; - return acc; - }, - {} as Record - ); - const awsTagObj = (describedSecret.Tags || []).reduce( (acc, item) => { if (item.Key && item.Value) { @@ -1184,7 +1247,7 @@ const syncSecretsAWSSecretManager = async ({ } }); - secretAWSTag?.forEach((tag) => { + tagArray.forEach((tag) => { if (!(tag.key in awsTagObj)) { // create tag in AWS secret manager tagsToUpdate.push({ @@ -1221,8 +1284,8 @@ const syncSecretsAWSSecretManager = async ({ Name: secretId, SecretString: typeof secretValue === "string" ? secretValue : JSON.stringify(secretValue), ...(metadata.kmsKeyId && { KmsKeyId: metadata.kmsKeyId }), - Tags: metadata.secretAWSTag - ? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({ + Tags: shouldTag + ? tagArray.map((tag: { key: string; value: string }) => ({ Key: tag.key, Value: tag.value })) @@ -1239,7 +1302,10 @@ const syncSecretsAWSSecretManager = async ({ if (metadata.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE) { for await (const [key, value] of Object.entries(secrets)) { - await processAwsSecret(key, value.value); + await processAwsSecret(key, value.value, value.secretMetadata).catch((error) => { + error.secretKey = key; + throw error; + }); } } else { await processAwsSecret(integration.app as string, getSecretKeyValuePair(secrets)); @@ -1365,19 +1431,33 @@ const syncSecretsHeroku = async ({ * Sync/push [secrets] to Vercel project named [integration.app] */ const syncSecretsVercel = async ({ + createManySecretsRawFn, integration, integrationAuth, - secrets, + secrets: infisicalSecrets, accessToken }: { - integration: TIntegrations; + createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>; + integration: TIntegrations & { + projectId: string; + environment: { + id: string; + name: string; + slug: string; + }; + secretPath: string; + }; integrationAuth: TIntegrationAuths; - secrets: Record; + secrets: Record; accessToken: string; }) => { + const isCustomEnvironment = !["development", "preview", "production"].includes( + integration.targetEnvironment as string + ); interface VercelSecret { id?: string; type: string; + customEnvironmentIds?: string[]; key: string; value: string; target: string[]; @@ -1411,6 +1491,16 @@ const syncSecretsVercel = async ({ } ) ).data.envs.filter((secret) => { + if (isCustomEnvironment) { + if (!secret.customEnvironmentIds?.includes(integration.targetEnvironment as string)) { + // case: secret does not have the same custom environment + return false; + } + + // no need to check for preview environment, as custom environments are not available in preview + return true; + } + if (!secret.target.includes(integration.targetEnvironment as string)) { // case: secret does not have the same target environment return false; @@ -1445,80 +1535,135 @@ const syncSecretsVercel = async ({ } } - const updateSecrets: VercelSecret[] = []; - const deleteSecrets: VercelSecret[] = []; - const newSecrets: VercelSecret[] = []; + const metadata = IntegrationMetadataSchema.parse(integration.metadata); - // Identify secrets to create - Object.keys(secrets).forEach((key) => { - if (!(key in res)) { - // case: secret has been created - newSecrets.push({ - key, - value: secrets[key].value, - type: "encrypted", - target: [integration.targetEnvironment as string], - ...(integration.path - ? { - gitBranch: integration.path - } - : {}) - }); + // Default to overwrite target for old integrations that doesn't have a initial sync behavior set. + if (!metadata.initialSyncBehavior) { + metadata.initialSyncBehavior = IntegrationInitialSyncBehavior.OVERWRITE_TARGET; + } + + const secretsToAddToInfisical: { [key: string]: VercelSecret } = {}; + + Object.keys(res).forEach((vercelKey) => { + if (!integration.lastUsed) { + // first time using integration + // -> apply initial sync behavior + switch (metadata.initialSyncBehavior) { + // Override all the secrets in Vercel + case IntegrationInitialSyncBehavior.OVERWRITE_TARGET: { + if (!(vercelKey in infisicalSecrets)) infisicalSecrets[vercelKey] = null; + break; + } + case IntegrationInitialSyncBehavior.PREFER_SOURCE: { + // if the vercel secret is not in infisical, we need to add it to infisical + if (!(vercelKey in infisicalSecrets)) { + infisicalSecrets[vercelKey] = { + value: res[vercelKey].value + }; + secretsToAddToInfisical[vercelKey] = res[vercelKey]; + } + break; + } + default: { + throw new Error(`Invalid initial sync behavior: ${metadata.initialSyncBehavior}`); + } + } + } else if (!(vercelKey in infisicalSecrets)) { + infisicalSecrets[vercelKey] = null; } }); - // Identify secrets to update and delete - Object.keys(res).forEach((key) => { - if (key in secrets) { - if (res[key].value !== secrets[key].value) { - // case: secret value has changed - updateSecrets.push({ - id: res[key].id, - key, - value: secrets[key].value, - type: res[key].type, - target: res[key].target.includes(integration.targetEnvironment as string) - ? [...res[key].target] - : [...res[key].target, integration.targetEnvironment as string], - ...(integration.path - ? { - gitBranch: integration.path - } - : {}) - }); - } - } else { - // case: secret has been deleted - deleteSecrets.push({ - id: res[key].id, - key, - value: res[key].value, - type: "encrypted", // value doesn't matter - target: [integration.targetEnvironment as string], - ...(integration.path - ? { - gitBranch: integration.path - } - : {}) - }); - } - }); - - // Sync/push new secrets - if (newSecrets.length > 0) { - await request.post(`${IntegrationUrls.VERCEL_API_URL}/v10/projects/${integration.app}/env`, newSecrets, { - params, - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json" - } + if (Object.keys(secretsToAddToInfisical).length) { + await createManySecretsRawFn({ + projectId: integration.projectId, + environment: integration.environment.slug, + path: integration.secretPath, + secrets: Object.keys(secretsToAddToInfisical).map((key) => ({ + secretName: key, + secretValue: secretsToAddToInfisical[key].value, + type: SecretType.Shared, + secretComment: "" + })) }); } - for await (const secret of updateSecrets) { - if (secret.type !== "sensitive") { - const { id, ...updatedSecret } = secret; - await request.patch(`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env/${id}`, updatedSecret, { + // update and create logic + for await (const key of Object.keys(infisicalSecrets)) { + if (!(key in res) || infisicalSecrets[key]?.value !== res[key].value) { + // if the key is not in the vercel res, we need to create it + if (!(key in res)) { + await request.post( + `${IntegrationUrls.VERCEL_API_URL}/v10/projects/${integration.app}/env`, + { + key, + value: infisicalSecrets[key]?.value, + type: "encrypted", + ...(isCustomEnvironment + ? { + customEnvironmentIds: [integration.targetEnvironment as string] + } + : { + target: [integration.targetEnvironment as string] + }), + ...(integration.path + ? { + gitBranch: integration.path + } + : {}) + }, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + // Else if the key already exists and its not sensitive, we need to update it + } else if (res[key].type !== "sensitive") { + await request.patch( + `${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env/${res[key].id}`, + { + key, + value: infisicalSecrets[key]?.value, + type: res[key].type, + + ...(!isCustomEnvironment + ? { + target: res[key].target.includes(integration.targetEnvironment as string) + ? [...res[key].target] + : [...res[key].target, integration.targetEnvironment as string] + } + : { + customEnvironmentIds: res[key].customEnvironmentIds?.includes(integration.targetEnvironment as string) + ? [...(res[key].customEnvironmentIds || [])] + : [...(res[key]?.customEnvironmentIds || []), integration.targetEnvironment as string] + }), + + ...(integration.path + ? { + gitBranch: integration.path + } + : {}) + }, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } + } + } + + // delete logic + for await (const key of Object.keys(res)) { + if (infisicalSecrets[key] === null) { + // case: delete secret + await request.delete(`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env/${res[key].id}`, { params, headers: { Authorization: `Bearer ${accessToken}`, @@ -1527,16 +1672,6 @@ const syncSecretsVercel = async ({ }); } } - - for await (const secret of deleteSecrets) { - await request.delete(`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, { - params, - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json" - } - }); - } }; /** @@ -2123,7 +2258,9 @@ const syncSecretsFlyio = async ({ } `; - await request.post( + type TFlyioErrors = { message: string }[]; + + const setSecretsResp = await request.post<{ errors?: TFlyioErrors }>( IntegrationUrls.FLYIO_API_URL, { query: SetSecrets, @@ -2145,6 +2282,10 @@ const syncSecretsFlyio = async ({ } ); + if (setSecretsResp.data.errors?.length) { + throw new Error(JSON.stringify(setSecretsResp.data.errors)); + } + // get secrets interface FlyioSecret { name: string; @@ -2235,102 +2376,174 @@ const syncSecretsCircleCI = async ({ secrets: Record; accessToken: string; }) => { - const getProjectSlug = async () => { - const requestConfig = { - headers: { - "Circle-Token": accessToken, - "Accept-Encoding": "application/json" - } - }; - - try { - const projectDetails = ( - await request.get<{ slug: string }>( - `${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${integration.appId}`, - requestConfig + if (integration.scope === CircleCiScope.Context) { + // sync secrets to CircleCI + await Promise.all( + Object.keys(secrets).map(async (key) => + request.put( + `${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable/${key}`, + { + value: secrets[key].value + }, + { + headers: { + "Circle-Token": accessToken, + "Content-Type": "application/json" + } + } ) - ).data; + ) + ); - return projectDetails.slug; - } catch (err) { - if (err instanceof AxiosError) { - if (err.response?.data?.message !== "Not Found") { - throw new Error("Failed to get project slug from CircleCI during first attempt."); - } - } - } + // get secrets from CircleCI + const getSecretsRes = async () => { + type EnvVars = { + variable: string; + created_at: string; + updated_at: string; + context_id: string; + }; - // For backwards compatibility with old CircleCI integrations where we don't keep track of the organization name, so we can't filter by organization - try { - const circleCiOrganization = ( - await request.get<{ slug: string; name: string }[]>( - `${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`, - requestConfig - ) - ).data; + let nextPageToken: string | null | undefined; + const envVars: EnvVars[] = []; - // Case 1: This is a new integration where the organization name is stored under `integration.owner` - if (integration.owner) { - const org = circleCiOrganization.find((o) => o.name === integration.owner); - if (org) { - return `${org.slug}/${integration.app}`; - } - } - - // Case 2: This is an old integration where the organization name is not stored, so we have to assume the first organization is the correct one - return `${circleCiOrganization[0].slug}/${integration.app}`; - } catch (err) { - throw new Error("Failed to get project slug from CircleCI during second attempt."); - } - }; - - const projectSlug = await getProjectSlug(); - - // sync secrets to CircleCI - await Promise.all( - Object.keys(secrets).map(async (key) => - request.post( - `${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`, - { - name: key, - value: secrets[key].value - }, - { + while (nextPageToken !== null) { + const res = await request.get<{ + items: EnvVars[]; + next_page_token: string | null; + }>(`${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable`, { headers: { "Circle-Token": accessToken, - "Content-Type": "application/json" - } - } - ) - ) - ); + "Accept-Encoding": "application/json" + }, + params: nextPageToken + ? new URLSearchParams({ + "page-token": nextPageToken + }) + : undefined + }); - // get secrets from CircleCI - const getSecretsRes = ( - await request.get<{ items: { name: string }[] }>( - `${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`, - { + envVars.push(...res.data.items); + nextPageToken = res.data.next_page_token; + } + + return envVars; + }; + + // delete secrets from CircleCI + await Promise.all( + (await getSecretsRes()).map(async (sec) => { + if (!(sec.variable in secrets)) { + return request.delete( + `${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable/${sec.variable}`, + { + headers: { + "Circle-Token": accessToken, + "Content-Type": "application/json" + } + } + ); + } + }) + ); + } else { + const getProjectSlug = async () => { + const requestConfig = { headers: { "Circle-Token": accessToken, "Accept-Encoding": "application/json" } - } - ) - ).data?.items; + }; - // delete secrets from CircleCI - await Promise.all( - getSecretsRes.map(async (sec) => { - if (!(sec.name in secrets)) { - return request.delete(`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar/${sec.name}`, { + try { + const projectDetails = ( + await request.get<{ slug: string }>( + `${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${integration.appId}`, + requestConfig + ) + ).data; + + return projectDetails.slug; + } catch (err) { + if (err instanceof AxiosError) { + if (err.response?.data?.message !== "Not Found") { + throw new Error("Failed to get project slug from CircleCI during first attempt."); + } + } + } + + // For backwards compatibility with old CircleCI integrations where we don't keep track of the organization name, so we can't filter by organization + try { + const circleCiOrganization = ( + await request.get<{ slug: string; name: string }[]>( + `${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`, + requestConfig + ) + ).data; + + // Case 1: This is a new integration where the organization name is stored under `integration.owner` + if (integration.owner) { + const org = circleCiOrganization.find((o) => o.name === integration.owner); + if (org) { + return `${org.slug}/${integration.app}`; + } + } + + // Case 2: This is an old integration where the organization name is not stored, so we have to assume the first organization is the correct one + return `${circleCiOrganization[0].slug}/${integration.app}`; + } catch (err) { + throw new Error("Failed to get project slug from CircleCI during second attempt."); + } + }; + + const projectSlug = await getProjectSlug(); + + // sync secrets to CircleCI + await Promise.all( + Object.keys(secrets).map(async (key) => + request.post( + `${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`, + { + name: key, + value: secrets[key].value + }, + { + headers: { + "Circle-Token": accessToken, + "Content-Type": "application/json" + } + } + ) + ) + ); + + // get secrets from CircleCI + const getSecretsRes = ( + await request.get<{ items: { name: string }[] }>( + `${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`, + { headers: { "Circle-Token": accessToken, - "Content-Type": "application/json" + "Accept-Encoding": "application/json" } - }); - } - }) - ); + } + ) + ).data?.items; + + // delete secrets from CircleCI + await Promise.all( + getSecretsRes.map(async (sec) => { + if (!(sec.name in secrets)) { + return request.delete(`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar/${sec.name}`, { + headers: { + "Circle-Token": accessToken, + "Content-Type": "application/json" + } + }); + } + }) + ); + } }; /** @@ -2611,13 +2824,23 @@ const syncSecretsAzureDevops = async ({ * Sync/push [secrets] to GitLab repo with name [integration.app] */ const syncSecretsGitLab = async ({ + createManySecretsRawFn, integrationAuth, integration, secrets, accessToken }: { + createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>; integrationAuth: TIntegrationAuths; - integration: TIntegrations; + integration: TIntegrations & { + projectId: string; + environment: { + id: string; + name: string; + slug: string; + }; + secretPath: string; + }; secrets: Record; accessToken: string; }) => { @@ -2674,6 +2897,81 @@ const syncSecretsGitLab = async ({ return isValid; }); + if (!integration.lastUsed) { + const secretsToAddToInfisical: { [key: string]: GitLabSecret } = {}; + const secretsToRemoveInGitlab: GitLabSecret[] = []; + + if (!metadata.initialSyncBehavior) { + metadata.initialSyncBehavior = IntegrationInitialSyncBehavior.OVERWRITE_TARGET; + } + + getSecretsRes.forEach((gitlabSecret) => { + // first time using integration + // -> apply initial sync behavior + switch (metadata.initialSyncBehavior) { + // Override all the secrets in GitLab + case IntegrationInitialSyncBehavior.OVERWRITE_TARGET: { + if (!(gitlabSecret.key in secrets)) { + secretsToRemoveInGitlab.push(gitlabSecret); + } + break; + } + case IntegrationInitialSyncBehavior.PREFER_SOURCE: { + // if the secret is not in infisical, we need to add it to infisical + if (!(gitlabSecret.key in secrets)) { + secrets[gitlabSecret.key] = { + value: gitlabSecret.value + }; + // need to remove prefix and suffix from what we're saving to Infisical + const prefix = metadata?.secretPrefix || ""; + const suffix = metadata?.secretSuffix || ""; + let processedKey = gitlabSecret.key; + + // Remove prefix if it exists at the start + if (prefix && processedKey.startsWith(prefix)) { + processedKey = processedKey.slice(prefix.length); + } + + // Remove suffix if it exists at the end + if (suffix && processedKey.endsWith(suffix)) { + processedKey = processedKey.slice(0, -suffix.length); + } + + secretsToAddToInfisical[processedKey] = gitlabSecret; + } + break; + } + default: { + throw new Error(`Invalid initial sync behavior: ${metadata.initialSyncBehavior}`); + } + } + }); + + if (Object.keys(secretsToAddToInfisical).length) { + await createManySecretsRawFn({ + projectId: integration.projectId, + environment: integration.environment.slug, + path: integration.secretPath, + secrets: Object.keys(secretsToAddToInfisical).map((key) => ({ + secretName: key, + secretValue: secretsToAddToInfisical[key].value, + type: SecretType.Shared + })) + }); + } + + for await (const gitlabSecret of secretsToRemoveInGitlab) { + await request.delete( + `${gitLabApiUrl}/v4/projects/${integration?.appId}/variables/${gitlabSecret.key}?filter[environment_scope]=${integration.targetEnvironment}`, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + } + } + for await (const key of Object.keys(secrets)) { const existingSecret = getSecretsRes.find((s) => s.key === key); if (!existingSecret) { @@ -3075,7 +3373,7 @@ const syncSecretsTerraformCloud = async ({ }) => { // get secrets from Terraform Cloud const terraformSecrets = ( - await request.get<{ data: { attributes: { key: string; value: string }; id: string }[] }>( + await request.get<{ data: { attributes: { key: string; value: string; sensitive: boolean }; id: string }[] }>( `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars`, { headers: { @@ -3089,7 +3387,7 @@ const syncSecretsTerraformCloud = async ({ ...obj, [secret.attributes.key]: secret }), - {} as Record + {} as Record ); const secretsToAdd: { [key: string]: string } = {}; @@ -3170,7 +3468,8 @@ const syncSecretsTerraformCloud = async ({ attributes: { key, value: secrets[key]?.value, - category: integration.targetService + category: integration.targetService, + sensitive: true } } }, @@ -3183,7 +3482,11 @@ const syncSecretsTerraformCloud = async ({ } ); // case: secret exists in Terraform Cloud - } else if (secrets[key]?.value !== terraformSecrets[key].attributes.value) { + } else if ( + // we now set secrets to sensitive in Terraform Cloud, this checks if existing secrets are not sensitive and updates them accordingly + !terraformSecrets[key].attributes.sensitive || + secrets[key]?.value !== terraformSecrets[key].attributes.value + ) { // -> update secret await request.patch( `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars/${terraformSecrets[key].id}`, @@ -3193,7 +3496,8 @@ const syncSecretsTerraformCloud = async ({ id: terraformSecrets[key].id, attributes: { ...terraformSecrets[key], - value: secrets[key]?.value + value: secrets[key]?.value, + sensitive: true } } }, @@ -3275,7 +3579,7 @@ const syncSecretsTeamCity = async ({ .filter((parameter) => !parameter.inherited) .reduce( (obj, secret) => { - const secretName = secret.name.replace(/^env\./, ""); + const secretName = secret.name.startsWith(".env") ? secret.name.slice(4) : secret.name; return { ...obj, [secretName]: secret.value @@ -3292,7 +3596,10 @@ const syncSecretsTeamCity = async ({ `${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`, { name: `env.${key}`, - value: secrets[key].value + value: secrets[key].value, + type: { + rawValue: "password display='hidden'" + } }, { headers: { @@ -3332,7 +3639,7 @@ const syncSecretsTeamCity = async ({ ) ).data.property.reduce( (obj, secret) => { - const secretName = secret.name.replace(/^env\./, ""); + const secretName = secret.name.startsWith("env.") ? secret.name.slice(4) : secret.name; return { ...obj, [secretName]: secret.value @@ -3349,7 +3656,10 @@ const syncSecretsTeamCity = async ({ `${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters`, { name: `env.${key}`, - value: secrets[key].value + value: secrets[key].value, + type: { + rawValue: "password display='hidden'" + } }, { headers: { @@ -3495,17 +3805,28 @@ const syncSecretsCloudflarePages = async ({ ); const metadata = z.record(z.any()).parse(integration.metadata); - if (metadata.shouldAutoRedeploy) { - await request.post( - `${IntegrationUrls.CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}/deployments`, - {}, - { - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: "application/json" + if (metadata.shouldAutoRedeploy && integration.targetEnvironment === "production") { + await request + .post( + `${IntegrationUrls.CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}/deployments`, + {}, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } } - } - ); + ) + .catch((error) => { + if (error instanceof AxiosError && error.response?.status === 304) { + logger.info( + `syncSecretsCloudflarePages: CF pages redeployment returned status code 304 for integration [id=${integration.id}]` + ); + return; + } + + throw error; + }); } }; @@ -3631,7 +3952,14 @@ const syncSecretsBitBucket = async ({ const res: { [key: string]: BitbucketVariable } = {}; let hasNextPage = true; - let variablesUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${integration.targetEnvironmentId}/${integration.appId}/pipelines_config/variables`; + + const rootUrl = integration.targetServiceId + ? // scope: deployment environment + `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${integration.targetEnvironmentId}/${integration.appId}/deployments_config/environments/${integration.targetServiceId}/variables` + : // scope: repository + `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${integration.targetEnvironmentId}/${integration.appId}/pipelines_config/variables`; + + let variablesUrl = rootUrl; while (hasNextPage) { const { data }: { data: VariablesResponse } = await request.get(variablesUrl, { @@ -3658,7 +3986,7 @@ const syncSecretsBitBucket = async ({ if (key in res) { // update existing secret await request.put( - `${variablesUrl}/${res[key].uuid}`, + `${rootUrl}/${res[key].uuid}`, { key, value: secrets[key].value, @@ -3674,7 +4002,7 @@ const syncSecretsBitBucket = async ({ } else { // create new secret await request.post( - variablesUrl, + rootUrl, { key, value: secrets[key].value, @@ -3806,10 +4134,10 @@ const syncSecretsWindmill = async ({ is_secret: boolean; description?: string; } - + const apiUrl = integration.url ? `${integration.url}/api` : IntegrationUrls.WINDMILL_API_URL; // get secrets stored in windmill workspace const res = ( - await request.get(`${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/list`, { + await request.get(`${apiUrl}/w/${integration.appId}/variables/list`, { headers: { Authorization: `Bearer ${accessToken}`, "Accept-Encoding": "application/json" @@ -3824,8 +4152,7 @@ const syncSecretsWindmill = async ({ ); // eslint-disable-next-line - const pattern = new RegExp("^(u/|f/)[a-zA-Z0-9_-]+/([a-zA-Z0-9_-]+/)*[a-zA-Z0-9_-]*[^/]$"); - + const pattern = new RE2("^(u/|f/)[a-zA-Z0-9_-]+/([a-zA-Z0-9_-]+/)*[a-zA-Z0-9_-]*[^/]$"); for await (const key of Object.keys(secrets)) { if ((key.startsWith("u/") || key.startsWith("f/")) && pattern.test(key)) { if (!(key in res)) { @@ -3833,7 +4160,7 @@ const syncSecretsWindmill = async ({ // -> create secret await request.post( - `${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/create`, + `${apiUrl}/w/${integration.appId}/variables/create`, { path: key, value: secrets[key].value, @@ -3850,7 +4177,7 @@ const syncSecretsWindmill = async ({ } else { // -> update secret await request.post( - `${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/update/${res[key].path}`, + `${apiUrl}/w/${integration.appId}/variables/update/${res[key].path}`, { path: key, value: secrets[key].value, @@ -3871,16 +4198,13 @@ const syncSecretsWindmill = async ({ for await (const key of Object.keys(res)) { if (!(key in secrets)) { // -> delete secret - await request.delete( - `${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/delete/${res[key].path}`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json", - "Accept-Encoding": "application/json" - } + await request.delete(`${apiUrl}/w/${integration.appId}/variables/delete/${res[key].path}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + "Accept-Encoding": "application/json" } - ); + }); } } }; @@ -4188,6 +4512,61 @@ const syncSecretsRundeck = async ({ } }; +const syncSecretsOctopusDeploy = async ({ + integration, + integrationAuth, + secrets, + accessToken +}: { + integration: TIntegrations; + integrationAuth: TIntegrationAuths; + secrets: Record; + accessToken: string; +}) => { + let url: string; + switch (integration.scope) { + case OctopusDeployScope.Project: + url = `${integrationAuth.url}/api/${integration.targetEnvironmentId}/projects/${integration.appId}/variables`; + break; + // future support tenant, variable set, etc. + default: + throw new InternalServerError({ message: `Unhandled Octopus Deploy scope: ${integration.scope}` }); + } + + // SDK doesn't support variable set... + const { data: variableSet } = await request.get(url, { + headers: { + "X-NuGet-ApiKey": accessToken, + Accept: "application/json" + } + }); + + await request.put( + url, + { + ...variableSet, + Variables: Object.entries(secrets).map(([key, value]) => ({ + Name: key, + Value: value.value, + Description: value.comment ?? "", + Scope: + (integration.metadata as { octopusDeployScopeValues: TOctopusDeployVariableSet["ScopeValues"] }) + ?.octopusDeployScopeValues ?? {}, + IsEditable: false, + Prompt: null, + Type: "String", + IsSensitive: true + })) + } as unknown as TOctopusDeployVariableSet, + { + headers: { + "X-NuGet-ApiKey": accessToken, + Accept: "application/json" + } + } + ); +}; + /** * Sync/push [secrets] to [app] in integration named [integration] * @@ -4220,7 +4599,7 @@ export const syncIntegrationSecrets = async ({ secretPath: string; }; integrationAuth: TIntegrationAuths; - secrets: Record; + secrets: Record; accessId: string | null; awsAssumeRoleArn: string | null; accessToken: string; @@ -4299,7 +4678,8 @@ export const syncIntegrationSecrets = async ({ integration, integrationAuth, secrets, - accessToken + accessToken, + createManySecretsRawFn }); break; case Integrations.NETLIFY: @@ -4324,7 +4704,8 @@ export const syncIntegrationSecrets = async ({ integrationAuth, integration, secrets, - accessToken + accessToken, + createManySecretsRawFn }); break; case Integrations.RENDER: @@ -4500,6 +4881,14 @@ export const syncIntegrationSecrets = async ({ accessToken }); break; + case Integrations.OCTOPUS_DEPLOY: + await syncSecretsOctopusDeploy({ + integration, + integrationAuth, + secrets, + accessToken + }); + break; default: throw new BadRequestError({ message: "Invalid integration" }); } diff --git a/backend/src/services/integration-auth/integration-team.ts b/backend/src/services/integration-auth/integration-team.ts index c39b2c44f..d738625e2 100644 --- a/backend/src/services/integration-auth/integration-team.ts +++ b/backend/src/services/integration-auth/integration-team.ts @@ -1,3 +1,5 @@ +import { AxiosResponse } from "axios"; + import { request } from "@app/lib/config/request"; import { BadRequestError } from "@app/lib/errors"; @@ -11,19 +13,27 @@ const getTeamsGitLab = async ({ url, accessToken }: { url: string; accessToken: const gitLabApiUrl = url ? `${url}/api` : IntegrationUrls.GITLAB_API_URL; let teams: Team[] = []; - const res = ( - await request.get<{ name: string; id: string }[]>(`${gitLabApiUrl}/v4/groups`, { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json" + let page: number = 1; + while (page > 0) { + // eslint-disable-next-line no-await-in-loop + const { data, headers }: AxiosResponse<{ name: string; id: string }[]> = await request.get( + `${gitLabApiUrl}/v4/groups?page=${page}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } } - }) - ).data; + ); - teams = res.map((t) => ({ - name: t.name, - id: t.id.toString() - })); + page = Number(headers["x-next-page"] ?? ""); + teams = teams.concat( + data.map((t) => ({ + name: t.name, + id: t.id.toString() + })) + ); + } return teams; }; diff --git a/backend/src/services/integration/integration-schema.ts b/backend/src/services/integration/integration-schema.ts index 99f1d996f..084946c0b 100644 --- a/backend/src/services/integration/integration-schema.ts +++ b/backend/src/services/integration/integration-schema.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { INTEGRATION } from "@app/lib/api-docs"; -import { IntegrationMappingBehavior } from "../integration-auth/integration-list"; +import { IntegrationMappingBehavior, IntegrationMetadataSyncMode } from "../integration-auth/integration-list"; export const IntegrationMetadataSchema = z.object({ initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir), @@ -35,6 +35,8 @@ export const IntegrationMetadataSchema = z.object({ .optional() .describe(INTEGRATION.CREATE.metadata.secretAWSTag), + azureLabel: z.string().optional().describe(INTEGRATION.CREATE.metadata.azureLabel), + githubVisibility: z .union([z.literal("selected"), z.literal("private"), z.literal("all")]) .optional() @@ -46,5 +48,23 @@ export const IntegrationMetadataSchema = z.object({ shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete), shouldEnableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldEnableDelete), shouldMaskSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldMaskSecrets), - shouldProtectSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldProtectSecrets) + shouldProtectSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldProtectSecrets), + + metadataSyncMode: z + .nativeEnum(IntegrationMetadataSyncMode) + .optional() + .describe(INTEGRATION.CREATE.metadata.metadataSyncMode), + + octopusDeployScopeValues: z + .object({ + // in Octopus Deploy Scope Value Format + Environment: z.string().array().optional(), + Action: z.string().array().optional(), + Channel: z.string().array().optional(), + Machine: z.string().array().optional(), + ProcessOwner: z.string().array().optional(), + Role: z.string().array().optional() + }) + .optional() + .describe(INTEGRATION.CREATE.metadata.octopusDeployScopeValues) }); diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts index 12f4c77de..ad08e89d1 100644 --- a/backend/src/services/integration/integration-service.ts +++ b/backend/src/services/integration/integration-service.ts @@ -1,7 +1,13 @@ -import { ForbiddenError, subject } from "@casl/ability"; +import { ForbiddenError } from "@casl/ability"; +import { ActionProjectType } from "@app/db/schemas"; +import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { NotFoundError } from "@app/lib/errors"; import { TProjectPermission } from "@app/lib/types"; @@ -9,6 +15,7 @@ import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service"; import { deleteIntegrationSecrets } from "../integration-auth/integration-delete-secret"; import { TKmsServiceFactory } from "../kms/kms-service"; +import { KmsDataKey } from "../kms/kms-types"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TSecretDALFactory } from "../secret/secret-dal"; import { TSecretQueueFactory } from "../secret/secret-queue"; @@ -79,22 +86,20 @@ export const integrationServiceFactory = ({ if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${integrationAuthId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integrationAuth.projectId, + projectId: integrationAuth.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: sourceEnvironment, - secretPath - }) - ); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment: sourceEnvironment, + secretPath + }); const folder = await folderDAL.findBySecretPath(integrationAuth.projectId, sourceEnvironment, secretPath); if (!folder) { @@ -150,31 +155,31 @@ export const integrationServiceFactory = ({ isActive, environment, secretPath, - metadata + region, + metadata, + path }: TUpdateIntegrationDTO) => { const integration = await integrationDAL.findById(id); if (!integration) throw new NotFoundError({ message: `Integration with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integration.projectId, + projectId: integration.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations); const newEnvironment = environment || integration.environment.slug; const newSecretPath = secretPath || integration.secretPath; if (environment || secretPath) { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: newEnvironment, - secretPath: newSecretPath - }) - ); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment: newEnvironment, + secretPath: newSecretPath + }); } const folder = await folderDAL.findBySecretPath(integration.projectId, newEnvironment, newSecretPath); @@ -191,7 +196,9 @@ export const integrationServiceFactory = ({ appId, targetEnvironment, owner, + region, secretPath, + path, metadata: { ...(integration.metadata as object), ...metadata @@ -219,13 +226,14 @@ export const integrationServiceFactory = ({ }); } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integration?.projectId || "", + projectId: integration.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); if (!integration) { @@ -237,6 +245,47 @@ export const integrationServiceFactory = ({ return { ...integration, envId: integration.environment.id }; }; + const getIntegrationAWSIamRole = async ({ id, actor, actorAuthMethod, actorId, actorOrgId }: TGetIntegrationDTO) => { + const integration = await integrationDAL.findById(id); + + if (!integration) { + throw new NotFoundError({ + message: `Integration with ID '${id}' not found` + }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: integration.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + + const integrationAuth = await integrationAuthDAL.findById(integration.integrationAuthId); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: integration.projectId + }); + let awsIamRole: string | null = null; + if (integrationAuth.encryptedAwsAssumeIamRoleArn) { + const awsAssumeRoleArn = secretManagerDecryptor({ + cipherTextBlob: Buffer.from(integrationAuth.encryptedAwsAssumeIamRoleArn) + }).toString(); + if (awsAssumeRoleArn) { + const [, role] = awsAssumeRoleArn.split(":role/"); + awsIamRole = role; + } + } + + return { + role: awsIamRole + }; + }; + const deleteIntegration = async ({ actorId, id, @@ -248,13 +297,14 @@ export const integrationServiceFactory = ({ const integration = await integrationDAL.findById(id); if (!integration) throw new NotFoundError({ message: `Integration with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integration.projectId, + projectId: integration.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations); const integrationAuth = await integrationAuthDAL.findById(integration.integrationAuthId); @@ -284,13 +334,14 @@ export const integrationServiceFactory = ({ actorAuthMethod, projectId }: TProjectPermission) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const integrations = await integrationDAL.findByProjectId(projectId); @@ -303,13 +354,14 @@ export const integrationServiceFactory = ({ throw new NotFoundError({ message: `Integration with ID '${id}' not found` }); } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - integration.projectId, + projectId: integration.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); await secretQueueService.syncIntegrations({ @@ -329,6 +381,7 @@ export const integrationServiceFactory = ({ deleteIntegration, listIntegrationByProject, getIntegration, + getIntegrationAWSIamRole, syncIntegration }; }; diff --git a/backend/src/services/integration/integration-types.ts b/backend/src/services/integration/integration-types.ts index a27c4f6ac..f662affd8 100644 --- a/backend/src/services/integration/integration-types.ts +++ b/backend/src/services/integration/integration-types.ts @@ -49,6 +49,8 @@ export type TUpdateIntegrationDTO = { appId?: string; isActive?: boolean; secretPath?: string; + region?: string; + path?: string; targetEnvironment?: string; owner?: string; environment?: string; diff --git a/backend/src/services/kms/kms-fns.ts b/backend/src/services/kms/kms-fns.ts index 96196e1af..8c7aa13dd 100644 --- a/backend/src/services/kms/kms-fns.ts +++ b/backend/src/services/kms/kms-fns.ts @@ -1,11 +1,55 @@ -import { SymmetricEncryption } from "@app/lib/crypto/cipher"; +import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher"; +import { AsymmetricKeyAlgorithm } from "@app/lib/crypto/sign"; +import { BadRequestError } from "@app/lib/errors"; -export const getByteLengthForAlgorithm = (encryptionAlgorithm: SymmetricEncryption) => { +import { KmsKeyUsage } from "./kms-types"; + +export const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000"; + +export const getByteLengthForSymmetricEncryptionAlgorithm = (encryptionAlgorithm: SymmetricKeyAlgorithm) => { switch (encryptionAlgorithm) { - case SymmetricEncryption.AES_GCM_128: + case SymmetricKeyAlgorithm.AES_GCM_128: return 16; - case SymmetricEncryption.AES_GCM_256: + case SymmetricKeyAlgorithm.AES_GCM_256: default: return 32; } }; + +export const verifyKeyTypeAndAlgorithm = ( + keyUsage: KmsKeyUsage, + algorithm: SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm, + extra?: { + forceType?: KmsKeyUsage; + } +) => { + if (extra?.forceType && keyUsage !== extra.forceType) { + throw new BadRequestError({ + message: `Unsupported key type, expected ${extra.forceType} but got ${keyUsage}` + }); + } + + if (keyUsage === KmsKeyUsage.ENCRYPT_DECRYPT) { + if (!Object.values(SymmetricKeyAlgorithm).includes(algorithm as SymmetricKeyAlgorithm)) { + throw new BadRequestError({ + message: `Unsupported encryption algorithm for encrypt/decrypt key: ${algorithm as string}` + }); + } + + return true; + } + + if (keyUsage === KmsKeyUsage.SIGN_VERIFY) { + if (!Object.values(AsymmetricKeyAlgorithm).includes(algorithm as AsymmetricKeyAlgorithm)) { + throw new BadRequestError({ + message: `Unsupported sign/verify algorithm for sign/verify key: ${algorithm as string}` + }); + } + + return true; + } + + throw new BadRequestError({ + message: `Unsupported key type: ${keyUsage as string}` + }); +}; diff --git a/backend/src/services/kms/kms-key-dal.ts b/backend/src/services/kms/kms-key-dal.ts index e0246c096..a0dd12191 100644 --- a/backend/src/services/kms/kms-key-dal.ts +++ b/backend/src/services/kms/kms-key-dal.ts @@ -3,12 +3,32 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { KmsKeysSchema, TableName, TInternalKms, TKmsKeys } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex"; import { OrderByDirection } from "@app/lib/types"; import { CmekOrderBy, TListCmeksByProjectIdDTO } from "@app/services/cmek/cmek-types"; export type TKmsKeyDALFactory = ReturnType; +type TCmekFindFilter = Parameters>[0]; + +const baseCmekQuery = ({ filter, db, tx }: { db: TDbClient; filter?: TCmekFindFilter; tx?: Knex }) => { + const query = (tx || db.replicaNode())(TableName.KmsKey) + .where(`${TableName.KmsKey}.isReserved`, false) + .join(TableName.InternalKms, `${TableName.InternalKms}.kmsKeyId`, `${TableName.KmsKey}.id`) + .select( + selectAllTableCols(TableName.KmsKey), + db.ref("encryptionAlgorithm").withSchema(TableName.InternalKms), + db.ref("version").withSchema(TableName.InternalKms) + ); + + if (filter) { + /* eslint-disable @typescript-eslint/no-misused-promises */ + void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.KmsKey, filter))); + } + + return query; +}; + export const kmskeyDALFactory = (db: TDbClient) => { const kmsOrm = ormify(db, TableName.KmsKey); @@ -73,7 +93,33 @@ export const kmskeyDALFactory = (db: TDbClient) => { } }; - const findKmsKeysByProjectId = async ( + const findProjectCmeks = async (projectId: string, tx?: Knex) => { + try { + const result = await (tx || db.replicaNode())(TableName.KmsKey) + .where({ + [`${TableName.KmsKey}.projectId` as "projectId"]: projectId, + [`${TableName.KmsKey}.isReserved` as "isReserved"]: false + }) + .join(TableName.Organization, `${TableName.KmsKey}.orgId`, `${TableName.Organization}.id`) + .join(TableName.InternalKms, `${TableName.KmsKey}.id`, `${TableName.InternalKms}.kmsKeyId`) + .select(selectAllTableCols(TableName.KmsKey)) + .select( + db.ref("encryptionAlgorithm").withSchema(TableName.InternalKms).as("internalKmsEncryptionAlgorithm"), + db.ref("version").withSchema(TableName.InternalKms).as("internalKmsVersion") + ); + + return result.map((entry) => ({ + ...KmsKeysSchema.parse(entry), + isActive: !entry.isDisabled, + algorithm: entry.internalKmsEncryptionAlgorithm, + version: entry.internalKmsVersion + })); + } catch (error) { + throw new DatabaseError({ error, name: "Find project cmeks" }); + } + }; + + const listCmeksByProjectId = async ( { projectId, offset = 0, @@ -92,6 +138,7 @@ export const kmskeyDALFactory = (db: TDbClient) => { void qb.whereILike("name", `%${search}%`); } }) + .where(`${TableName.KmsKey}.isReserved`, false) .join(TableName.InternalKms, `${TableName.InternalKms}.kmsKeyId`, `${TableName.KmsKey}.id`) .select< (TKmsKeys & @@ -118,5 +165,33 @@ export const kmskeyDALFactory = (db: TDbClient) => { } }; - return { ...kmsOrm, findByIdWithAssociatedKms, findKmsKeysByProjectId }; + const findCmekById = async (id: string, tx?: Knex) => { + try { + const key = await baseCmekQuery({ + filter: { id }, + db, + tx + }).first(); + + return key; + } catch (error) { + throw new DatabaseError({ error, name: "Find by ID - KMS Key" }); + } + }; + + const findCmekByName = async (keyName: string, projectId: string, tx?: Knex) => { + try { + const key = await baseCmekQuery({ + filter: { name: keyName, projectId }, + db, + tx + }).first(); + + return key; + } catch (error) { + throw new DatabaseError({ error, name: "Find by Name - KMS Key" }); + } + }; + + return { ...kmsOrm, findByIdWithAssociatedKms, listCmeksByProjectId, findCmekById, findCmekByName, findProjectCmeks }; }; diff --git a/backend/src/services/kms/kms-root-config-dal.ts b/backend/src/services/kms/kms-root-config-dal.ts index f448e2df8..31826b79d 100644 --- a/backend/src/services/kms/kms-root-config-dal.ts +++ b/backend/src/services/kms/kms-root-config-dal.ts @@ -1,10 +1,25 @@ +import { Knex } from "knex"; + import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; export type TKmsRootConfigDALFactory = ReturnType; export const kmsRootConfigDALFactory = (db: TDbClient) => { const kmsOrm = ormify(db, TableName.KmsServerRootConfig); - return kmsOrm; + + const findById = async (id: string, tx?: Knex) => { + try { + const result = await (tx || db)(TableName.KmsServerRootConfig) + .where({ id } as never) + .first("*"); + return result; + } catch (error) { + throw new DatabaseError({ error, name: "Find by id" }); + } + }; + + return { ...kmsOrm, findById }; }; diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index 1b5c282a6..07ed90bef 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -2,22 +2,30 @@ import slugify from "@sindresorhus/slugify"; import { Knex } from "knex"; import { z } from "zod"; -import { KmsKeysSchema } from "@app/db/schemas"; +import { KmsKeysSchema, TKmsRootConfig } from "@app/db/schemas"; import { AwsKmsProviderFactory } from "@app/ee/services/external-kms/providers/aws-kms"; +import { GcpKmsProviderFactory } from "@app/ee/services/external-kms/providers/gcp-kms"; import { ExternalKmsAwsSchema, + ExternalKmsGcpSchema, KmsProviders, TExternalKmsProviderFns } from "@app/ee/services/external-kms/providers/model"; -import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; -import { getConfig } from "@app/lib/config/env"; +import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; +import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; +import { TEnvConfig } from "@app/lib/config/env"; import { randomSecureBytes } from "@app/lib/crypto"; -import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher"; +import { symmetricCipherService, SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher"; import { generateHash } from "@app/lib/crypto/encryption"; +import { AsymmetricKeyAlgorithm, signingService } from "@app/lib/crypto/sign"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; -import { getByteLengthForAlgorithm } from "@app/services/kms/kms-fns"; +import { + getByteLengthForSymmetricEncryptionAlgorithm, + KMS_ROOT_CONFIG_UUID, + verifyKeyTypeAndAlgorithm +} from "@app/services/kms/kms-fns"; import { TOrgDALFactory } from "../org/org-dal"; import { TProjectDALFactory } from "../project/project-dal"; @@ -26,44 +34,50 @@ import { TKmsKeyDALFactory } from "./kms-key-dal"; import { TKmsRootConfigDALFactory } from "./kms-root-config-dal"; import { KmsDataKey, + KmsKeyUsage, KmsType, + RootKeyEncryptionStrategy, TDecryptWithKeyDTO, TDecryptWithKmsDTO, TEncryptionWithKeyDTO, TEncryptWithKmsDataKeyDTO, TEncryptWithKmsDTO, TGenerateKMSDTO, - TUpdateProjectSecretManagerKmsKeyDTO + TGetKeyMaterialDTO, + TGetPublicKeyDTO, + TImportKeyMaterialDTO, + TSignWithKmsDTO, + TUpdateProjectSecretManagerKmsKeyDTO, + TVerifyWithKmsDTO } from "./kms-types"; type TKmsServiceFactoryDep = { kmsDAL: TKmsKeyDALFactory; projectDAL: Pick; orgDAL: Pick; - kmsRootConfigDAL: Pick; + kmsRootConfigDAL: Pick; keyStore: Pick; internalKmsDAL: Pick; + hsmService: THsmServiceFactory; + envConfig: Pick; }; export type TKmsServiceFactory = ReturnType; -const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000"; - -const KMS_ROOT_CREATION_WAIT_KEY = "wait_till_ready_kms_root_key"; -const KMS_ROOT_CREATION_WAIT_TIME = 10; - // akhilmhdh: Don't edit this value. This is measured for blob concatination in kms const KMS_VERSION = "v01"; const KMS_VERSION_BLOB_LENGTH = 3; const KmsSanitizedSchema = KmsKeysSchema.extend({ isExternal: z.boolean() }); export const kmsServiceFactory = ({ + envConfig, kmsDAL, kmsRootConfigDAL, keyStore, internalKmsDAL, orgDAL, - projectDAL + projectDAL, + hsmService }: TKmsServiceFactoryDep) => { let ROOT_ENCRYPTION_KEY = Buffer.alloc(0); @@ -78,19 +92,42 @@ export const kmsServiceFactory = ({ tx, name, projectId, - encryptionAlgorithm = SymmetricEncryption.AES_GCM_256, + encryptionAlgorithm = SymmetricKeyAlgorithm.AES_GCM_256, + keyUsage = KmsKeyUsage.ENCRYPT_DECRYPT, description }: TGenerateKMSDTO) => { - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + // daniel: ensure that the key type (sign/encrypt) and the encryption algorithm are compatible. + verifyKeyTypeAndAlgorithm(keyUsage, encryptionAlgorithm); - const kmsKeyMaterial = randomSecureBytes(getByteLengthForAlgorithm(encryptionAlgorithm)); + let kmsKeyMaterial: Buffer | null = null; + if (keyUsage === KmsKeyUsage.ENCRYPT_DECRYPT) { + kmsKeyMaterial = randomSecureBytes( + getByteLengthForSymmetricEncryptionAlgorithm(encryptionAlgorithm as SymmetricKeyAlgorithm) + ); + } else if (keyUsage === KmsKeyUsage.SIGN_VERIFY) { + const { generateAsymmetricPrivateKey, getPublicKeyFromPrivateKey } = signingService( + encryptionAlgorithm as AsymmetricKeyAlgorithm + ); + kmsKeyMaterial = await generateAsymmetricPrivateKey(); + // daniel: safety check to ensure we're able to extract the public key from the private key before we proceed to key creation + getPublicKeyFromPrivateKey(kmsKeyMaterial); + } + + if (!kmsKeyMaterial) { + throw new BadRequestError({ + message: `Invalid KMS key type. No key material was created for key usage '${keyUsage}' using algorithm '${encryptionAlgorithm}'` + }); + } + + const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY); const sanitizedName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase()); const dbQuery = async (db: Knex) => { const kmsDoc = await kmsDAL.create( { name: sanitizedName, + keyUsage, orgId, isReserved, projectId, @@ -110,6 +147,7 @@ export const kmsServiceFactory = ({ ); return kmsDoc; }; + if (tx) return dbQuery(tx); const doc = await kmsDAL.transaction(async (tx2) => dbQuery(tx2)); return doc; @@ -129,7 +167,7 @@ export const kmsServiceFactory = ({ */ const encryptWithInputKey = async ({ key }: Omit) => { // akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); return ({ plainText }: Pick) => { const encryptedPlainTextBlob = cipher.encrypt(plainText, key); // Buffer#1 encrypted text + Buffer#2 version number @@ -144,7 +182,7 @@ export const kmsServiceFactory = ({ * This can be even later exposed directly as api for encryption as function */ const decryptWithInputKey = async ({ key }: Omit) => { - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); return ({ cipherTextBlob: versionedCipherTextBlob }: Pick) => { const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH); @@ -222,7 +260,7 @@ export const kmsServiceFactory = ({ }; const encryptWithRootKey = () => { - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); return (plainTextBuffer: Buffer) => { const encryptedBuffer = cipher.encrypt(plainTextBuffer, ROOT_ENCRYPTION_KEY); @@ -231,7 +269,7 @@ export const kmsServiceFactory = ({ }; const decryptWithRootKey = () => { - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); return (cipherTextBuffer: Buffer) => { return cipher.decrypt(cipherTextBuffer, ROOT_ENCRYPTION_KEY); @@ -289,6 +327,16 @@ export const kmsServiceFactory = ({ }); break; } + case KmsProviders.Gcp: { + const decryptedProviderInput = await ExternalKmsGcpSchema.parseAsync( + JSON.parse(decryptedProviderInputBlob.toString("utf8")) + ); + + externalKms = await GcpKmsProviderFactory({ + inputs: decryptedProviderInput + }); + break; + } default: throw new Error("Invalid KMS provider."); } @@ -300,9 +348,14 @@ export const kmsServiceFactory = ({ }; } + const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as SymmetricKeyAlgorithm; + verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, { + forceType: KmsKeyUsage.ENCRYPT_DECRYPT + }); + // internal KMS - const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); - const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption); + const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); + const dataCipher = symmetricCipherService(encryptionAlgorithm); const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); return ({ cipherTextBlob: versionedCipherTextBlob }: Pick) => { @@ -312,6 +365,144 @@ export const kmsServiceFactory = ({ }; }; + const getKeyMaterial = async ({ kmsId }: TGetKeyMaterialDTO) => { + const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId); + if (!kmsDoc) { + throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` }); + } + + if (kmsDoc.isReserved) { + throw new BadRequestError({ + message: "Cannot get key material for reserved key" + }); + } + + if (kmsDoc.externalKms) { + throw new BadRequestError({ + message: "Cannot get key material for external key" + }); + } + + const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); + const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); + + return kmsKey; + }; + + const importKeyMaterial = async ( + { key, algorithm, name, isReserved, projectId, orgId, keyUsage }: TImportKeyMaterialDTO, + tx?: Knex + ) => { + // daniel: currently we only support imports for encrypt/decrypt keys + verifyKeyTypeAndAlgorithm(keyUsage, algorithm, { forceType: KmsKeyUsage.ENCRYPT_DECRYPT }); + + const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); + + const expectedByteLength = getByteLengthForSymmetricEncryptionAlgorithm(algorithm as SymmetricKeyAlgorithm); + if (key.byteLength !== expectedByteLength) { + throw new BadRequestError({ + message: `Invalid key length for ${algorithm}. Expected ${expectedByteLength} bytes but got ${key.byteLength} bytes` + }); + } + + const encryptedKeyMaterial = cipher.encrypt(key, ROOT_ENCRYPTION_KEY); + const sanitizedName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase()); + const dbQuery = async (db: Knex) => { + const kmsDoc = await kmsDAL.create( + { + name: sanitizedName, + keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT, + orgId, + isReserved, + projectId + }, + db + ); + + await internalKmsDAL.create( + { + version: 1, + encryptedKey: encryptedKeyMaterial, + encryptionAlgorithm: algorithm, + kmsKeyId: kmsDoc.id + }, + db + ); + return kmsDoc; + }; + if (tx) return dbQuery(tx); + const doc = await kmsDAL.transaction(async (tx2) => dbQuery(tx2)); + return doc; + }; + + const getPublicKey = async ({ kmsId }: TGetPublicKeyDTO) => { + const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId); + if (!kmsDoc) { + throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` }); + } + + const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as AsymmetricKeyAlgorithm; + + verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, { + forceType: KmsKeyUsage.SIGN_VERIFY + }); + + const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); + const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); + + return signingService(encryptionAlgorithm).getPublicKeyFromPrivateKey(kmsKey); + }; + + const signWithKmsKey = async ({ kmsId }: Pick) => { + const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId); + if (!kmsDoc) { + throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` }); + } + + const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as AsymmetricKeyAlgorithm; + verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, { + forceType: KmsKeyUsage.SIGN_VERIFY + }); + + const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); + const { sign } = signingService(encryptionAlgorithm); + return async ({ + data, + signingAlgorithm, + isDigest + }: Pick) => { + const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); + const signature = await sign(data, kmsKey, signingAlgorithm, isDigest); + + return Promise.resolve({ signature, algorithm: signingAlgorithm }); + }; + }; + + const verifyWithKmsKey = async ({ + kmsId, + signingAlgorithm + }: Pick) => { + const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId); + if (!kmsDoc) { + throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` }); + } + + const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as AsymmetricKeyAlgorithm; + verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, { + forceType: KmsKeyUsage.SIGN_VERIFY + }); + + const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); + const { verify, getPublicKeyFromPrivateKey } = signingService(encryptionAlgorithm); + return async ({ data, signature, isDigest }: Pick) => { + const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); + + const publicKey = getPublicKeyFromPrivateKey(kmsKey); + const signatureValid = await verify(data, signature, publicKey, signingAlgorithm, isDigest); + return Promise.resolve({ signatureValid, algorithm: signingAlgorithm }); + }; + }; + const encryptWithKmsKey = async ({ kmsId }: Omit, tx?: Knex) => { const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx); if (!kmsDoc) { @@ -351,6 +542,16 @@ export const kmsServiceFactory = ({ }); break; } + case KmsProviders.Gcp: { + const decryptedProviderInput = await ExternalKmsGcpSchema.parseAsync( + JSON.parse(decryptedProviderInputBlob.toString("utf8")) + ); + + externalKms = await GcpKmsProviderFactory({ + inputs: decryptedProviderInput + }); + break; + } default: throw new Error("Invalid KMS provider."); } @@ -362,9 +563,14 @@ export const kmsServiceFactory = ({ }; } + const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as SymmetricKeyAlgorithm; + verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, { + forceType: KmsKeyUsage.ENCRYPT_DECRYPT + }); + // internal KMS - const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); - const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption); + const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); + const dataCipher = symmetricCipherService(encryptionAlgorithm); return ({ plainText }: Pick) => { const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); const encryptedPlainTextBlob = dataCipher.encrypt(plainText, kmsKey); @@ -449,7 +655,8 @@ export const kmsServiceFactory = ({ } const kmsDecryptor = await decryptWithKmsKey({ - kmsId: kmsKeyId + kmsId: kmsKeyId, + tx: trx }); return kmsDecryptor({ @@ -610,13 +817,70 @@ export const kmsServiceFactory = ({ } }; + const $getBasicEncryptionKey = () => { + const encryptionKey = envConfig.ENCRYPTION_KEY || envConfig.ROOT_ENCRYPTION_KEY; + const isBase64 = !envConfig.ENCRYPTION_KEY; + if (!encryptionKey) + throw new Error( + "Root encryption key not found for KMS service. Did you set the ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY environment variables?" + ); + + const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8"); + + return encryptionKeyBuffer; + }; + + const $decryptRootKey = async (kmsRootConfig: TKmsRootConfig) => { + // case 1: root key is encrypted with HSM + if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.HSM) { + const hsmIsActive = await hsmService.isActive(); + if (!hsmIsActive) { + throw new Error("Unable to decrypt root KMS key. HSM service is inactive. Did you configure the HSM?"); + } + + const decryptedKey = await hsmService.decrypt(kmsRootConfig.encryptedRootKey); + return decryptedKey; + } + + // case 2: root key is encrypted with software encryption + if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.Software) { + const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); + const encryptionKeyBuffer = $getBasicEncryptionKey(); + + return cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer); + } + + throw new Error(`Invalid root key encryption strategy: ${kmsRootConfig.encryptionStrategy}`); + }; + + const $encryptRootKey = async (plainKeyBuffer: Buffer, strategy: RootKeyEncryptionStrategy) => { + if (strategy === RootKeyEncryptionStrategy.HSM) { + const hsmIsActive = await hsmService.isActive(); + if (!hsmIsActive) { + throw new Error("Unable to encrypt root KMS key. HSM service is inactive. Did you configure the HSM?"); + } + const encrypted = await hsmService.encrypt(plainKeyBuffer); + return encrypted; + } + + if (strategy === RootKeyEncryptionStrategy.Software) { + const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); + const encryptionKeyBuffer = $getBasicEncryptionKey(); + + return cipher.encrypt(plainKeyBuffer, encryptionKeyBuffer); + } + + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new Error(`Invalid root key encryption strategy: ${strategy}`); + }; + // by keeping the decrypted data key in inner scope // none of the entities outside can interact directly or expose the data key // NOTICE: If changing here update migrations/utils/kms const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO, trx?: Knex) => { const dataKey = await $getDataKey(encryptionContext, trx); - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); return { encryptor: ({ plainText }: Pick) => { @@ -771,7 +1035,6 @@ export const kmsServiceFactory = ({ }, tx ); - return kmsDAL.findByIdWithAssociatedKms(key.id, tx); }); @@ -792,50 +1055,60 @@ export const kmsServiceFactory = ({ return { id, name, orgId, isExternal }; }; - // akhilmhdh: a copy of this is made in migrations/utils/kms const startService = async () => { - const appCfg = getConfig(); - // This will switch to a seal process and HMS flow in future - const encryptionKey = appCfg.ENCRYPTION_KEY || appCfg.ROOT_ENCRYPTION_KEY; - // if root key its base64 encoded - const isBase64 = !appCfg.ENCRYPTION_KEY; - if (!encryptionKey) throw new Error("Root encryption key not found for KMS service."); - const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8"); + const kmsRootConfig = await kmsRootConfigDAL.transaction(async (tx) => { + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.KmsRootKeyInit]); + // check if KMS root key was already generated and saved in DB + const existingRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID); + if (existingRootConfig) return existingRootConfig; - const lock = await keyStore.acquireLock([`KMS_ROOT_CFG_LOCK`], 3000, { retryCount: 3 }).catch(() => null); - if (!lock) { - await keyStore.waitTillReady({ - key: KMS_ROOT_CREATION_WAIT_KEY, - keyCheckCb: (val) => val === "true", - waitingCb: () => logger.info("KMS. Waiting for leader to finish creation of KMS Root Key") + logger.info("KMS: Generating new ROOT Key"); + const newRootKey = randomSecureBytes(32); + const encryptedRootKey = await $encryptRootKey(newRootKey, RootKeyEncryptionStrategy.Software).catch((err) => { + logger.error({ hsmEnabled: hsmService.isActive() }, "KMS: Failed to encrypt ROOT Key"); + throw err; }); + + const newRootConfig = await kmsRootConfigDAL.create({ + // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition + id: KMS_ROOT_CONFIG_UUID, + encryptedRootKey, + encryptionStrategy: RootKeyEncryptionStrategy.Software + }); + return newRootConfig; + }); + + const decryptedRootKey = await $decryptRootKey(kmsRootConfig); + + logger.info("KMS: Loading ROOT Key into Memory."); + + ROOT_ENCRYPTION_KEY = decryptedRootKey; + }; + + const updateEncryptionStrategy = async (strategy: RootKeyEncryptionStrategy) => { + const kmsRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID); + if (!kmsRootConfig) { + throw new NotFoundError({ message: "KMS root config not found" }); } - // check if KMS root key was already generated and saved in DB - const kmsRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID); - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); - if (kmsRootConfig) { - if (lock) await lock.release(); - logger.info("KMS: Encrypted ROOT Key found from DB. Decrypting."); - const decryptedRootKey = cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer); - // set the flag so that other instancen nodes can start - await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true"); - logger.info("KMS: Loading ROOT Key into Memory."); - ROOT_ENCRYPTION_KEY = decryptedRootKey; + if (kmsRootConfig.encryptionStrategy === strategy) { return; } - logger.info("KMS: Generating ROOT Key"); - const newRootKey = randomSecureBytes(32); - const encryptedRootKey = cipher.encrypt(newRootKey, encryptionKeyBuffer); - // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition - await kmsRootConfigDAL.create({ encryptedRootKey, id: KMS_ROOT_CONFIG_UUID }); + const decryptedRootKey = await $decryptRootKey(kmsRootConfig); + const encryptedRootKey = await $encryptRootKey(decryptedRootKey, strategy); - // set the flag so that other instancen nodes can start - await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true"); - logger.info("KMS: Saved and loaded ROOT Key into memory"); - if (lock) await lock.release(); - ROOT_ENCRYPTION_KEY = newRootKey; + if (!encryptedRootKey) { + logger.error("KMS: Failed to re-encrypt ROOT Key with selected strategy"); + throw new Error("Failed to re-encrypt ROOT Key with selected strategy"); + } + + await kmsRootConfigDAL.updateById(KMS_ROOT_CONFIG_UUID, { + encryptedRootKey, + encryptionStrategy: strategy + }); + + ROOT_ENCRYPTION_KEY = decryptedRootKey; }; return { @@ -849,11 +1122,17 @@ export const kmsServiceFactory = ({ encryptWithRootKey, decryptWithRootKey, getOrgKmsKeyId, + updateEncryptionStrategy, getProjectSecretManagerKmsKeyId, updateProjectSecretManagerKmsKey, getProjectKeyBackup, loadProjectKeyBackup, getKmsById, - createCipherPairWithDataKey + createCipherPairWithDataKey, + getKeyMaterial, + importKeyMaterial, + signWithKmsKey, + verifyWithKmsKey, + getPublicKey }; }; diff --git a/backend/src/services/kms/kms-types.ts b/backend/src/services/kms/kms-types.ts index 5d5b77a09..ca2401bb6 100644 --- a/backend/src/services/kms/kms-types.ts +++ b/backend/src/services/kms/kms-types.ts @@ -1,6 +1,7 @@ import { Knex } from "knex"; -import { SymmetricEncryption } from "@app/lib/crypto/cipher"; +import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher"; +import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign/types"; export enum KmsDataKey { Organization, @@ -13,6 +14,11 @@ export enum KmsType { Internal = "internal" } +export enum KmsKeyUsage { + ENCRYPT_DECRYPT = "encrypt-decrypt", + SIGN_VERIFY = "sign-verify" +} + export type TEncryptWithKmsDataKeyDTO = | { type: KmsDataKey.Organization; orgId: string } | { type: KmsDataKey.SecretManager; projectId: string }; @@ -25,7 +31,8 @@ export type TEncryptWithKmsDataKeyDTO = export type TGenerateKMSDTO = { orgId: string; projectId?: string; - encryptionAlgorithm?: SymmetricEncryption; + encryptionAlgorithm?: SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm; + keyUsage?: KmsKeyUsage; isReserved?: boolean; name?: string; description?: string; @@ -37,6 +44,25 @@ export type TEncryptWithKmsDTO = { plainText: Buffer; }; +export type TGetPublicKeyDTO = { + kmsId: string; +}; + +export type TSignWithKmsDTO = { + kmsId: string; + data: Buffer; + signingAlgorithm: SigningAlgorithm; + isDigest: boolean; +}; + +export type TVerifyWithKmsDTO = { + kmsId: string; + data: Buffer; + signature: Buffer; + signingAlgorithm: SigningAlgorithm; + isDigest: boolean; +}; + export type TEncryptionWithKeyDTO = { key: Buffer; plainText: Buffer; @@ -56,3 +82,21 @@ export type TUpdateProjectSecretManagerKmsKeyDTO = { projectId: string; kms: { type: KmsType.Internal } | { type: KmsType.External; kmsId: string }; }; + +export enum RootKeyEncryptionStrategy { + Software = "SOFTWARE", + HSM = "HSM" +} +export type TGetKeyMaterialDTO = { + kmsId: string; +}; + +export type TImportKeyMaterialDTO = { + key: Buffer; + algorithm: SymmetricKeyAlgorithm; + name?: string; + isReserved: boolean; + projectId: string; + orgId: string; + keyUsage: KmsKeyUsage; +}; diff --git a/backend/src/services/org-admin/org-admin-service.ts b/backend/src/services/org-admin/org-admin-service.ts index c9f792978..62767200c 100644 --- a/backend/src/services/org-admin/org-admin-service.ts +++ b/backend/src/services/org-admin/org-admin-service.ts @@ -12,17 +12,22 @@ import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; +import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TUserDALFactory } from "../user/user-dal"; import { TAccessProjectDTO, TListOrgProjectsDTO } from "./org-admin-types"; type TOrgAdminServiceFactoryDep = { permissionService: Pick; - projectDAL: Pick; - projectMembershipDAL: Pick; + projectDAL: Pick; + projectMembershipDAL: Pick< + TProjectMembershipDALFactory, + "findOne" | "create" | "transaction" | "delete" | "findAllProjectMembers" + >; projectKeyDAL: Pick; projectBotDAL: Pick; userDAL: Pick; projectUserMembershipRoleDAL: Pick; + smtpService: Pick; }; export type TOrgAdminServiceFactory = ReturnType; @@ -34,7 +39,8 @@ export const orgAdminServiceFactory = ({ projectKeyDAL, projectBotDAL, userDAL, - projectUserMembershipRoleDAL + projectUserMembershipRoleDAL, + smtpService }: TOrgAdminServiceFactoryDep) => { const listOrgProjects = async ({ actor, @@ -89,7 +95,7 @@ export const orgAdminServiceFactory = ({ OrgPermissionSubjects.AdminConsole ); - const project = await projectDAL.findById(projectId); + const project = await projectDAL.findOne({ id: projectId, orgId: actorOrgId }); if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` }); if (project.version === ProjectVersion.V1) { @@ -184,6 +190,23 @@ export const orgAdminServiceFactory = ({ ); return newProjectMembership; }); + + const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId); + const filteredProjectMembers = projectMembers + .filter( + (member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin) && member.userId !== actorId + ) + .map((el) => el.user.email!); + + await smtpService.sendMail({ + template: SmtpTemplates.OrgAdminProjectDirectAccess, + recipients: filteredProjectMembers, + subjectLine: "Organization Admin Project Direct Access Issued", + substitutions: { + projectName: project.name, + email: projectMembers.find((el) => el.userId === actorId)?.user?.username + } + }); return { isExistingMember: false, membership: updatedMembership }; }; diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index 24f1d55b0..54b0e1b0f 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -2,6 +2,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { + OrgMembershipRole, TableName, TOrganizations, TOrganizationsInsert, @@ -14,6 +15,8 @@ import { DatabaseError } from "@app/lib/errors"; import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt, withTransaction } from "@app/lib/knex"; import { generateKnexQueryFromScim } from "@app/lib/knex/scim"; +import { OrgAuthMethod } from "./org-types"; + export type TOrgDALFactory = ReturnType; export const orgDALFactory = (db: TDbClient) => { @@ -21,15 +24,82 @@ export const orgDALFactory = (db: TDbClient) => { const findOrgById = async (orgId: string) => { try { - const org = await db.replicaNode()(TableName.Organization).where({ id: orgId }).first(); + const org = (await db + .replicaNode()(TableName.Organization) + .where({ [`${TableName.Organization}.id` as "id"]: orgId }) + .leftJoin(TableName.SamlConfig, (qb) => { + qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn( + `${TableName.SamlConfig}.isActive`, + "=", + db.raw("true") + ); + }) + .leftJoin(TableName.OidcConfig, (qb) => { + qb.on(`${TableName.OidcConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn( + `${TableName.OidcConfig}.isActive`, + "=", + db.raw("true") + ); + }) + .select(selectAllTableCols(TableName.Organization)) + .select( + db.raw(` + CASE + WHEN ${TableName.SamlConfig}."orgId" IS NOT NULL THEN '${OrgAuthMethod.SAML}' + WHEN ${TableName.OidcConfig}."orgId" IS NOT NULL THEN '${OrgAuthMethod.OIDC}' + ELSE '' + END as "orgAuthMethod" + `) + ) + .first()) as TOrganizations & { orgAuthMethod?: string }; + return org; } catch (error) { throw new DatabaseError({ error, name: "Find org by id" }); } }; + const findOrgBySlug = async (orgSlug: string) => { + try { + const org = (await db + .replicaNode()(TableName.Organization) + .where({ [`${TableName.Organization}.slug` as "slug"]: orgSlug }) + .leftJoin(TableName.SamlConfig, (qb) => { + qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn( + `${TableName.SamlConfig}.isActive`, + "=", + db.raw("true") + ); + }) + .leftJoin(TableName.OidcConfig, (qb) => { + qb.on(`${TableName.OidcConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn( + `${TableName.OidcConfig}.isActive`, + "=", + db.raw("true") + ); + }) + .select(selectAllTableCols(TableName.Organization)) + .select( + db.raw(` + CASE + WHEN ${TableName.SamlConfig}."orgId" IS NOT NULL THEN '${OrgAuthMethod.SAML}' + WHEN ${TableName.OidcConfig}."orgId" IS NOT NULL THEN '${OrgAuthMethod.OIDC}' + ELSE '' + END as "orgAuthMethod" + `) + ) + .first()) as TOrganizations & { orgAuthMethod?: string }; + + return org; + } catch (error) { + throw new DatabaseError({ error, name: "Find org by slug" }); + } + }; + // special query - const findAllOrgsByUserId = async (userId: string): Promise<(TOrganizations & { orgAuthMethod: string })[]> => { + const findAllOrgsByUserId = async ( + userId: string + ): Promise<(TOrganizations & { orgAuthMethod: string; userRole: string })[]> => { try { const org = (await db .replicaNode()(TableName.OrgMembership) @@ -50,15 +120,16 @@ export const orgDALFactory = (db: TDbClient) => { ); }) .select(selectAllTableCols(TableName.Organization)) + .select(db.ref("role").withSchema(TableName.OrgMembership).as("userRole")) .select( db.raw(` - CASE + CASE WHEN ${TableName.SamlConfig}."orgId" IS NOT NULL THEN 'saml' WHEN ${TableName.OidcConfig}."orgId" IS NOT NULL THEN 'oidc' ELSE '' END as "orgAuthMethod" `) - )) as (TOrganizations & { orgAuthMethod: string })[]; + )) as (TOrganizations & { orgAuthMethod: string; userRole: string })[]; return org; } catch (error) { @@ -146,9 +217,8 @@ export const orgDALFactory = (db: TDbClient) => { const findOrgMembersByUsername = async (orgId: string, usernames: string[], tx?: Knex) => { try { - const conn = tx || db; + const conn = tx || db.replicaNode(); const members = await conn(TableName.OrgMembership) - // .replicaNode()(TableName.OrgMembership) .where(`${TableName.OrgMembership}.orgId`, orgId) .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) .leftJoin( @@ -181,6 +251,43 @@ export const orgDALFactory = (db: TDbClient) => { } }; + const findOrgMembersByRole = async (orgId: string, role: OrgMembershipRole, tx?: Knex) => { + try { + const conn = tx || db.replicaNode(); + const members = await conn(TableName.OrgMembership) + .where(`${TableName.OrgMembership}.orgId`, orgId) + .where(`${TableName.OrgMembership}.role`, role) + .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .leftJoin( + TableName.UserEncryptionKey, + `${TableName.UserEncryptionKey}.userId`, + `${TableName.Users}.id` + ) + .select( + conn.ref("id").withSchema(TableName.OrgMembership), + conn.ref("inviteEmail").withSchema(TableName.OrgMembership), + conn.ref("orgId").withSchema(TableName.OrgMembership), + conn.ref("role").withSchema(TableName.OrgMembership), + conn.ref("roleId").withSchema(TableName.OrgMembership), + conn.ref("status").withSchema(TableName.OrgMembership), + conn.ref("username").withSchema(TableName.Users), + conn.ref("email").withSchema(TableName.Users), + conn.ref("firstName").withSchema(TableName.Users), + conn.ref("lastName").withSchema(TableName.Users), + conn.ref("id").withSchema(TableName.Users).as("userId"), + conn.ref("publicKey").withSchema(TableName.UserEncryptionKey) + ) + .where({ isGhost: false }); + + return members.map(({ username, email, firstName, lastName, userId, publicKey, ...data }) => ({ + ...data, + user: { username, email, firstName, lastName, id: userId, publicKey } + })); + } catch (error) { + throw new DatabaseError({ error, name: "Find org members by role" }); + } + }; + const findOrgGhostUser = async (orgId: string) => { try { const member = await db @@ -398,9 +505,11 @@ export const orgDALFactory = (db: TDbClient) => { findAllOrgMembers, countAllOrgMembers, findOrgById, + findOrgBySlug, findAllOrgsByUserId, ghostUserExists, findOrgMembersByUsername, + findOrgMembersByRole, findOrgGhostUser, create, updateById, diff --git a/backend/src/services/org/org-schema.ts b/backend/src/services/org/org-schema.ts new file mode 100644 index 000000000..2aa793c04 --- /dev/null +++ b/backend/src/services/org/org-schema.ts @@ -0,0 +1,21 @@ +import { OrganizationsSchema } from "@app/db/schemas"; + +export const sanitizedOrganizationSchema = OrganizationsSchema.pick({ + id: true, + name: true, + customerId: true, + slug: true, + createdAt: true, + updatedAt: true, + authEnforced: true, + scimEnabled: true, + kmsDefaultKeyId: true, + defaultMembershipRole: true, + enforceMfa: true, + selectedMfaMethod: true, + allowSecretSharingOutsideOrganization: true, + shouldUseNewPrivilegeSystem: true, + privilegeUpgradeInitiatedByUsername: true, + privilegeUpgradeInitiatedAt: true, + bypassOrgAuthEnabled: true +}); diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 3d74f80b6..3a6373575 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -5,6 +5,7 @@ import jwt from "jsonwebtoken"; import { Knex } from "knex"; import { + ActionProjectType, OrgMembershipRole, OrgMembershipStatus, ProjectMembershipRole, @@ -15,28 +16,44 @@ import { TProjectUserMembershipRolesInsert, TUsers } from "@app/db/schemas"; -import { TProjects } from "@app/db/schemas/projects"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { + OrgPermissionActions, + OrgPermissionGroupActions, + OrgPermissionSecretShareAction, + OrgPermissionSubjects +} from "@app/ee/services/permission/org-permission"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; import { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal"; import { getConfig } from "@app/lib/config/env"; import { generateAsymmetricKeyPair } from "@app/lib/crypto"; import { generateSymmetricKey, infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { generateUserSrpKeys } from "@app/lib/crypto/srp"; -import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { isDisposableEmail } from "@app/lib/validator"; +import { TQueueServiceFactory } from "@app/queue"; import { getDefaultOrgMembershipRoleForUpdateOrg } from "@app/services/org/org-role-fns"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; -import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type"; +import { TAuthLoginFactory } from "../auth/auth-login-service"; +import { ActorAuthMethod, ActorType, AuthMethod, AuthModeJwtTokenPayload, AuthTokenType } from "../auth/auth-type"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal"; @@ -48,6 +65,10 @@ import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; +import { TSecretDALFactory } from "../secret/secret-dal"; +import { fnDeleteProjectSecretReminders } from "../secret/secret-fns"; +import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; +import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TUserDALFactory } from "../user/user-dal"; import { TIncidentContactsDALFactory } from "./incident-contacts-dal"; @@ -63,13 +84,18 @@ import { TGetOrgMembershipDTO, TInviteUserToOrgDTO, TListProjectMembershipsByOrgMembershipIdDTO, + TResendOrgMemberInvitationDTO, TUpdateOrgDTO, TUpdateOrgMembershipDTO, + TUpgradePrivilegeSystemDTO, TVerifyUserToOrgDTO } from "./org-types"; type TOrgServiceFactoryDep = { userAliasDAL: Pick; + secretDAL: Pick; + secretV2BridgeDAL: Pick; + folderDAL: Pick; orgDAL: TOrgDALFactory; orgBotDAL: TOrgBotDALFactory; orgRoleDAL: TOrgRoleDALFactory; @@ -84,8 +110,8 @@ type TOrgServiceFactoryDep = { projectKeyDAL: Pick; orgMembershipDAL: Pick; incidentContactDAL: TIncidentContactsDALFactory; - samlConfigDAL: Pick; - oidcConfigDAL: Pick; + samlConfigDAL: Pick; + oidcConfigDAL: Pick; smtpService: TSmtpService; tokenService: TAuthTokenServiceFactory; permissionService: TPermissionServiceFactory; @@ -98,6 +124,8 @@ type TOrgServiceFactoryDep = { projectBotDAL: Pick; projectUserMembershipRoleDAL: Pick; projectBotService: Pick; + queueService: Pick; + loginService: Pick; }; export type TOrgServiceFactory = ReturnType; @@ -105,6 +133,9 @@ export type TOrgServiceFactory = ReturnType; export const orgServiceFactory = ({ userAliasDAL, orgDAL, + secretDAL, + secretV2BridgeDAL, + folderDAL, userDAL, groupDAL, orgRoleDAL, @@ -125,7 +156,9 @@ export const orgServiceFactory = ({ projectBotDAL, projectUserMembershipRoleDAL, identityMetadataDAL, - projectBotService + projectBotService, + queueService, + loginService }: TOrgServiceFactoryDep) => { /* * Get organization details by the organization id @@ -166,7 +199,7 @@ export const orgServiceFactory = ({ const getOrgGroups = async ({ actor, actorId, orgId, actorAuthMethod, actorOrgId }: TGetOrgGroupsDTO) => { const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); const groups = await groupDAL.findByOrgId(orgId); return groups; }; @@ -187,26 +220,27 @@ export const orgServiceFactory = ({ return members; }; - const findAllWorkspaces = async ({ actor, actorId, orgId }: TFindAllWorkspacesDTO) => { - const organizationWorkspaceIds = new Set((await projectDAL.find({ orgId })).map((workspace) => workspace.id)); - - let workspaces: (TProjects & { organization: string } & { - environments: { - id: string; - slug: string; - name: string; - }[]; - })[]; - - if (actor === ActorType.USER) { - workspaces = await projectDAL.findAllProjects(actorId); - } else if (actor === ActorType.IDENTITY) { - workspaces = await projectDAL.findAllProjectsByIdentity(actorId); - } else { - throw new BadRequestError({ message: "Invalid actor type" }); + const findOrgBySlug = async (slug: string) => { + const org = await orgDAL.findOrgBySlug(slug); + if (!org) { + throw new NotFoundError({ message: `Organization with slug '${slug}' not found` }); } - return workspaces.filter((workspace) => organizationWorkspaceIds.has(workspace.id)); + return org; + }; + + const findAllWorkspaces = async ({ actor, actorId, orgId, type }: TFindAllWorkspacesDTO) => { + if (actor === ActorType.USER) { + const workspaces = await projectDAL.findUserProjects(actorId, orgId, type || "all"); + return workspaces; + } + + if (actor === ActorType.IDENTITY) { + const workspaces = await projectDAL.findAllProjectsByIdentity(actorId, type); + return workspaces; + } + + throw new BadRequestError({ message: "Invalid actor type" }); }; const addGhostUser = async (orgId: string, tx?: Knex) => { @@ -259,6 +293,45 @@ export const orgServiceFactory = ({ }; }; + const upgradePrivilegeSystem = async ({ + actorId, + actorOrgId, + actorAuthMethod, + orgId + }: TUpgradePrivilegeSystemDTO) => { + const { membership } = await permissionService.getUserOrgPermission(actorId, orgId, actorAuthMethod, actorOrgId); + + if (membership.role !== OrgMembershipRole.Admin) { + throw new ForbiddenRequestError({ + message: "Insufficient privileges - only the organization admin can upgrade the privilege system." + }); + } + + return orgDAL.transaction(async (tx) => { + const org = await orgDAL.findById(actorOrgId, tx); + if (org.shouldUseNewPrivilegeSystem) { + throw new BadRequestError({ + message: "Privilege system already upgraded" + }); + } + + const user = await userDAL.findById(actorId, tx); + if (!user) { + throw new NotFoundError({ message: `User with ID '${actorId}' not found` }); + } + + return orgDAL.updateById( + actorOrgId, + { + shouldUseNewPrivilegeSystem: true, + privilegeUpgradeInitiatedAt: new Date(), + privilegeUpgradeInitiatedByUsername: user.username + }, + tx + ); + }); + }; + /* * Update organization details * */ @@ -268,13 +341,30 @@ export const orgServiceFactory = ({ actorOrgId, actorAuthMethod, orgId, - data: { name, slug, authEnforced, scimEnabled, defaultMembershipRoleSlug, enforceMfa } + data: { + name, + slug, + authEnforced, + scimEnabled, + defaultMembershipRoleSlug, + enforceMfa, + selectedMfaMethod, + allowSecretSharingOutsideOrganization, + bypassOrgAuthEnabled + } }: TUpdateOrgDTO) => { const appCfg = getConfig(); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); + if (allowSecretSharingOutsideOrganization !== undefined) { + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionSecretShareAction.ManageSettings, + OrgPermissionSubjects.SecretShare + ); + } const plan = await licenseService.getPlan(orgId); + const currentOrg = await orgDAL.findOrgById(actorOrgId); if (enforceMfa !== undefined) { if (!plan.enforceMfa) { @@ -305,16 +395,41 @@ export const orgServiceFactory = ({ "Failed to enable/disable SCIM provisioning due to plan restriction. Upgrade plan to enable/disable SCIM provisioning." }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim); + if (scimEnabled && !currentOrg.orgAuthMethod) { + throw new BadRequestError({ + message: "Cannot enable SCIM when neither SAML or OIDC is configured." + }); + } } if (authEnforced) { - const samlCfg = await samlConfigDAL.findEnforceableSamlCfg(orgId); - const oidcCfg = await oidcConfigDAL.findEnforceableOidcCfg(orgId); + const samlCfg = await samlConfigDAL.findOne({ + orgId, + isActive: true + }); + const oidcCfg = await oidcConfigDAL.findOne({ + orgId, + isActive: true + }); if (!samlCfg && !oidcCfg) throw new NotFoundError({ message: `SAML or OIDC configuration for organization with ID '${orgId}' not found` }); + + if (samlCfg && !samlCfg.lastUsed) { + throw new BadRequestError({ + message: + "To apply the new SAML auth enforcement, please log in via SAML at least once. This step is required to enforce SAML-based authentication." + }); + } + + if (oidcCfg && !oidcCfg.lastUsed) { + throw new BadRequestError({ + message: + "To apply the new OIDC auth enforcement, please log in via OIDC at least once. This step is required to enforce OIDC-based authentication." + }); + } } let defaultMembershipRole: string | undefined; @@ -333,7 +448,10 @@ export const orgServiceFactory = ({ authEnforced, scimEnabled, defaultMembershipRole, - enforceMfa + enforceMfa, + selectedMfaMethod, + allowSecretSharingOutsideOrganization, + bypassOrgAuthEnabled }); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); return org; @@ -412,24 +530,88 @@ export const orgServiceFactory = ({ /* * Delete organization by id * */ - const deleteOrganizationById = async ( - userId: string, - orgId: string, - actorAuthMethod: ActorAuthMethod, - actorOrgId: string | undefined - ) => { + const deleteOrganizationById = async ({ + userId, + authorizationHeader, + userAgentHeader, + ipAddress, + orgId, + actorAuthMethod, + actorOrgId + }: { + userId: string; + authorizationHeader?: string; + userAgentHeader?: string; + ipAddress: string; + orgId: string; + actorAuthMethod: ActorAuthMethod; + actorOrgId: string | undefined; + }) => { const { membership } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); - if ((membership.role as OrgMembershipRole) !== OrgMembershipRole.Admin) + if ((membership.role as OrgMembershipRole) !== OrgMembershipRole.Admin) { throw new ForbiddenRequestError({ name: "DeleteOrganizationById", message: "Insufficient privileges" }); - - const organization = await orgDAL.deleteById(orgId); - if (organization.customerId) { - await licenseService.removeOrgCustomer(organization.customerId); } - return organization; + + if (!authorizationHeader) { + throw new UnauthorizedError({ name: "Authorization header not set on request." }); + } + + if (!userAgentHeader) { + throw new BadRequestError({ name: "User agent not set on request." }); + } + + const cfg = getConfig(); + const authToken = authorizationHeader.replace("Bearer ", ""); + + const decodedToken = jwt.verify(authToken, cfg.AUTH_SECRET) as AuthModeJwtTokenPayload; + if (!decodedToken.authMethod) throw new UnauthorizedError({ name: "Auth method not found on existing token" }); + + const response = await orgDAL.transaction(async (tx) => { + const projects = await projectDAL.find({ orgId }, { tx }); + + for await (const project of projects) { + await fnDeleteProjectSecretReminders(project.id, { + secretDAL, + secretV2BridgeDAL, + queueService, + projectBotService, + folderDAL + }); + } + + const deletedOrg = await orgDAL.deleteById(orgId, tx); + + if (deletedOrg.customerId) { + await licenseService.removeOrgCustomer(deletedOrg.customerId); + } + + // Generate new tokens without the organization ID present + const user = await userDAL.findById(userId, tx); + const { access: accessToken, refresh: refreshToken } = await loginService.generateUserTokens( + { + user, + authMethod: decodedToken.authMethod, + ip: ipAddress, + userAgent: userAgentHeader, + isMfaVerified: decodedToken.isMfaVerified, + mfaMethod: decodedToken.mfaMethod + }, + tx + ); + + return { + organization: deletedOrg, + tokens: { + accessToken, + refreshToken + } + }; + }); + + return response; }; /* * Org membership management @@ -496,6 +678,66 @@ export const orgServiceFactory = ({ }); return membership; }; + + const resendOrgMemberInvitation = async ({ + orgId, + actorId, + actor, + actorAuthMethod, + actorOrgId, + membershipId + }: TResendOrgMemberInvitationDTO) => { + const appCfg = getConfig(); + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member); + + const org = await orgDAL.findOrgById(orgId); + + const [inviteeOrgMembership] = await orgDAL.findMembership({ + [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId, + [`${TableName.OrgMembership}.id` as "id"]: membershipId + }); + + if (inviteeOrgMembership.status !== OrgMembershipStatus.Invited) { + throw new BadRequestError({ + message: "Organization invitation already accepted" + }); + } + + const token = await tokenService.createTokenForUser({ + type: TokenType.TOKEN_EMAIL_ORG_INVITATION, + userId: inviteeOrgMembership.userId, + orgId + }); + + if (!appCfg.isSmtpConfigured) { + return { + signupToken: { + email: inviteeOrgMembership.email as string, + link: `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${inviteeOrgMembership.email}&organization_id=${org?.id}` + } + }; + } + + await smtpService.sendMail({ + template: SmtpTemplates.OrgInvite, + subjectLine: "Infisical organization invitation", + recipients: [inviteeOrgMembership.email as string], + substitutions: { + inviterFirstName: inviteeOrgMembership.firstName, + inviterUsername: inviteeOrgMembership.email, + organizationName: org?.name, + email: inviteeOrgMembership.email, + organizationId: org?.id.toString(), + token, + callback_url: `${appCfg.SITE_URL}/signupinvite` + } + }); + + return { signupToken: undefined }; + }; + /* * Invite user to organization */ @@ -539,6 +781,7 @@ export const orgServiceFactory = ({ } }) : []; + if (projectsToInvite.length !== invitedProjects?.length) { throw new ForbiddenRequestError({ message: "Access denied to one or more of the specified projects" @@ -686,15 +929,16 @@ export const orgServiceFactory = ({ // if there exist no project membership we set is as given by the request for await (const project of projectsToInvite) { const projectId = project.id; - const { permission: projectPermission } = await permissionService.getProjectPermission( + const { permission: projectPermission, membership } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(projectPermission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionMemberActions.Create, ProjectPermissionSub.Member ); const existingMembers = await projectMembershipDAL.find( @@ -717,6 +961,34 @@ export const orgServiceFactory = ({ ProjectMembershipRole.Member ]; + for await (const invitedRole of invitedProjectRoles) { + const { permission: rolePermission } = await permissionService.getProjectPermissionByRole( + invitedRole, + projectId + ); + + if (invitedRole !== ProjectMembershipRole.NoAccess) { + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionMemberActions.GrantPrivileges, + ProjectPermissionSub.Member, + projectPermission, + rolePermission + ); + + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to invite user to the project", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionMemberActions.GrantPrivileges, + ProjectPermissionSub.Member + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + } + const customProjectRoles = invitedProjectRoles.filter( (role) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole) ); @@ -850,9 +1122,9 @@ export const orgServiceFactory = ({ const sanitizedProjectMembershipRoles: TProjectUserMembershipRolesInsert[] = []; invitedProjectRoles.forEach((projectRole) => { const isCustomRole = Boolean(customRolesGroupBySlug?.[projectRole]?.[0]); - projectMemberships.forEach((membership) => { + projectMemberships.forEach((membershipEntry) => { sanitizedProjectMembershipRoles.push({ - projectMembershipId: membership.id, + projectMembershipId: membershipEntry.id, role: isCustomRole ? ProjectMembershipRole.Custom : projectRole, customRoleId: customRolesGroupBySlug[projectRole] ? customRolesGroupBySlug[projectRole][0].id : null }); @@ -1131,6 +1403,9 @@ export const orgServiceFactory = ({ createIncidentContact, deleteIncidentContact, getOrgGroups, - listProjectMembershipsByOrgMembershipId + listProjectMembershipsByOrgMembershipId, + findOrgBySlug, + resendOrgMemberInvitation, + upgradePrivilegeSystem }; }; diff --git a/backend/src/services/org/org-types.ts b/backend/src/services/org/org-types.ts index 5b44eeea5..8a1698015 100644 --- a/backend/src/services/org/org-types.ts +++ b/backend/src/services/org/org-types.ts @@ -1,6 +1,7 @@ +import { ProjectType } from "@app/db/schemas"; import { TOrgPermission } from "@app/lib/types"; -import { ActorAuthMethod, ActorType } from "../auth/auth-type"; +import { ActorAuthMethod, ActorType, MfaMethod } from "../auth/auth-type"; export type TUpdateOrgMembershipDTO = { userId: string; @@ -34,6 +35,10 @@ export type TInviteUserToOrgDTO = { }[]; } & TOrgPermission; +export type TResendOrgMemberInvitationDTO = { + membershipId: string; +} & TOrgPermission; + export type TVerifyUserToOrgDTO = { email: string; orgId: string; @@ -55,6 +60,7 @@ export type TFindAllWorkspacesDTO = { actorOrgId: string | undefined; actorAuthMethod: ActorAuthMethod; orgId: string; + type?: ProjectType; }; export type TUpdateOrgDTO = { @@ -65,11 +71,21 @@ export type TUpdateOrgDTO = { scimEnabled: boolean; defaultMembershipRoleSlug: string; enforceMfa: boolean; + selectedMfaMethod: MfaMethod; + allowSecretSharingOutsideOrganization: boolean; + bypassOrgAuthEnabled: boolean; }>; } & TOrgPermission; +export type TUpgradePrivilegeSystemDTO = Omit; + export type TGetOrgGroupsDTO = TOrgPermission; export type TListProjectMembershipsByOrgMembershipIdDTO = { orgMembershipId: string; } & TOrgPermission; + +export enum OrgAuthMethod { + OIDC = "oidc", + SAML = "saml" +} diff --git a/backend/src/services/pki-alert/pki-alert-service.ts b/backend/src/services/pki-alert/pki-alert-service.ts index 1e7d26825..0ddc0f3ae 100644 --- a/backend/src/services/pki-alert/pki-alert-service.ts +++ b/backend/src/services/pki-alert/pki-alert-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { ActionProjectType, ProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; @@ -8,6 +9,7 @@ import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-colle import { pkiItemTypeToNameMap } from "@app/services/pki-collection/pki-collection-types"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; +import { TProjectDALFactory } from "../project/project-dal"; import { TPkiAlertDALFactory } from "./pki-alert-dal"; import { TCreateAlertDTO, TDeleteAlertDTO, TGetAlertByIdDTO, TUpdateAlertDTO } from "./pki-alert-types"; @@ -19,6 +21,7 @@ type TPkiAlertServiceFactoryDep = { pkiCollectionDAL: Pick; permissionService: Pick; smtpService: Pick; + projectDAL: Pick; }; export type TPkiAlertServiceFactory = ReturnType; @@ -27,7 +30,8 @@ export const pkiAlertServiceFactory = ({ pkiAlertDAL, pkiCollectionDAL, permissionService, - smtpService + smtpService, + projectDAL }: TPkiAlertServiceFactoryDep) => { const sendPkiItemExpiryNotices = async () => { const allAlertItems = await pkiAlertDAL.getExpiringPkiCollectionItemsForAlerting(); @@ -63,7 +67,7 @@ export const pkiAlertServiceFactory = ({ }; const createPkiAlert = async ({ - projectId, + projectId: preSplitProjectId, name, pkiCollectionId, alertBeforeDays, @@ -73,13 +77,23 @@ export const pkiAlertServiceFactory = ({ actor, actorOrgId }: TCreateAlertDTO) => { - const { permission } = await permissionService.getProjectPermission( + let projectId = preSplitProjectId; + const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId( + projectId, + ProjectType.CertificateManager + ); + if (certManagerProjectFromSplit) { + projectId = certManagerProjectFromSplit.id; + } + + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts); @@ -102,13 +116,14 @@ export const pkiAlertServiceFactory = ({ const alert = await pkiAlertDAL.findById(alertId); if (!alert) throw new NotFoundError({ message: `Alert with ID '${alertId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - alert.projectId, + projectId: alert.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts); return alert; @@ -128,13 +143,14 @@ export const pkiAlertServiceFactory = ({ let alert = await pkiAlertDAL.findById(alertId); if (!alert) throw new NotFoundError({ message: `Alert with ID '${alertId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - alert.projectId, + projectId: alert.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts); @@ -160,13 +176,14 @@ export const pkiAlertServiceFactory = ({ let alert = await pkiAlertDAL.findById(alertId); if (!alert) throw new NotFoundError({ message: `Alert with ID '${alertId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - alert.projectId, + projectId: alert.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts); alert = await pkiAlertDAL.deleteById(alertId); diff --git a/backend/src/services/pki-collection/pki-collection-service.ts b/backend/src/services/pki-collection/pki-collection-service.ts index ef849c54f..bee3ee621 100644 --- a/backend/src/services/pki-collection/pki-collection-service.ts +++ b/backend/src/services/pki-collection/pki-collection-service.ts @@ -1,12 +1,13 @@ import { ForbiddenError } from "@casl/ability"; -import { TPkiCollectionItems } from "@app/db/schemas"; +import { ActionProjectType, ProjectType, TPkiCollectionItems } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; +import { TProjectDALFactory } from "../project/project-dal"; import { TPkiCollectionDALFactory } from "./pki-collection-dal"; import { transformPkiCollectionItem } from "./pki-collection-fns"; import { TPkiCollectionItemDALFactory } from "./pki-collection-item-dal"; @@ -30,6 +31,7 @@ type TPkiCollectionServiceFactoryDep = { certificateAuthorityDAL: Pick; certificateDAL: Pick; permissionService: Pick; + projectDAL: Pick; }; export type TPkiCollectionServiceFactory = ReturnType; @@ -39,24 +41,35 @@ export const pkiCollectionServiceFactory = ({ pkiCollectionItemDAL, certificateAuthorityDAL, certificateDAL, - permissionService + permissionService, + projectDAL }: TPkiCollectionServiceFactoryDep) => { const createPkiCollection = async ({ name, description, - projectId, + projectId: preSplitProjectId, actorId, actorAuthMethod, actor, actorOrgId }: TCreatePkiCollectionDTO) => { - const { permission } = await permissionService.getProjectPermission( + let projectId = preSplitProjectId; + const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId( + projectId, + ProjectType.CertificateManager + ); + if (certManagerProjectFromSplit) { + projectId = certManagerProjectFromSplit.id; + } + + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, @@ -82,13 +95,14 @@ export const pkiCollectionServiceFactory = ({ const pkiCollection = await pkiCollectionDAL.findById(collectionId); if (!pkiCollection) throw new NotFoundError({ message: `PKI collection with ID '${collectionId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - pkiCollection.projectId, + projectId: pkiCollection.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections); return pkiCollection; @@ -106,13 +120,14 @@ export const pkiCollectionServiceFactory = ({ let pkiCollection = await pkiCollectionDAL.findById(collectionId); if (!pkiCollection) throw new NotFoundError({ message: `PKI collection with ID '${collectionId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - pkiCollection.projectId, + projectId: pkiCollection.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections); pkiCollection = await pkiCollectionDAL.updateById(collectionId, { @@ -133,13 +148,14 @@ export const pkiCollectionServiceFactory = ({ let pkiCollection = await pkiCollectionDAL.findById(collectionId); if (!pkiCollection) throw new NotFoundError({ message: `PKI collection with ID '${collectionId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - pkiCollection.projectId, + projectId: pkiCollection.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, @@ -162,13 +178,14 @@ export const pkiCollectionServiceFactory = ({ const pkiCollection = await pkiCollectionDAL.findById(collectionId); if (!pkiCollection) throw new NotFoundError({ message: `PKI collection with ID '${collectionId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - pkiCollection.projectId, + projectId: pkiCollection.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections); @@ -205,13 +222,14 @@ export const pkiCollectionServiceFactory = ({ const pkiCollection = await pkiCollectionDAL.findById(collectionId); if (!pkiCollection) throw new NotFoundError({ message: `PKI collection with ID '${collectionId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - pkiCollection.projectId, + projectId: pkiCollection.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, @@ -298,13 +316,14 @@ export const pkiCollectionServiceFactory = ({ if (!pkiCollectionItem) throw new NotFoundError({ message: `PKI collection item with ID '${itemId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - pkiCollection.projectId, + projectId: pkiCollection.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, diff --git a/backend/src/services/project-bot/project-bot-service.ts b/backend/src/services/project-bot/project-bot-service.ts index 5d7f78c1b..dc69cbf99 100644 --- a/backend/src/services/project-bot/project-bot-service.ts +++ b/backend/src/services/project-bot/project-bot-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { ProjectVersion } from "@app/db/schemas"; +import { ActionProjectType, ProjectVersion } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { generateAsymmetricKeyPair } from "@app/lib/crypto"; @@ -41,13 +41,14 @@ export const projectBotServiceFactory = ({ botKey, publicKey }: TFindBotByProjectIdDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const bot = await projectBotDAL.transaction(async (tx) => { @@ -107,13 +108,14 @@ export const projectBotServiceFactory = ({ const bot = await projectBotDAL.findById(botId); if (!bot) throw new NotFoundError({ message: `Project bot with ID '${botId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - bot.projectId, + projectId: bot.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations); const project = await projectBotDAL.findProjectByBotId(botId); diff --git a/backend/src/services/project-env/project-env-service.ts b/backend/src/services/project-env/project-env-service.ts index a54e8de43..f9935df54 100644 --- a/backend/src/services/project-env/project-env-service.ts +++ b/backend/src/services/project-env/project-env-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { ActionProjectType } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -41,13 +42,14 @@ export const projectEnvServiceFactory = ({ name, slug }: TCreateEnvDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Environments); const lock = await keyStore @@ -129,13 +131,14 @@ export const projectEnvServiceFactory = ({ id, position }: TUpdateEnvDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments); const lock = await keyStore @@ -192,13 +195,14 @@ export const projectEnvServiceFactory = ({ }; const deleteEnvironment = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteEnvDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments); const lock = await keyStore @@ -247,13 +251,14 @@ export const projectEnvServiceFactory = ({ }); } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - environment.projectId, + projectId: environment.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); diff --git a/backend/src/services/project-key/project-key-service.ts b/backend/src/services/project-key/project-key-service.ts index 70c8365ee..8ce2569a0 100644 --- a/backend/src/services/project-key/project-key-service.ts +++ b/backend/src/services/project-key/project-key-service.ts @@ -1,7 +1,8 @@ import { ForbiddenError } from "@casl/ability"; +import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError } from "@app/lib/errors"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; @@ -31,14 +32,15 @@ export const projectKeyServiceFactory = ({ nonce, encryptedKey }: TUploadProjectKeyDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); const receiverMembership = await projectMembershipDAL.findOne({ userId: receiverId, @@ -60,7 +62,14 @@ export const projectKeyServiceFactory = ({ actorOrgId, actorAuthMethod }: TGetLatestProjectKeyDTO) => { - await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId); + await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.Any + }); const latestKey = await projectKeyDAL.findLatestProjectKey(actorId, projectId); return latestKey; }; @@ -72,14 +81,15 @@ export const projectKeyServiceFactory = ({ actorAuthMethod, projectId }: TGetLatestProjectKeyDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); return projectKeyDAL.findAllProjectUserPubKeys(projectId); }; diff --git a/backend/src/services/project-membership/project-membership-dal.ts b/backend/src/services/project-membership/project-membership-dal.ts index bfd0c6f85..61b703e70 100644 --- a/backend/src/services/project-membership/project-membership-dal.ts +++ b/backend/src/services/project-membership/project-membership-dal.ts @@ -217,20 +217,33 @@ export const projectMembershipDALFactory = (db: TDbClient) => { db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole), db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole), db.ref("name").as("projectName").withSchema(TableName.Project), - db.ref("id").as("projectId").withSchema(TableName.Project) + db.ref("id").as("projectId").withSchema(TableName.Project), + db.ref("type").as("projectType").withSchema(TableName.Project) ) .where({ isGhost: false }); const members = sqlNestRelationships({ data: docs, - parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, projectId, projectName }) => ({ + parentMapper: ({ + email, + firstName, + username, + lastName, + publicKey, + isGhost, + id, + projectId, + projectName, + projectType + }) => ({ id, userId, projectId, user: { email, username, firstName, lastName, id: userId, publicKey, isGhost }, project: { id: projectId, - name: projectName + name: projectName, + type: projectType } }), key: "id", diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index 74b830c6d..1d6ba969a 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -1,16 +1,19 @@ /* eslint-disable no-await-in-loop */ import { ForbiddenError } from "@casl/ability"; -import ms from "ms"; -import { ProjectMembershipRole, ProjectVersion, TableName } from "@app/db/schemas"; +import { ActionProjectType, ProjectMembershipRole, ProjectVersion, TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; -import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; +import { ms } from "@app/lib/ms"; import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal"; import { ActorType } from "../auth/auth-type"; @@ -78,14 +81,15 @@ export const projectMembershipServiceFactory = ({ includeGroupMembers, projectId }: TGetProjectMembershipDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId); @@ -121,14 +125,15 @@ export const projectMembershipServiceFactory = ({ projectId, username }: TGetProjectMembershipByUsernameDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); const [membership] = await projectMembershipDAL.findAllProjectMembers(projectId, { username }); if (!membership) throw new NotFoundError({ message: `Project membership not found for user '${username}'` }); @@ -143,14 +148,15 @@ export const projectMembershipServiceFactory = ({ projectId, id }: TGetProjectMembershipByIdDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); const [membership] = await projectMembershipDAL.findAllProjectMembers(projectId, { id }); if (!membership) throw new NotFoundError({ message: `Project membership not found for user ${id}` }); @@ -169,14 +175,15 @@ export const projectMembershipServiceFactory = ({ const project = await projectDAL.findById(projectId); if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Create, ProjectPermissionSub.Member); const orgMembers = await orgDAL.findMembership({ [`${TableName.OrgMembership}.orgId` as "orgId"]: project.orgId, $in: { @@ -249,14 +256,15 @@ export const projectMembershipServiceFactory = ({ membershipId, roles }: TUpdateProjectMembershipDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, membership } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); const membershipUser = await userDAL.findUserByProjectMembershipId(membershipId); if (membershipUser?.isGhost || membershipUser?.projectId !== projectId) { @@ -269,18 +277,33 @@ export const projectMembershipServiceFactory = ({ projectId ); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission); - - if (!hasRequiredPriviledges) { - throw new ForbiddenRequestError({ - message: `Failed to change to a more privileged role ${requestedRoleChange}` + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionMemberActions.GrantPrivileges, + ProjectPermissionSub.Member, + permission, + rolePermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + `Failed to change role ${requestedRoleChange}`, + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionMemberActions.GrantPrivileges, + ProjectPermissionSub.Member + ), + details: { missingPermissions: permissionBoundary.missingPermissions } }); - } } // validate custom roles input const customInputRoles = roles.filter( - ({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole) + ({ role }) => + !Object.values(ProjectMembershipRole) + // we don't want to include custom in this check; + // this unintentionally enables setting slug to custom which is reserved + .filter((r) => r !== ProjectMembershipRole.Custom) + .includes(role as ProjectMembershipRole) ); const hasCustomRole = Boolean(customInputRoles.length); if (hasCustomRole) { @@ -343,14 +366,15 @@ export const projectMembershipServiceFactory = ({ projectId, membershipId }: TDeleteProjectMembershipOldDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Delete, ProjectPermissionSub.Member); const member = await userDAL.findUserByProjectMembershipId(membershipId); @@ -378,14 +402,15 @@ export const projectMembershipServiceFactory = ({ emails, usernames }: TDeleteProjectMembershipsDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Delete, ProjectPermissionSub.Member); const project = await projectDAL.findById(projectId); @@ -398,7 +423,7 @@ export const projectMembershipServiceFactory = ({ const usernamesAndEmails = [...emails, ...usernames]; const projectMembers = await projectMembershipDAL.findMembershipsByUsername(projectId, [ - ...new Set(usernamesAndEmails.map((element) => element.toLowerCase())) + ...new Set(usernamesAndEmails.map((element) => element)) ]); if (projectMembers.length !== usernamesAndEmails.length) { diff --git a/backend/src/services/project-role/project-role-service.ts b/backend/src/services/project-role/project-role-service.ts index 55564bc17..fc2fb9319 100644 --- a/backend/src/services/project-role/project-role-service.ts +++ b/backend/src/services/project-role/project-role-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability"; import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; -import { ProjectMembershipRole, TableName } from "@app/db/schemas"; +import { ActionProjectType, ProjectMembershipRole, TableName } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -9,7 +9,8 @@ import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; -import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; +import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { ActorAuthMethod } from "../auth/auth-type"; import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; @@ -58,19 +59,23 @@ export const projectRoleServiceFactory = ({ projectId = filter.projectId; } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Role); const existingRole = await projectRoleDAL.findOne({ slug: data.slug, projectId }); if (existingRole) { throw new BadRequestError({ name: "Create Role", message: "Project role with same slug already exists" }); } + validateHandlebarTemplate("Project Role Create", JSON.stringify(data.permissions || []), { + allowedExpressions: (val) => val.includes("identity.") + }); const role = await projectRoleDAL.create({ ...data, projectId @@ -95,13 +100,14 @@ export const projectRoleServiceFactory = ({ projectId = filter.projectId; } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role); if (roleSlug !== "custom" && Object.values(ProjectMembershipRole).includes(roleSlug as ProjectMembershipRole)) { const predefinedRole = getPredefinedRoles(projectId, roleSlug as ProjectMembershipRole)[0]; @@ -117,13 +123,14 @@ export const projectRoleServiceFactory = ({ const projectRole = await projectRoleDAL.findById(roleId); if (!projectRole) throw new NotFoundError({ message: "Project role not found", name: "Delete role" }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - projectRole.projectId, + projectId: projectRole.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Role); if (data?.slug) { @@ -131,6 +138,9 @@ export const projectRoleServiceFactory = ({ if (existingRole && existingRole.id !== roleId) throw new BadRequestError({ name: "Update Role", message: "Project role with the same slug already exists" }); } + validateHandlebarTemplate("Project Role Update", JSON.stringify(data.permissions || []), { + allowedExpressions: (val) => val.includes("identity.") + }); const updatedRole = await projectRoleDAL.updateById(projectRole.id, { ...data, @@ -144,13 +154,14 @@ export const projectRoleServiceFactory = ({ const deleteRole = async ({ actor, actorId, actorAuthMethod, actorOrgId, roleId }: TDeleteRoleDTO) => { const projectRole = await projectRoleDAL.findById(roleId); if (!projectRole) throw new NotFoundError({ message: "Project role not found", name: "Delete role" }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - projectRole.projectId, + projectId: projectRole.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Role); const identityRole = await identityProjectMembershipRoleDAL.findOne({ customRoleId: roleId }); @@ -185,13 +196,14 @@ export const projectRoleServiceFactory = ({ projectId = filter.projectId; } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role); const customRoles = await projectRoleDAL.find( { projectId }, @@ -208,12 +220,13 @@ export const projectRoleServiceFactory = ({ actorAuthMethod: ActorAuthMethod, actorOrgId: string | undefined ) => { - const { permission, membership } = await permissionService.getUserProjectPermission( + const { permission, membership } = await permissionService.getUserProjectPermission({ userId, projectId, - actorAuthMethod, - actorOrgId - ); + authMethod: actorAuthMethod, + userOrgId: actorOrgId, + actionProjectType: ActionProjectType.Any + }); return { permissions: packRules(permission.rules), membership }; }; diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts index 4e7425326..43f1d57e4 100644 --- a/backend/src/services/project/project-dal.ts +++ b/backend/src/services/project/project-dal.ts @@ -1,23 +1,39 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { ProjectsSchema, ProjectUpgradeStatus, ProjectVersion, TableName, TProjectsUpdate } from "@app/db/schemas"; +import { + ProjectsSchema, + ProjectType, + ProjectUpgradeStatus, + ProjectVersion, + SortDirection, + TableName, + TProjects, + TProjectsUpdate +} from "@app/db/schemas"; import { BadRequestError, DatabaseError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; -import { Filter, ProjectFilterType } from "./project-types"; +import { ActorType } from "../auth/auth-type"; +import { Filter, ProjectFilterType, SearchProjectSortBy } from "./project-types"; export type TProjectDALFactory = ReturnType; export const projectDALFactory = (db: TDbClient) => { const projectOrm = ormify(db, TableName.Project); - const findAllProjects = async (userId: string) => { + const findUserProjects = async (userId: string, orgId: string, projectType: ProjectType | "all") => { try { const workspaces = await db .replicaNode()(TableName.ProjectMembership) .where({ userId }) .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) + .where(`${TableName.Project}.orgId`, orgId) + .andWhere((qb) => { + if (projectType !== "all") { + void qb.where(`${TableName.Project}.type`, projectType); + } + }) .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) .select( selectAllTableCols(TableName.Project), @@ -31,14 +47,17 @@ export const projectDALFactory = (db: TDbClient) => { { column: `${TableName.Environment}.position`, order: "asc" } ]); - const groups: string[] = await db(TableName.UserGroupMembership) - .where({ userId }) - .select(selectAllTableCols(TableName.UserGroupMembership)) - .pluck("groupId"); + const groups = db(TableName.UserGroupMembership).where({ userId }).select("groupId"); const groupWorkspaces = await db(TableName.GroupProjectMembership) .whereIn("groupId", groups) .join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`) + .where(`${TableName.Project}.orgId`, orgId) + .andWhere((qb) => { + if (projectType !== "all") { + void qb.where(`${TableName.Project}.type`, projectType); + } + }) .whereNotIn( `${TableName.Project}.id`, workspaces.map(({ id }) => id) @@ -108,12 +127,17 @@ export const projectDALFactory = (db: TDbClient) => { } }; - const findAllProjectsByIdentity = async (identityId: string) => { + const findAllProjectsByIdentity = async (identityId: string, projectType?: ProjectType) => { try { const workspaces = await db .replicaNode()(TableName.IdentityProjectMembership) .where({ identityId }) .join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`) + .andWhere((qb) => { + if (projectType) { + void qb.where(`${TableName.Project}.type`, projectType); + } + }) .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) .select( selectAllTableCols(TableName.Project), @@ -191,6 +215,10 @@ export const projectDALFactory = (db: TDbClient) => { return project; } catch (error) { + if (error instanceof NotFoundError) { + throw error; + } + throw new DatabaseError({ error, name: "Find all projects" }); } }; @@ -240,6 +268,10 @@ export const projectDALFactory = (db: TDbClient) => { return project; } catch (error) { + if (error instanceof NotFoundError || error instanceof UnauthorizedError) { + throw error; + } + throw new DatabaseError({ error, name: "Find project by slug" }); } }; @@ -260,7 +292,7 @@ export const projectDALFactory = (db: TDbClient) => { } throw new BadRequestError({ message: "Invalid filter type" }); } catch (error) { - if (error instanceof BadRequestError) { + if (error instanceof BadRequestError || error instanceof NotFoundError || error instanceof UnauthorizedError) { throw error; } throw new DatabaseError({ error, name: `Failed to find project by ${filter.type}` }); @@ -307,9 +339,95 @@ export const projectDALFactory = (db: TDbClient) => { }; }; + const getProjectFromSplitId = async (projectId: string, projectType: ProjectType) => { + try { + const project = await db(TableName.ProjectSplitBackfillIds) + .where({ + sourceProjectId: projectId, + destinationProjectType: projectType + }) + .join(TableName.Project, `${TableName.Project}.id`, `${TableName.ProjectSplitBackfillIds}.destinationProjectId`) + .select(selectAllTableCols(TableName.Project)) + .first(); + return project; + } catch (error) { + throw new DatabaseError({ error, name: `Failed to find split project with id ${projectId}` }); + } + }; + + const searchProjects = async (dto: { + orgId: string; + actor: ActorType; + actorId: string; + type?: ProjectType; + limit?: number; + offset?: number; + name?: string; + sortBy?: SearchProjectSortBy; + sortDir?: SortDirection; + }) => { + const { limit = 20, offset = 0, sortBy = SearchProjectSortBy.NAME, sortDir = SortDirection.ASC } = dto; + + const userMembershipSubquery = db(TableName.ProjectMembership).where({ userId: dto.actorId }).select("projectId"); + const groups = db(TableName.UserGroupMembership).where({ userId: dto.actorId }).select("groupId"); + const groupMembershipSubquery = db(TableName.GroupProjectMembership).whereIn("groupId", groups).select("projectId"); + + const identityMembershipSubQuery = db(TableName.IdentityProjectMembership) + .where({ identityId: dto.actorId }) + .select("projectId"); + + // Get the SQL strings for the subqueries + const userMembershipSql = userMembershipSubquery.toQuery(); + const groupMembershipSql = groupMembershipSubquery.toQuery(); + const identityMembershipSql = identityMembershipSubQuery.toQuery(); + + const query = db + .replicaNode()(TableName.Project) + .where(`${TableName.Project}.orgId`, dto.orgId) + .select(selectAllTableCols(TableName.Project)) + .select(db.raw("COUNT(*) OVER() AS count")) + .select<(TProjects & { isMember: boolean; count: number })[]>( + dto.actor === ActorType.USER + ? db.raw( + ` + CASE + WHEN ${TableName.Project}.id IN (?) THEN TRUE + WHEN ${TableName.Project}.id IN (?) THEN TRUE + ELSE FALSE + END as "isMember" + `, + [db.raw(userMembershipSql), db.raw(groupMembershipSql)] + ) + : db.raw( + ` + CASE + WHEN ${TableName.Project}.id IN (?) THEN TRUE + ELSE FALSE + END as "isMember" + `, + [db.raw(identityMembershipSql)] + ) + ) + .limit(limit) + .offset(offset); + if (sortBy === SearchProjectSortBy.NAME) { + void query.orderBy([{ column: `${TableName.Project}.name`, order: sortDir }]); + } + + if (dto.type) { + void query.where(`${TableName.Project}.type`, dto.type); + } + if (dto.name) { + void query.whereILike(`${TableName.Project}.name`, `%${dto.name}%`); + } + const docs = await query; + + return { docs, totalCount: Number(docs?.[0]?.count ?? 0) }; + }; + return { ...projectOrm, - findAllProjects, + findUserProjects, setProjectUpgradeStatus, findAllProjectsByIdentity, findProjectGhostUser, @@ -317,6 +435,8 @@ export const projectDALFactory = (db: TDbClient) => { findProjectByFilter, findProjectBySlug, findProjectWithOrg, - checkProjectUpgradeStatus + checkProjectUpgradeStatus, + getProjectFromSplitId, + searchProjects }; }; diff --git a/backend/src/services/project/project-fns.ts b/backend/src/services/project/project-fns.ts index 92d0dfc39..08652e348 100644 --- a/backend/src/services/project/project-fns.ts +++ b/backend/src/services/project/project-fns.ts @@ -1,12 +1,15 @@ import crypto from "crypto"; import { ProjectVersion, TProjects } from "@app/db/schemas"; +import { createSshCaHelper } from "@app/ee/services/ssh/ssh-certificate-authority-fns"; +import { SshCaKeySource } from "@app/ee/services/ssh/ssh-certificate-authority-types"; +import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types"; import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto"; import { NotFoundError } from "@app/lib/errors"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TProjectDALFactory } from "@app/services/project/project-dal"; -import { AddUserToWsDTO } from "./project-types"; +import { AddUserToWsDTO, TBootstrapSshProjectDTO } from "./project-types"; export const assignWorkspaceKeysToMembers = ({ members, decryptKey, userPrivateKey }: AddUserToWsDTO) => { const plaintextProjectKey = decryptAsymmetric({ @@ -102,3 +105,48 @@ export const getProjectKmsCertificateKeyId = async ({ return keyId; }; + +/** + * Bootstraps an SSH project. + * - Creates a user and host SSH CA + * - Creates a project SSH config with the user and host SSH CA as defaults + */ +export const bootstrapSshProject = async ({ + projectId, + sshCertificateAuthorityDAL, + sshCertificateAuthoritySecretDAL, + kmsService, + projectSshConfigDAL, + tx +}: TBootstrapSshProjectDTO) => { + const userSshCa = await createSshCaHelper({ + projectId, + friendlyName: "User CA", + keyAlgorithm: SshCertKeyAlgorithm.ED25519, + keySource: SshCaKeySource.INTERNAL, + sshCertificateAuthorityDAL, + sshCertificateAuthoritySecretDAL, + kmsService, + tx + }); + + const hostSshCa = await createSshCaHelper({ + projectId, + friendlyName: "Host CA", + keyAlgorithm: SshCertKeyAlgorithm.ED25519, + keySource: SshCaKeySource.INTERNAL, + sshCertificateAuthorityDAL, + sshCertificateAuthoritySecretDAL, + kmsService, + tx + }); + + await projectSshConfigDAL.create( + { + projectId, + defaultHostSshCaId: hostSshCa.id, + defaultUserSshCaId: userSshCa.id + }, + tx + ); +}; diff --git a/backend/src/services/project/project-queue.ts b/backend/src/services/project/project-queue.ts index d59bde6c1..e845ebd35 100644 --- a/backend/src/services/project/project-queue.ts +++ b/backend/src/services/project/project-queue.ts @@ -285,11 +285,14 @@ export const projectQueueFactory = ({ if (!orgMembership) { // This can happen. Since we don't remove project memberships and project keys when a user is removed from an org, this is a valid case. - logger.info("User is not in organization", { - userId: key.receiverId, - orgId: project.orgId, - projectId: project.id - }); + logger.info( + { + userId: key.receiverId, + orgId: project.orgId, + projectId: project.id + }, + "User is not in organization" + ); // eslint-disable-next-line no-continue continue; } @@ -551,10 +554,10 @@ export const projectQueueFactory = ({ .catch(() => [null]); if (!project) { - logger.error("Failed to upgrade project, because no project was found", data); + logger.error(data, "Failed to upgrade project, because no project was found"); } else { await projectDAL.setProjectUpgradeStatus(data.projectId, ProjectUpgradeStatus.Failed); - logger.error("Failed to upgrade project", err, { + logger.error(err, "Failed to upgrade project", { extra: { project, jobData: data diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index dfe2ce3ec..9b7c29c1b 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -1,25 +1,44 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; -import { OrgMembershipRole, ProjectMembershipRole, ProjectVersion, TProjectEnvironments } from "@app/db/schemas"; +import { + ActionProjectType, + ProjectMembershipRole, + ProjectType, + ProjectVersion, + TProjectEnvironments +} from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSshHostActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service"; import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types"; +import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; +import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal"; +import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal"; +import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal"; +import { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal"; import { TKeyStoreFactory } from "@app/keystore/keystore"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; +import { getConfig } from "@app/lib/config/env"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TProjectPermission } from "@app/lib/types"; +import { TQueueServiceFactory } from "@app/queue"; import { ActorType } from "../auth/auth-type"; import { TCertificateDALFactory } from "../certificate/certificate-dal"; import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal"; import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal"; +import { TGroupProjectDALFactory } from "../group-project/group-project-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal"; import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; @@ -29,19 +48,25 @@ import { TOrgServiceFactory } from "../org/org-service"; import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal"; import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal"; import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; +import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; import { getPredefinedRoles } from "../project-role/project-role-fns"; +import { TSecretDALFactory } from "../secret/secret-dal"; +import { fnDeleteProjectSecretReminders } from "../secret/secret-fns"; import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; +import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal"; import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal"; +import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TUserDALFactory } from "../user/user-dal"; import { TProjectDALFactory } from "./project-dal"; -import { assignWorkspaceKeysToMembers, createProjectKey } from "./project-fns"; +import { assignWorkspaceKeysToMembers, bootstrapSshProject, createProjectKey } from "./project-fns"; import { TProjectQueueFactory } from "./project-queue"; +import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal"; import { TCreateProjectDTO, TDeleteProjectDTO, @@ -53,8 +78,15 @@ import { TListProjectCertificateTemplatesDTO, TListProjectCertsDTO, TListProjectsDTO, + TListProjectSshCasDTO, + TListProjectSshCertificatesDTO, + TListProjectSshCertificateTemplatesDTO, + TListProjectSshHostsDTO, TLoadProjectKmsBackupDTO, + TProjectAccessRequestDTO, + TSearchProjectsDTO, TToggleProjectAutoCapitalizationDTO, + TToggleProjectDeleteProtectionDTO, TUpdateAuditLogsRetentionDTO, TUpdateProjectDTO, TUpdateProjectKmsDTO, @@ -71,17 +103,24 @@ export const DEFAULT_PROJECT_ENVS = [ ]; type TProjectServiceFactoryDep = { - // TODO: Pick projectDAL: TProjectDALFactory; + projectSshConfigDAL: Pick; projectQueue: TProjectQueueFactory; userDAL: TUserDALFactory; - folderDAL: TSecretFolderDALFactory; + projectBotService: Pick; + folderDAL: Pick; + secretDAL: Pick; + secretV2BridgeDAL: Pick; projectEnvDAL: Pick; identityOrgMembershipDAL: TIdentityOrgDALFactory; identityProjectDAL: TIdentityProjectDALFactory; identityProjectMembershipRoleDAL: Pick; projectKeyDAL: Pick; - projectMembershipDAL: Pick; + projectMembershipDAL: Pick< + TProjectMembershipDALFactory, + "create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers" + >; + groupProjectDAL: Pick; projectSlackConfigDAL: Pick; slackIntegrationDAL: Pick; projectUserMembershipRoleDAL: Pick; @@ -90,13 +129,21 @@ type TProjectServiceFactoryDep = { certificateTemplateDAL: Pick; pkiAlertDAL: Pick; pkiCollectionDAL: Pick; + sshCertificateAuthorityDAL: Pick; + sshCertificateAuthoritySecretDAL: Pick; + sshCertificateDAL: Pick; + sshCertificateTemplateDAL: Pick; + sshHostDAL: Pick; permissionService: TPermissionServiceFactory; orgService: Pick; licenseService: Pick; + queueService: Pick; + smtpService: Pick; + orgDAL: Pick; keyStore: Pick; projectBotDAL: Pick; - projectRoleDAL: Pick; + projectRoleDAL: Pick; kmsService: Pick< TKmsServiceFactory, | "updateProjectSecretManagerKmsKey" @@ -105,6 +152,7 @@ type TProjectServiceFactoryDep = { | "getKmsById" | "getProjectSecretManagerKmsKeyId" | "deleteInternalKms" + | "createCipherPairWithDataKey" >; projectTemplateService: TProjectTemplateServiceFactory; }; @@ -113,9 +161,14 @@ export type TProjectServiceFactory = ReturnType; export const projectServiceFactory = ({ projectDAL, + projectSshConfigDAL, + secretDAL, + secretV2BridgeDAL, projectQueue, projectKeyDAL, permissionService, + queueService, + projectBotService, orgDAL, userDAL, folderDAL, @@ -133,12 +186,19 @@ export const projectServiceFactory = ({ certificateTemplateDAL, pkiCollectionDAL, pkiAlertDAL, + sshCertificateAuthorityDAL, + sshCertificateAuthoritySecretDAL, + sshCertificateDAL, + sshCertificateTemplateDAL, + sshHostDAL, keyStore, kmsService, projectBotDAL, projectSlackConfigDAL, slackIntegrationDAL, - projectTemplateService + projectTemplateService, + groupProjectDAL, + smtpService }: TProjectServiceFactoryDep) => { /* * Create workspace. Make user the admin @@ -149,14 +209,15 @@ export const projectServiceFactory = ({ actorOrgId, actorAuthMethod, workspaceName, + workspaceDescription, slug: projectSlug, kmsKeyId, tx: trx, createDefaultEnvs = true, - template = InfisicalProjectTemplate.Default + template = InfisicalProjectTemplate.Default, + type = ProjectType.SecretManager }: TCreateProjectDTO) => { const organization = await orgDAL.findOne({ id: actorOrgId }); - const { permission, membership: orgMembership } = await permissionService.getOrgPermission( actor, actorId, @@ -206,6 +267,8 @@ export const projectServiceFactory = ({ const project = await projectDAL.create( { name: workspaceName, + type, + description: workspaceDescription, orgId: organization.id, slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`), kmsSecretManagerKeyId: kmsKeyId, @@ -215,6 +278,17 @@ export const projectServiceFactory = ({ tx ); + if (type === ProjectType.SSH) { + await bootstrapSshProject({ + projectId: project.id, + sshCertificateAuthorityDAL, + sshCertificateAuthoritySecretDAL, + kmsService, + projectSshConfigDAL, + tx + }); + } + // set ghost user as admin of project const projectMembership = await projectMembershipDAL.create( { @@ -368,20 +442,6 @@ export const projectServiceFactory = ({ }); } - // Get the role permission for the identity - const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole( - OrgMembershipRole.Member, - organization.id - ); - - // Identity has to be at least a member in order to create projects - const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission); - if (!hasPrivilege) - throw new ForbiddenRequestError({ - message: "Failed to add identity to project with more privileged role" - }); - const isCustomRole = Boolean(customRole); - const identityProjectMembership = await identityProjectDAL.create( { identityId: actorId, @@ -393,8 +453,7 @@ export const projectServiceFactory = ({ await identityProjectMembershipRoleDAL.create( { projectMembershipId: identityProjectMembership.id, - role: isCustomRole ? ProjectMembershipRole.Custom : ProjectMembershipRole.Admin, - customRoleId: customRole?.id + role: ProjectMembershipRole.Admin }, tx ); @@ -414,29 +473,63 @@ export const projectServiceFactory = ({ const deleteProject = async ({ actor, actorId, actorOrgId, actorAuthMethod, filter }: TDeleteProjectDTO) => { const project = await projectDAL.findProjectByFilter(filter); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - project.id, + projectId: project.id, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project); + if (project.hasDeleteProtection) { + throw new ForbiddenRequestError({ + message: "Project delete protection is enabled" + }); + } + const deletedProject = await projectDAL.transaction(async (tx) => { + // delete these so that project custom roles can be deleted in cascade effect + // direct deletion of project without these will cause fk error + await projectMembershipDAL.delete({ projectId: project.id }, tx); + await groupProjectDAL.delete({ projectId: project.id }, tx); const delProject = await projectDAL.deleteById(project.id, tx); const projectGhostUser = await projectMembershipDAL.findProjectGhostUser(project.id, tx).catch(() => null); + // akhilmhdh: before removing those kms checking any other project uses it + // happened due to project split if (delProject.kmsCertificateKeyId) { - await kmsService.deleteInternalKms(delProject.kmsCertificateKeyId, delProject.orgId, tx); + const projectsLinkedToForiegnKey = await projectDAL.find( + { kmsCertificateKeyId: delProject.kmsCertificateKeyId }, + { tx } + ); + if (!projectsLinkedToForiegnKey.length) { + await kmsService.deleteInternalKms(delProject.kmsCertificateKeyId, delProject.orgId, tx); + } } + if (delProject.kmsSecretManagerKeyId) { - await kmsService.deleteInternalKms(delProject.kmsSecretManagerKeyId, delProject.orgId, tx); + const projectsLinkedToForiegnKey = await projectDAL.find( + { kmsSecretManagerKeyId: delProject.kmsSecretManagerKeyId }, + { tx } + ); + if (!projectsLinkedToForiegnKey.length) { + await kmsService.deleteInternalKms(delProject.kmsSecretManagerKeyId, delProject.orgId, tx); + } } // Delete the org membership for the ghost user if it's found. if (projectGhostUser) { await userDAL.deleteById(projectGhostUser.id, tx); } + await fnDeleteProjectSecretReminders(project.id, { + secretDAL, + secretV2BridgeDAL, + queueService, + projectBotService, + folderDAL + }); + return delProject; }); @@ -444,11 +537,22 @@ export const projectServiceFactory = ({ return deletedProject; }; - const getProjects = async ({ actorId, includeRoles, actorAuthMethod, actorOrgId }: TListProjectsDTO) => { - const workspaces = await projectDAL.findAllProjects(actorId); + const getProjects = async ({ + actorId, + includeRoles, + actorAuthMethod, + actorOrgId, + type = ProjectType.SecretManager + }: TListProjectsDTO) => { + const workspaces = await projectDAL.findUserProjects(actorId, actorOrgId, type); if (includeRoles) { - const { permission } = await permissionService.getUserOrgPermission(actorId, actorOrgId, actorAuthMethod); + const { permission } = await permissionService.getUserOrgPermission( + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); // `includeRoles` is specifically used by organization admins when inviting new users to the organizations to avoid looping redundant api calls. ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member); @@ -478,26 +582,51 @@ export const projectServiceFactory = ({ const getAProject = async ({ actorId, actorOrgId, actorAuthMethod, filter, actor }: TGetProjectDTO) => { const project = await projectDAL.findProjectByFilter(filter); - await permissionService.getProjectPermission(actor, actorId, project.id, actorAuthMethod, actorOrgId); + await permissionService.getProjectPermission({ + actor, + actorId, + projectId: project.id, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.Any + }); return project; }; const updateProject = async ({ actor, actorId, actorOrgId, actorAuthMethod, update, filter }: TUpdateProjectDTO) => { const project = await projectDAL.findProjectByFilter(filter); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - project.id, + projectId: project.id, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); + if (update.slug) { + const existingProject = await projectDAL.findOne({ + slug: update.slug, + orgId: actorOrgId + }); + if (existingProject && existingProject.id !== project.id) { + throw new BadRequestError({ + message: `Failed to update project slug. The project "${existingProject.name}" with the slug "${existingProject.slug}" already exists in your organization. Please choose a unique slug for your project.` + }); + } + } + const updatedProject = await projectDAL.updateById(project.id, { name: update.name, - autoCapitalization: update.autoCapitalization + description: update.description, + autoCapitalization: update.autoCapitalization, + enforceCapitalization: update.autoCapitalization, + hasDeleteProtection: update.hasDeleteProtection, + slug: update.slug }); + return updatedProject; }; @@ -509,16 +638,44 @@ export const projectServiceFactory = ({ actorAuthMethod, autoCapitalization }: TToggleProjectAutoCapitalizationDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); - const updatedProject = await projectDAL.updateById(projectId, { autoCapitalization }); + const updatedProject = await projectDAL.updateById(projectId, { + autoCapitalization, + enforceCapitalization: autoCapitalization + }); + + return updatedProject; + }; + + const toggleDeleteProtection = async ({ + projectId, + actor, + actorId, + actorOrgId, + actorAuthMethod, + hasDeleteProtection + }: TToggleProjectDeleteProtectionDTO) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); + + const updatedProject = await projectDAL.updateById(projectId, { hasDeleteProtection }); + return updatedProject; }; @@ -537,13 +694,14 @@ export const projectServiceFactory = ({ }); } - const { hasRole } = await permissionService.getProjectPermission( + const { hasRole } = await permissionService.getProjectPermission({ actor, actorId, - project.id, + projectId: project.id, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); if (!hasRole(ProjectMembershipRole.Admin)) throw new ForbiddenRequestError({ @@ -568,13 +726,14 @@ export const projectServiceFactory = ({ }); } - const { hasRole } = await permissionService.getProjectPermission( + const { hasRole } = await permissionService.getProjectPermission({ actor, actorId, - project.id, + projectId: project.id, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); if (!hasRole(ProjectMembershipRole.Admin)) { throw new ForbiddenRequestError({ @@ -600,13 +759,14 @@ export const projectServiceFactory = ({ actorAuthMethod, name }: TUpdateProjectNameDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); const updatedProject = await projectDAL.updateById(projectId, { name }); @@ -621,13 +781,14 @@ export const projectServiceFactory = ({ actorOrgId, userPrivateKey }: TUpgradeProjectDTO) => { - const { permission, hasRole } = await permissionService.getProjectPermission( + const { permission, hasRole } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project); @@ -658,14 +819,15 @@ export const projectServiceFactory = ({ actorOrgId, actorId }: TProjectPermission) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); const project = await projectDAL.findProjectById(projectId); @@ -694,14 +856,23 @@ export const projectServiceFactory = ({ actor }: TListProjectCasDTO) => { const project = await projectDAL.findProjectByFilter(filter); + let projectId = project.id; + const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId( + projectId, + ProjectType.CertificateManager + ); + if (certManagerProjectFromSplit) { + projectId = certManagerProjectFromSplit.id; + } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - project.id, + projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, @@ -710,7 +881,7 @@ export const projectServiceFactory = ({ const cas = await certificateAuthorityDAL.find( { - projectId: project.id, + projectId, ...(status && { status }), ...(friendlyName && { friendlyName }), ...(commonName && { commonName }) @@ -736,18 +907,27 @@ export const projectServiceFactory = ({ actor }: TListProjectCertsDTO) => { const project = await projectDAL.findProjectByFilter(filter); + let projectId = project.id; + const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId( + projectId, + ProjectType.CertificateManager + ); + if (certManagerProjectFromSplit) { + projectId = certManagerProjectFromSplit.id; + } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - project.id, + projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); - const cas = await certificateAuthorityDAL.find({ projectId: project.id }); + const cas = await certificateAuthorityDAL.find({ projectId }); const certificates = await certificateDAL.find( { @@ -761,7 +941,7 @@ export const projectServiceFactory = ({ ); const count = await certificateDAL.countCertificatesInProject({ - projectId: project.id, + projectId, friendlyName, commonName }); @@ -776,19 +956,29 @@ export const projectServiceFactory = ({ * Return list of (PKI) alerts configured for project */ const listProjectAlerts = async ({ - projectId, + projectId: preSplitProjectId, actor, actorId, actorAuthMethod, actorOrgId }: TListProjectAlertsDTO) => { - const { permission } = await permissionService.getProjectPermission( + let projectId = preSplitProjectId; + const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId( + projectId, + ProjectType.CertificateManager + ); + if (certManagerProjectFromSplit) { + projectId = certManagerProjectFromSplit.id; + } + + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts); @@ -803,19 +993,28 @@ export const projectServiceFactory = ({ * Return list of PKI collections for project */ const listProjectPkiCollections = async ({ - projectId, + projectId: preSplitProjectId, actor, actorId, actorAuthMethod, actorOrgId }: TListProjectAlertsDTO) => { - const { permission } = await permissionService.getProjectPermission( + let projectId = preSplitProjectId; + const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId( + projectId, + ProjectType.CertificateManager + ); + if (certManagerProjectFromSplit) { + projectId = certManagerProjectFromSplit.id; + } + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections); @@ -830,19 +1029,29 @@ export const projectServiceFactory = ({ * Return list of certificate templates for project */ const listProjectCertificateTemplates = async ({ - projectId, + projectId: preSplitProjectId, actorId, actorOrgId, actorAuthMethod, actor }: TListProjectCertificateTemplatesDTO) => { - const { permission } = await permissionService.getProjectPermission( + let projectId = preSplitProjectId; + const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId( + projectId, + ProjectType.CertificateManager + ); + if (certManagerProjectFromSplit) { + projectId = certManagerProjectFromSplit.id; + } + + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, @@ -856,6 +1065,160 @@ export const projectServiceFactory = ({ }; }; + /** + * Return list of SSH CAs for project + */ + const listProjectSshCas = async ({ + actorId, + actorOrgId, + actorAuthMethod, + actor, + projectId + }: TListProjectSshCasDTO) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.SshCertificateAuthorities + ); + + const cas = await sshCertificateAuthorityDAL.find( + { + projectId + }, + { sort: [["updatedAt", "desc"]] } + ); + + return cas; + }; + + /** + * Return list of SSH hosts for project + */ + const listProjectSshHosts = async ({ + actorId, + actorOrgId, + actorAuthMethod, + actor, + projectId + }: TListProjectSshHostsDTO) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + const allowedHosts = []; + + // (dangtony98): room to optimize + const hosts = await sshHostDAL.findSshHostsWithLoginMappings(projectId); + + for (const host of hosts) { + try { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSshHostActions.Read, + subject(ProjectPermissionSub.SshHosts, { + hostname: host.hostname + }) + ); + + allowedHosts.push(host); + } catch { + // intentionally ignore projects where user lacks access + } + } + + return allowedHosts; + }; + + /** + * Return list of SSH certificates for project + */ + const listProjectSshCertificates = async ({ + limit = 25, + offset = 0, + actorId, + actorOrgId, + actorAuthMethod, + actor, + projectId + }: TListProjectSshCertificatesDTO) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates); + + const cas = await sshCertificateAuthorityDAL.find({ + projectId + }); + + const certificates = await sshCertificateDAL.find( + { + $in: { + sshCaId: cas.map((ca) => ca.id) + } + }, + { offset, limit, sort: [["updatedAt", "desc"]] } + ); + + const count = await sshCertificateDAL.countSshCertificatesInProject(projectId); + + return { certificates, totalCount: count }; + }; + + /** + * Return list of SSH certificate templates for project + */ + const listProjectSshCertificateTemplates = async ({ + actorId, + actorOrgId, + actorAuthMethod, + actor, + projectId + }: TListProjectSshCertificateTemplatesDTO) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SSH + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.SshCertificateTemplates + ); + + const cas = await sshCertificateAuthorityDAL.find({ + projectId + }); + + const certificateTemplates = await sshCertificateTemplateDAL.find({ + $in: { + sshCaId: cas.map((ca) => ca.id) + } + }); + + return { certificateTemplates }; + }; + const updateProjectKmsKey = async ({ projectId, kms, @@ -864,13 +1227,14 @@ export const projectServiceFactory = ({ actorAuthMethod, actorOrgId }: TUpdateProjectKmsDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms); @@ -891,13 +1255,14 @@ export const projectServiceFactory = ({ actorAuthMethod, actorOrgId }: TProjectPermission) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms); @@ -920,13 +1285,14 @@ export const projectServiceFactory = ({ actorOrgId, backup }: TLoadProjectKmsBackupDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms); @@ -942,13 +1308,14 @@ export const projectServiceFactory = ({ }; const getProjectKmsKeys = async ({ projectId, actor, actorId, actorAuthMethod, actorOrgId }: TGetProjectKmsKey) => { - const { membership } = await permissionService.getProjectPermission( + const { membership } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); if (!membership) { throw new ForbiddenRequestError({ message: "You are not a member of this project" }); @@ -974,13 +1341,14 @@ export const projectServiceFactory = ({ }); } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); @@ -1022,13 +1390,14 @@ export const projectServiceFactory = ({ }); } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); @@ -1074,6 +1443,85 @@ export const projectServiceFactory = ({ }); }; + const searchProjects = async ({ + name, + offset, + permission, + limit, + type, + orderBy, + orderDirection + }: TSearchProjectsDTO) => { + // check user belong to org + await permissionService.getOrgPermission( + permission.type, + permission.id, + permission.orgId, + permission.authMethod, + permission.orgId + ); + + return projectDAL.searchProjects({ + limit, + offset, + name, + type, + orgId: permission.orgId, + actor: permission.type, + actorId: permission.id, + sortBy: orderBy, + sortDir: orderDirection + }); + }; + + const requestProjectAccess = async ({ permission, comment, projectId }: TProjectAccessRequestDTO) => { + // check user belong to org + await permissionService.getOrgPermission( + permission.type, + permission.id, + permission.orgId, + permission.authMethod, + permission.orgId + ); + + const projectMember = await permissionService + .getProjectPermission({ + actor: permission.type, + actorId: permission.id, + projectId, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: permission.authMethod, + actorOrgId: permission.orgId + }) + .catch(() => { + return null; + }); + if (projectMember) throw new BadRequestError({ message: "User already has access to the project" }); + + const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId); + const filteredProjectMembers = projectMembers + .filter((member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin)) + .map((el) => el.user.email!); + const org = await orgDAL.findOne({ id: permission.orgId }); + const project = await projectDAL.findById(projectId); + const userDetails = await userDAL.findById(permission.id); + const appCfg = getConfig(); + + await smtpService.sendMail({ + template: SmtpTemplates.ProjectAccessRequest, + recipients: filteredProjectMembers, + subjectLine: "Project Access Request", + substitutions: { + requesterName: `${userDetails.firstName} ${userDetails.lastName}`, + requesterEmail: userDetails.email, + projectName: project?.name, + orgName: org?.name, + note: comment, + callback_url: `${appCfg.SITE_URL}/${project.type}/${project.id}/access-management?selectedTab=members&requesterEmail=${userDetails.email}` + } + }); + }; + return { createProject, deleteProject, @@ -1082,6 +1530,7 @@ export const projectServiceFactory = ({ getProjectUpgradeStatus, getAProject, toggleAutoCapitalization, + toggleDeleteProtection, updateName, upgradeProject, listProjectCas, @@ -1089,6 +1538,10 @@ export const projectServiceFactory = ({ listProjectAlerts, listProjectPkiCollections, listProjectCertificateTemplates, + listProjectSshCas, + listProjectSshHosts, + listProjectSshCertificates, + listProjectSshCertificateTemplates, updateVersionLimit, updateAuditLogsRetention, updateProjectKmsKey, @@ -1096,6 +1549,8 @@ export const projectServiceFactory = ({ loadProjectKmsBackup, getProjectKmsKeys, getProjectSlackConfig, - updateProjectSlackConfig + updateProjectSlackConfig, + requestProjectAccess, + searchProjects }; }; diff --git a/backend/src/services/project/project-ssh-config-dal.ts b/backend/src/services/project/project-ssh-config-dal.ts new file mode 100644 index 000000000..5085bd438 --- /dev/null +++ b/backend/src/services/project/project-ssh-config-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TProjectSshConfigDALFactory = ReturnType; + +export const projectSshConfigDALFactory = (db: TDbClient) => { + const projectSshConfigOrm = ormify(db, TableName.ProjectSshConfig); + + return projectSshConfigOrm; +}; diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 28cda2d95..444f6309c 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -1,11 +1,24 @@ import { Knex } from "knex"; -import { TProjectKeys } from "@app/db/schemas"; -import { TProjectPermission } from "@app/lib/types"; +import { ProjectType, SortDirection, TProjectKeys } from "@app/db/schemas"; +import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; +import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal"; +import { OrgServiceActor, TProjectPermission } from "@app/lib/types"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TProjectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal"; import { ActorAuthMethod, ActorType } from "../auth/auth-type"; -import { CaStatus } from "../certificate-authority/certificate-authority-types"; -import { KmsType } from "../kms/kms-types"; + +enum KmsType { + External = "external", + Internal = "internal" +} + +enum CaStatus { + ACTIVE = "active", + DISABLED = "disabled", + PENDING_CERTIFICATE = "pending-certificate" +} export enum ProjectFilterType { ID = "id", @@ -29,11 +42,13 @@ export type TCreateProjectDTO = { actorId: string; actorOrgId?: string; workspaceName: string; + workspaceDescription?: string; slug?: string; kmsKeyId?: string; createDefaultEnvs?: boolean; template?: string; tx?: Knex; + type?: ProjectType; }; export type TDeleteProjectBySlugDTO = { @@ -51,6 +66,10 @@ export type TToggleProjectAutoCapitalizationDTO = { autoCapitalization: boolean; } & TProjectPermission; +export type TToggleProjectDeleteProtectionDTO = { + hasDeleteProtection: boolean; +} & TProjectPermission; + export type TUpdateProjectVersionLimitDTO = { pitVersionLimit: number; workspaceSlug: string; @@ -69,7 +88,10 @@ export type TUpdateProjectDTO = { filter: Filter; update: { name?: string; + description?: string; autoCapitalization?: boolean; + hasDeleteProtection?: boolean; + slug?: string; }; } & Omit; @@ -82,6 +104,7 @@ export type TDeleteProjectDTO = { export type TListProjectsDTO = { includeRoles: boolean; + type?: ProjectType | "all"; } & Omit; export type TUpgradeProjectDTO = { @@ -128,6 +151,14 @@ export type TGetProjectKmsKey = TProjectPermission; export type TListProjectCertificateTemplatesDTO = TProjectPermission; +export type TListProjectSshCasDTO = TProjectPermission; +export type TListProjectSshHostsDTO = TProjectPermission; +export type TListProjectSshCertificateTemplatesDTO = TProjectPermission; +export type TListProjectSshCertificatesDTO = { + offset: number; + limit: number; +} & TProjectPermission; + export type TGetProjectSlackConfig = TProjectPermission; export type TUpdateProjectSlackConfig = { @@ -137,3 +168,32 @@ export type TUpdateProjectSlackConfig = { isSecretRequestNotificationEnabled: boolean; secretRequestChannels: string; } & TProjectPermission; + +export type TBootstrapSshProjectDTO = { + projectId: string; + sshCertificateAuthorityDAL: Pick; + sshCertificateAuthoritySecretDAL: Pick; + projectSshConfigDAL: Pick; + kmsService: Pick; + tx?: Knex; +}; + +export enum SearchProjectSortBy { + NAME = "name" +} + +export type TSearchProjectsDTO = { + permission: OrgServiceActor; + name?: string; + type?: ProjectType; + limit?: number; + offset?: number; + orderBy?: SearchProjectSortBy; + orderDirection?: SortDirection; +}; + +export type TProjectAccessRequestDTO = { + permission: OrgServiceActor; + projectId: string; + comment?: string; +}; diff --git a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts index dab70806f..32f180636 100644 --- a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts +++ b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts @@ -5,10 +5,12 @@ import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityUaClientSecretDALFactory } from "../identity-ua/identity-ua-client-secret-dal"; +import { TSecretDALFactory } from "../secret/secret-dal"; import { TSecretVersionDALFactory } from "../secret/secret-version-dal"; import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal"; import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal"; import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal"; +import { TServiceTokenServiceFactory } from "../service-token/service-token-service"; type TDailyResourceCleanUpQueueServiceFactoryDep = { auditLogDAL: Pick; @@ -16,9 +18,11 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = { identityUniversalAuthClientSecretDAL: Pick; secretVersionDAL: Pick; secretVersionV2DAL: Pick; + secretDAL: Pick; secretFolderVersionDAL: Pick; snapshotDAL: Pick; - secretSharingDAL: Pick; + secretSharingDAL: Pick; + serviceTokenService: Pick; queueService: TQueueServiceFactory; }; @@ -30,21 +34,26 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ snapshotDAL, secretVersionDAL, secretFolderVersionDAL, + secretDAL, identityAccessTokenDAL, secretSharingDAL, secretVersionV2DAL, - identityUniversalAuthClientSecretDAL + identityUniversalAuthClientSecretDAL, + serviceTokenService }: TDailyResourceCleanUpQueueServiceFactoryDep) => { queueService.start(QueueName.DailyResourceCleanUp, async () => { logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`); + await secretDAL.pruneSecretReminders(queueService); await auditLogDAL.pruneAuditLog(); await identityAccessTokenDAL.removeExpiredTokens(); await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets(); await secretSharingDAL.pruneExpiredSharedSecrets(); + await secretSharingDAL.pruneExpiredSecretRequests(); await snapshotDAL.pruneExcessSnapshots(); await secretVersionDAL.pruneExcessVersions(); await secretVersionV2DAL.pruneExcessVersions(); await secretFolderVersionDAL.pruneExcessVersions(); + await serviceTokenService.notifyExpiringTokens(); logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`); }); diff --git a/backend/src/services/resource-metadata/resource-metadata-dal.ts b/backend/src/services/resource-metadata/resource-metadata-dal.ts new file mode 100644 index 000000000..b8b7a1541 --- /dev/null +++ b/backend/src/services/resource-metadata/resource-metadata-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TResourceMetadataDALFactory = ReturnType; + +export const resourceMetadataDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.ResourceMetadata); + + return orm; +}; diff --git a/backend/src/services/resource-metadata/resource-metadata-schema.ts b/backend/src/services/resource-metadata/resource-metadata-schema.ts new file mode 100644 index 000000000..10641e869 --- /dev/null +++ b/backend/src/services/resource-metadata/resource-metadata-schema.ts @@ -0,0 +1,10 @@ +import z from "zod"; + +export const ResourceMetadataSchema = z + .object({ + key: z.string().trim().min(1), + value: z.string().trim().default("") + }) + .array(); + +export type ResourceMetadataDTO = z.infer; diff --git a/backend/src/services/secret-blind-index/secret-blind-index-service.ts b/backend/src/services/secret-blind-index/secret-blind-index-service.ts index 57746307a..1cbbbcbb8 100644 --- a/backend/src/services/secret-blind-index/secret-blind-index-service.ts +++ b/backend/src/services/secret-blind-index/secret-blind-index-service.ts @@ -1,4 +1,4 @@ -import { ProjectMembershipRole } from "@app/db/schemas"; +import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; @@ -31,7 +31,14 @@ export const secretBlindIndexServiceFactory = ({ actorAuthMethod, actorOrgId }: TGetProjectBlindIndexStatusDTO) => { - await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId); + await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); const secretCount = await secretBlindIndexDAL.countOfSecretsWithNullSecretBlindIndex(projectId); return Number(secretCount); @@ -44,13 +51,14 @@ export const secretBlindIndexServiceFactory = ({ actorOrgId, actor }: TGetProjectSecretsDTO) => { - const { hasRole } = await permissionService.getProjectPermission( + const { hasRole } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); if (!hasRole(ProjectMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Insufficient privileges, user must be admin" }); } @@ -67,13 +75,14 @@ export const secretBlindIndexServiceFactory = ({ actorOrgId, secretsToUpdate }: TUpdateProjectSecretNameDTO) => { - const { hasRole } = await permissionService.getProjectPermission( + const { hasRole } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); if (!hasRole(ProjectMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Insufficient privileges, user must be admin" }); } diff --git a/backend/src/services/secret-folder/secret-folder-dal.ts b/backend/src/services/secret-folder/secret-folder-dal.ts index a4e6aca5a..e136c5a50 100644 --- a/backend/src/services/secret-folder/secret-folder-dal.ts +++ b/backend/src/services/secret-folder/secret-folder-dal.ts @@ -1,19 +1,22 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TProjectEnvironments, TSecretFolders, TSecretFoldersUpdate } from "@app/db/schemas"; +import { TableName, TSecretFolders, TSecretFoldersUpdate } from "@app/db/schemas"; import { BadRequestError, DatabaseError } from "@app/lib/errors"; import { groupBy, removeTrailingSlash } from "@app/lib/fn"; import { ormify, selectAllTableCols } from "@app/lib/knex"; import { OrderByDirection } from "@app/lib/types"; +import { isValidSecretPath } from "@app/lib/validator"; +import { CharacterType, characterValidator } from "@app/lib/validator/validate-string"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { TFindFoldersDeepByParentIdsDTO } from "./secret-folder-types"; -export const validateFolderName = (folderName: string) => { - const validNameRegex = /^[a-zA-Z0-9-_]+$/; - return validNameRegex.test(folderName); -}; +export const validateFolderName = characterValidator([ + CharacterType.AlphaNumeric, + CharacterType.Hyphen, + CharacterType.Underscore +]); const sqlFindMultipleFolderByEnvPathQuery = (db: Knex, query: Array<{ envId: string; secretPath: string }>) => { // this is removing an trailing slash like /folder1/folder2/ -> /folder1/folder2 @@ -40,12 +43,12 @@ const sqlFindMultipleFolderByEnvPathQuery = (db: Knex, query: Array<{ envId: str void baseQb .select({ depth: 1, - // latestFolderVerId: db.raw("NULL::uuid"), path: db.raw("'/'") }) .from(TableName.SecretFolder) .where({ - parentId: null + parentId: null, + name: "root" }) .whereIn( "envId", @@ -68,9 +71,7 @@ const sqlFindMultipleFolderByEnvPathQuery = (db: Knex, query: Array<{ envId: str .where((wb) => formatedQuery.map(({ secretPath }) => wb.orWhereRaw( - `depth = array_position(ARRAY[${secretPath.map(() => "?").join(",")}]::varchar[], ${ - TableName.SecretFolder - }.name,depth)`, + `secret_folders.name = (ARRAY[${secretPath.map(() => "?").join(",")}]::varchar[])[depth]`, [...secretPath] ) ) @@ -106,7 +107,6 @@ const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environments: str void baseQb .select({ depth: 1, - // latestFolderVerId: db.raw("NULL::uuid"), path: db.raw("'/'") }) .from(TableName.SecretFolder) @@ -116,6 +116,11 @@ const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environments: str parentId: null }) .whereIn(`${TableName.Environment}.slug`, environments) + .select( + db.ref("slug").withSchema(TableName.Environment).as("envSlug"), + db.ref("name").withSchema(TableName.Environment).as("envName"), + db.ref("projectId").withSchema(TableName.Environment) + ) .select(selectAllTableCols(TableName.SecretFolder)) .union( (qb) => @@ -127,21 +132,20 @@ const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environments: str depth: db.raw("parent.depth + 1"), path: db.raw( "CONCAT((CASE WHEN parent.path = '/' THEN '' ELSE parent.path END),'/', secret_folders.name)" - ) + ), + envSlug: db.ref("envSlug").withSchema("parent"), + envName: db.ref("envName").withSchema("parent"), + projectId: db.ref("projectId").withSchema("parent") }) .select(selectAllTableCols(TableName.SecretFolder)) - .whereRaw( - `depth = array_position(ARRAY[${pathSegments - .map(() => "?") - .join(",")}]::varchar[], secret_folders.name,depth)`, - [...pathSegments] - ) + .whereRaw(`secret_folders.name = (ARRAY[${pathSegments.map(() => "?").join(",")}]::varchar[])[depth]`, [ + ...pathSegments + ]) .from(TableName.SecretFolder) .join("parent", "parent.id", `${TableName.SecretFolder}.parentId`) ); }) .from("parent") - .leftJoin(TableName.Environment, `${TableName.Environment}.id`, "parent.envId") .select< (TSecretFolders & { depth: number; @@ -151,13 +155,7 @@ const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environments: str envName: string; projectId: string; })[] - >( - selectAllTableCols("parent" as TableName.SecretFolder), - db.ref("id").withSchema(TableName.Environment).as("envId"), - db.ref("slug").withSchema(TableName.Environment).as("envSlug"), - db.ref("name").withSchema(TableName.Environment).as("envName"), - db.ref("projectId").withSchema(TableName.Environment) - ); + >(selectAllTableCols("parent" as TableName.SecretFolder)); }; const sqlFindSecretPathByFolderId = (db: Knex, projectId: string, folderIds: string[]) => @@ -192,9 +190,9 @@ const sqlFindSecretPathByFolderId = (db: Knex, projectId: string, folderIds: str // the root folder check is used to avoid last / and also root name in folders depth: db.raw("parent.depth + 1"), path: db.raw( - `CONCAT( CASE - WHEN ${TableName.SecretFolder}."parentId" is NULL THEN '' - ELSE CONCAT('/', secret_folders.name) + `CONCAT( CASE + WHEN ${TableName.SecretFolder}."parentId" is NULL THEN '' + ELSE CONCAT('/', secret_folders.name) END, parent.path )` ), child: db.raw("COALESCE(parent.child, parent.id)"), @@ -214,18 +212,17 @@ export const secretFolderDALFactory = (db: TDbClient) => { const secretFolderOrm = ormify(db, TableName.SecretFolder); const findBySecretPath = async (projectId: string, environment: string, path: string, tx?: Knex) => { + const isValidPath = isValidSecretPath(path); + if (!isValidPath) + throw new BadRequestError({ + message: "Invalid secret path. Only alphanumeric characters, dashes, and underscores are allowed." + }); + const formatedPath = removeTrailingSlash(path); try { - const folder = await sqlFindFolderByPathQuery( - tx || db.replicaNode(), - projectId, - [environment], - removeTrailingSlash(path) - ) - .orderBy("depth", "desc") + const query = sqlFindFolderByPathQuery(tx || db.replicaNode(), projectId, [environment], formatedPath) + .where("path", formatedPath) .first(); - if (folder && folder.path !== removeTrailingSlash(path)) { - return; - } + const folder = await query; if (!folder) return; const { envId: id, envName: name, envSlug: slug, ...el } = folder; return { ...el, envId: id, environment: { id, name, slug } }; @@ -236,23 +233,20 @@ export const secretFolderDALFactory = (db: TDbClient) => { // finds folders by path for multiple envs const findBySecretPathMultiEnv = async (projectId: string, environments: string[], path: string, tx?: Knex) => { - try { - const pathDepth = removeTrailingSlash(path).split("/").filter(Boolean).length + 1; + const isValidPath = isValidSecretPath(path); + if (!isValidPath) + throw new BadRequestError({ + message: "Invalid secret path. Only alphanumeric characters, dashes, and underscores are allowed." + }); + try { + const formatedPath = removeTrailingSlash(path); const folders = await sqlFindFolderByPathQuery( tx || db.replicaNode(), projectId, environments, - removeTrailingSlash(path) - ) - .orderBy("depth", "desc") - .where("depth", pathDepth); - - const firstFolder = folders[0]; - - if (firstFolder && firstFolder.path !== removeTrailingSlash(path)) { - return []; - } + formatedPath + ).where("path", removeTrailingSlash(path)); return folders.map((folder) => { const { envId: id, envName: name, envSlug: slug, ...el } = folder; @@ -267,6 +261,12 @@ export const secretFolderDALFactory = (db: TDbClient) => { // even if its the original given /path1/path2 // it will stop automatically at /path2 const findClosestFolder = async (projectId: string, environment: string, path: string, tx?: Knex) => { + const isValidPath = isValidSecretPath(path); + if (!isValidPath) + throw new BadRequestError({ + message: "Invalid secret path. Only alphanumeric characters, dashes, and underscores are allowed." + }); + try { const folder = await sqlFindFolderByPathQuery( tx || db.replicaNode(), @@ -304,7 +304,6 @@ export const secretFolderDALFactory = (db: TDbClient) => { const findSecretPathByFolderIds = async (projectId: string, folderIds: string[], tx?: Knex) => { try { const folders = await sqlFindSecretPathByFolderId(tx || db.replicaNode(), projectId, folderIds); - // travelling all the way from leaf node to root contains real path const rootFolders = groupBy( folders.filter(({ parentId }) => parentId === null), @@ -467,13 +466,14 @@ export const secretFolderDALFactory = (db: TDbClient) => { db.raw("parents.depth + 1 as depth"), db.raw( `CONCAT( - CASE WHEN parents.path = '/' THEN '' ELSE parents.path END, + CASE WHEN parents.path = '/' THEN '' ELSE parents.path END, CASE WHEN ${TableName.SecretFolder}."parentId" is NULL THEN '' ELSE CONCAT('/', secret_folders.name) END )` ), db.ref("parents.environment") ) .from(TableName.SecretFolder) + .where(`${TableName.SecretFolder}.isReserved`, false) .join("parents", `${TableName.SecretFolder}.parentId`, "parents.id"); }) ) diff --git a/backend/src/services/secret-folder/secret-folder-fns.ts b/backend/src/services/secret-folder/secret-folder-fns.ts new file mode 100644 index 000000000..a3783a1b9 --- /dev/null +++ b/backend/src/services/secret-folder/secret-folder-fns.ts @@ -0,0 +1,17 @@ +import { TSecretFolders } from "@app/db/schemas"; +import { InternalServerError } from "@app/lib/errors"; + +export const buildFolderPath = ( + folder: TSecretFolders, + foldersMap: Record, + depth: number = 0 +): string => { + if (depth > 20) { + throw new InternalServerError({ message: "Maximum folder depth of 20 exceeded" }); + } + if (!folder.parentId) { + return depth === 0 ? "/" : ""; + } + + return `${buildFolderPath(foldersMap[folder.parentId], foldersMap, depth + 1)}/${folder.name}`; +}; diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index d787520a2..842eb2bb7 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -2,12 +2,13 @@ import { ForbiddenError, subject } from "@casl/ability"; import path from "path"; import { v4 as uuidv4, validate as uuidValidate } from "uuid"; -import { TSecretFoldersInsert } from "@app/db/schemas"; +import { ActionProjectType, TSecretFoldersInsert } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { OrderByDirection, OrgServiceActor } from "@app/lib/types"; +import { buildFolderPath } from "@app/services/secret-folder/secret-folder-fns"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; @@ -27,7 +28,7 @@ type TSecretFolderServiceFactoryDep = { permissionService: Pick; snapshotService: Pick; folderDAL: TSecretFolderDALFactory; - projectEnvDAL: Pick; + projectEnvDAL: Pick; folderVersionDAL: TSecretFolderVersionDALFactory; projectDAL: Pick; }; @@ -50,15 +51,17 @@ export const secretFolderServiceFactory = ({ actorOrgId, name, environment, - path: secretPath + path: secretPath, + description }: TCreateFolderDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, @@ -120,7 +123,10 @@ export const secretFolderServiceFactory = ({ } } - const doc = await folderDAL.create({ name, envId: env.id, version: 1, parentId: parentFolderId }, tx); + const doc = await folderDAL.create( + { name, envId: env.id, version: 1, parentId: parentFolderId, description }, + tx + ); await folderVersionDAL.create( { name: doc.name, @@ -150,13 +156,14 @@ export const secretFolderServiceFactory = ({ throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - project.id, + projectId: project.id, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); folders.forEach(({ environment, path: secretPath }) => { ForbiddenError.from(permission).throwUnlessCan( @@ -168,7 +175,7 @@ export const secretFolderServiceFactory = ({ const result = await folderDAL.transaction(async (tx) => Promise.all( folders.map(async (newFolder) => { - const { environment, path: secretPath, id, name } = newFolder; + const { environment, path: secretPath, id, name, description } = newFolder; const parentFolder = await folderDAL.findBySecretPath(project.id, environment, secretPath); if (!parentFolder) { @@ -215,7 +222,7 @@ export const secretFolderServiceFactory = ({ const [doc] = await folderDAL.update( { envId: env.id, id: folder.id, parentId: parentFolder.id }, - { name }, + { name, description }, tx ); await folderVersionDAL.create( @@ -257,15 +264,17 @@ export const secretFolderServiceFactory = ({ name, environment, path: secretPath, - id + id, + description }: TUpdateFolderDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, @@ -309,7 +318,7 @@ export const secretFolderServiceFactory = ({ const newFolder = await folderDAL.transaction(async (tx) => { const [doc] = await folderDAL.update( { envId: env.id, id: folder.id, parentId: parentFolder.id, isReserved: false }, - { name }, + { name, description }, tx ); await folderVersionDAL.create( @@ -339,13 +348,14 @@ export const secretFolderServiceFactory = ({ path: secretPath, idOrName }: TDeleteFolderDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, @@ -391,11 +401,20 @@ export const secretFolderServiceFactory = ({ orderBy, orderDirection, limit, - offset + offset, + recursive, + lastSecretModified }: TGetFolderDTO) => { // folder list is allowed to be read by anyone // permission to check does user has access - await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId); + await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); const env = await projectEnvDAL.findOne({ projectId, slug: environment }); if (!env) throw new NotFoundError({ message: `Environment with slug '${environment}' not found` }); @@ -403,6 +422,26 @@ export const secretFolderServiceFactory = ({ const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!parentFolder) return []; + if (recursive) { + const recursiveFolders = await folderDAL.findByEnvsDeep({ parentIds: [parentFolder.id] }); + // remove the parent folder + return recursiveFolders + .filter((folder) => { + if (lastSecretModified) { + if (!folder.lastSecretModified) return false; + + if (folder.lastSecretModified < new Date(lastSecretModified)) { + return false; + } + } + return folder.id !== parentFolder.id; + }) + .map((folder) => ({ + ...folder, + relativePath: folder.path + })); + } + const folders = await folderDAL.find( { envId: env.id, @@ -416,6 +455,11 @@ export const secretFolderServiceFactory = ({ offset } ); + if (lastSecretModified) { + return folders.filter((el) => + el.lastSecretModified ? el.lastSecretModified >= new Date(lastSecretModified) : false + ); + } return folders; }; @@ -432,7 +476,14 @@ export const secretFolderServiceFactory = ({ }: Omit & { environments: string[] }) => { // folder list is allowed to be read by anyone // permission to check does user has access - await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId); + await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); const envs = await projectEnvDAL.findBySlugs(projectId, environments); @@ -467,7 +518,14 @@ export const secretFolderServiceFactory = ({ }: Omit & { environments: string[] }) => { // folder list is allowed to be read by anyone // permission to check does user has access - await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId); + await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); const envs = await projectEnvDAL.findBySlugs(projectId, environments); @@ -496,7 +554,14 @@ export const secretFolderServiceFactory = ({ if (!folder) throw new NotFoundError({ message: `Folder with ID '${id}' not found` }); // folder list is allowed to be read by anyone // permission to check does user has access - await permissionService.getProjectPermission(actor, actorId, folder.projectId, actorAuthMethod, actorOrgId); + await permissionService.getProjectPermission({ + actor, + actorId, + projectId: folder.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(folder.projectId, [folder.id]); @@ -518,7 +583,14 @@ export const secretFolderServiceFactory = ({ ) => { // folder list is allowed to be read by anyone // permission to check does user have access - await permissionService.getProjectPermission(actor.type, actor.id, projectId, actor.authMethod, actor.orgId); + await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager + }); const envs = await projectEnvDAL.findBySlugs(projectId, environments); @@ -536,6 +608,63 @@ export const secretFolderServiceFactory = ({ return folders; }; + const getProjectEnvironmentsFolders = async (projectId: string, actor: OrgServiceActor) => { + // folder list is allowed to be read by anyone + // permission is to check if user has access + await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager + }); + + const environments = await projectEnvDAL.find({ projectId }); + + const folders = await folderDAL.find({ + $in: { + envId: environments.map((env) => env.id) + }, + isReserved: false + }); + + const environmentFolders = Object.fromEntries( + environments.map((env) => { + const relevantFolders = folders.filter((folder) => folder.envId === env.id); + const foldersMap = Object.fromEntries(relevantFolders.map((folder) => [folder.id, folder])); + + const foldersWithPath = relevantFolders + .map((folder) => { + try { + return { + ...folder, + path: buildFolderPath(folder, foldersMap) + }; + } catch (error) { + return null; + } + }) + .filter(Boolean) as { + path: string; + id: string; + createdAt: Date; + updatedAt: Date; + name: string; + envId: string; + version?: number | null | undefined; + parentId?: string | null | undefined; + isReserved?: boolean | undefined; + description?: string | undefined; + }[]; + + return [env.slug, { ...env, folders: foldersWithPath }]; + }) + ); + + return environmentFolders; + }; + return { createFolder, updateFolder, @@ -545,6 +674,7 @@ export const secretFolderServiceFactory = ({ getFolderById, getProjectFolderCount, getFoldersMultiEnv, - getFoldersDeepByEnvs + getFoldersDeepByEnvs, + getProjectEnvironmentsFolders }; }; diff --git a/backend/src/services/secret-folder/secret-folder-types.ts b/backend/src/services/secret-folder/secret-folder-types.ts index eb98809cd..4008676db 100644 --- a/backend/src/services/secret-folder/secret-folder-types.ts +++ b/backend/src/services/secret-folder/secret-folder-types.ts @@ -9,6 +9,7 @@ export type TCreateFolderDTO = { environment: string; path: string; name: string; + description?: string | null; } & TProjectPermission; export type TUpdateFolderDTO = { @@ -16,6 +17,7 @@ export type TUpdateFolderDTO = { path: string; id: string; name: string; + description?: string | null; } & TProjectPermission; export type TUpdateManyFoldersDTO = { @@ -25,6 +27,7 @@ export type TUpdateManyFoldersDTO = { path: string; id: string; name: string; + description?: string | null; }[]; } & Omit; @@ -42,6 +45,8 @@ export type TGetFolderDTO = { orderDirection?: OrderByDirection; limit?: number; offset?: number; + recursive?: boolean; + lastSecretModified?: string; } & TProjectPermission; export type TGetFolderByIdDTO = { diff --git a/backend/src/services/secret-import/secret-import-dal.ts b/backend/src/services/secret-import/secret-import-dal.ts index da25f4d30..1a171aa2e 100644 --- a/backend/src/services/secret-import/secret-import-dal.ts +++ b/backend/src/services/secret-import/secret-import-dal.ts @@ -5,6 +5,8 @@ import { TableName, TSecretImports } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; +import { EnvironmentInfo, FolderInfo, FolderResult, SecretResult } from "./secret-import-types"; + export type TSecretImportDALFactory = ReturnType; export const secretImportDALFactory = (db: TDbClient) => { @@ -169,6 +171,136 @@ export const secretImportDALFactory = (db: TDbClient) => { } }; + const getFolderIsImportedBy = async ( + secretPath: string, + environmentId: string, + environment: string, + projectId: string, + tx?: Knex + ) => { + try { + const folderImports = await (tx || db.replicaNode())(TableName.SecretImport) + .where({ importPath: secretPath, importEnv: environmentId }) + .join(TableName.SecretFolder, `${TableName.SecretImport}.folderId`, `${TableName.SecretFolder}.id`) + .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) + .select( + db.ref("name").withSchema(TableName.Environment).as("envName"), + db.ref("slug").withSchema(TableName.Environment).as("envSlug"), + db.ref("name").withSchema(TableName.SecretFolder).as("folderName"), + db.ref("id").withSchema(TableName.SecretFolder).as("folderId") + ); + + const secretReferences = await (tx || db.replicaNode())(TableName.SecretReferenceV2) + .where({ secretPath, environment }) + .join(TableName.SecretV2, `${TableName.SecretReferenceV2}.secretId`, `${TableName.SecretV2}.id`) + .join(TableName.SecretFolder, `${TableName.SecretV2}.folderId`, `${TableName.SecretFolder}.id`) + .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) + .where(`${TableName.Environment}.projectId`, projectId) + .where(`${TableName.SecretFolder}.isReserved`, false) + .select( + db.ref("key").withSchema(TableName.SecretV2).as("secretId"), + db.ref("name").withSchema(TableName.SecretFolder).as("folderName"), + db.ref("name").withSchema(TableName.Environment).as("envName"), + db.ref("slug").withSchema(TableName.Environment).as("envSlug"), + db.ref("id").withSchema(TableName.SecretFolder).as("folderId"), + db.ref("secretKey").withSchema(TableName.SecretReferenceV2).as("referencedSecretKey") + ); + + const folderResults = folderImports.map(({ envName, envSlug, folderName, folderId }) => ({ + envName, + envSlug, + folderName, + folderId + })); + + const secretResults = secretReferences.map( + ({ envName, envSlug, secretId, folderName, folderId, referencedSecretKey }) => ({ + envName, + envSlug, + secretId, + folderName, + folderId, + referencedSecretKey + }) + ); + + type ResultItem = FolderResult | SecretResult; + const allResults: ResultItem[] = [...folderResults, ...secretResults]; + + type EnvFolderMap = { + [envName: string]: { + envSlug: string; + folders: { + [folderName: string]: { + secrets: { + secretId: string; + referencedSecretKey: string; + }[]; + folderId: string; + folderImported: boolean; + }; + }; + }; + }; + + const groupedByEnv = allResults.reduce((acc, item) => { + const env = item.envName; + const folder = item.folderName; + const { envSlug } = item; + + const updatedAcc = { ...acc }; + + if (!updatedAcc[env]) { + updatedAcc[env] = { + envSlug, + folders: {} + }; + } + + if (!updatedAcc[env].folders[folder]) { + updatedAcc[env].folders[folder] = { secrets: [], folderId: item.folderId, folderImported: false }; + } + + if ("secretId" in item && item.secretId) { + updatedAcc[env].folders[folder].secrets = [ + ...updatedAcc[env].folders[folder].secrets, + { secretId: item.secretId, referencedSecretKey: item.referencedSecretKey } + ]; + } else { + updatedAcc[env].folders[folder].folderImported = true; + } + + return updatedAcc; + }, {}); + + const formattedResult: EnvironmentInfo[] = Object.keys(groupedByEnv).map((envName) => { + const envData = groupedByEnv[envName]; + + const folders: FolderInfo[] = Object.keys(envData.folders).map((folderName) => { + const folderData = envData.folders[folderName]; + const hasSecrets = folderData.secrets.length > 0; + + return { + folderName, + folderId: folderData.folderId, + folderImported: folderData.folderImported, + ...(hasSecrets && { secrets: folderData.secrets }) + }; + }); + + return { + envName, + envSlug: envData.envSlug, + folders + }; + }); + + return formattedResult; + } catch (error) { + throw new DatabaseError({ error, name: "GetSecretImportsAndReferences" }); + } + }; + return { ...secretImportOrm, find, @@ -176,6 +308,7 @@ export const secretImportDALFactory = (db: TDbClient) => { findByFolderIds, findLastImportPosition, updateAllPosition, - getProjectImportCount + getProjectImportCount, + getFolderIsImportedBy }; }; diff --git a/backend/src/services/secret-import/secret-import-fns.ts b/backend/src/services/secret-import/secret-import-fns.ts index d75a25514..c68033911 100644 --- a/backend/src/services/secret-import/secret-import-fns.ts +++ b/backend/src/services/secret-import/secret-import-fns.ts @@ -1,7 +1,9 @@ import { SecretType, TSecretImports, TSecrets, TSecretsV2 } from "@app/db/schemas"; import { groupBy, unique } from "@app/lib/fn"; +import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { TSecretDALFactory } from "../secret/secret-dal"; +import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; import { TSecretImportDALFactory } from "./secret-import-dal"; @@ -31,6 +33,12 @@ type TSecretImportSecretsV2 = { folderId: string | undefined; importFolderId: string; secrets: (TSecretsV2 & { + secretTags: { + slug: string; + name: string; + color?: string | null; + id: string; + }[]; workspace: string; environment: string; _id: string; @@ -38,7 +46,9 @@ type TSecretImportSecretsV2 = { // akhilmhdh: yes i know you can put ?. // But for somereason ts consider ? and undefined explicit as different just ts things secretValue: string; + secretValueHidden: boolean; secretComment: string; + secretMetadata?: ResourceMetadataDTO; })[]; }; @@ -148,12 +158,14 @@ export const fnSecretsV2FromImports = async ({ secretImportDAL, decryptor, expandSecretReferences, - hasSecretAccess + hasSecretAccess, + viewSecretValue }: { secretImports: (Omit & { importEnv: { id: string; slug: string; name: string }; })[]; folderDAL: Pick; + viewSecretValue: boolean; secretDAL: Pick; secretImportDAL: Pick; decryptor: (value?: Buffer | null) => string; @@ -166,9 +178,14 @@ export const fnSecretsV2FromImports = async ({ hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean; }) => { const cyclicDetector = new Set(); - const stack: { secretImports: typeof rootSecretImports; depth: number; parentImportedSecrets: TSecretsV2[] }[] = [ - { secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] } - ]; + const stack: { + secretImports: typeof rootSecretImports; + depth: number; + parentImportedSecrets: (TSecretsV2 & { + secretValueHidden: boolean; + secretTags: { slug: string; name: string; id: string; color?: string | null }[]; + })[]; + }[] = [{ secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] }]; const processedImports: TSecretImportSecretsV2[] = []; @@ -190,7 +207,10 @@ export const fnSecretsV2FromImports = async ({ ); if (!importedFolders.length) continue; - const importedFolderIds = importedFolders.map((el) => el?.id) as string[]; + const importedFolderIds = importedFolders.filter(Boolean).map((el) => el?.id) as string[]; + + if (!importedFolderIds.length) continue; + const importedFolderGroupBySourceImport = groupBy(importedFolders, (i) => `${i?.envId}-${i?.path}`); const importedSecrets = await secretDAL.find( @@ -227,7 +247,9 @@ export const fnSecretsV2FromImports = async ({ .map((item) => ({ ...item, secretKey: item.key, - secretValue: decryptor(item.encryptedValue), + secretValue: viewSecretValue ? decryptor(item.encryptedValue) : INFISICAL_SECRET_VALUE_HIDDEN_MASK, + secretValueHidden: !viewSecretValue, + secretTags: item.tags, secretComment: decryptor(item.encryptedComment), environment: importEnv.slug, workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend. @@ -265,6 +287,8 @@ export const fnSecretsV2FromImports = async ({ processedImport.secrets = unique(processedImport.secrets, (i) => i.key); return Promise.allSettled( processedImport.secrets.map(async (decryptedSecret, index) => { + if (decryptedSecret.secretValueHidden) return; + const expandedSecretValue = await expandSecretReferences({ value: decryptedSecret.secretValue, secretPath: processedImport.secretPath, diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index 25e78fb65..2015516f5 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -2,10 +2,18 @@ import path from "node:path"; import { ForbiddenError, subject } from "@casl/ability"; -import { TableName } from "@app/db/schemas"; +import { ActionProjectType, TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { + hasSecretReadValueOrDescribePermission, + throwIfMissingSecretReadValueOrDescribePermission +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { getReplicationFolderName } from "@app/ee/services/secret-replication/secret-replication-service"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -19,6 +27,7 @@ import { decryptSecretRaw } from "../secret/secret-fns"; import { TSecretQueueFactory } from "../secret/secret-queue"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; +import { recursivelyGetSecretPaths } from "../secret-v2-bridge/secret-v2-bridge-fns"; import { TSecretImportDALFactory } from "./secret-import-dal"; import { fnSecretsFromImports, fnSecretsV2FromImports } from "./secret-import-fns"; import { @@ -35,7 +44,7 @@ type TSecretImportServiceFactoryDep = { secretImportDAL: TSecretImportDALFactory; folderDAL: TSecretFolderDALFactory; secretDAL: Pick; - secretV2BridgeDAL: Pick; + secretV2BridgeDAL: Pick; projectBotService: Pick; projectDAL: Pick; projectEnvDAL: TProjectEnvDALFactory; @@ -73,13 +82,14 @@ export const secretImportServiceFactory = ({ isReplication, path: secretPath }: TCreateSecretImportDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); // check if user has permission to import into destination path ForbiddenError.from(permission).throwUnlessCan( @@ -88,13 +98,11 @@ export const secretImportServiceFactory = ({ ); // check if user has permission to import from target path - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: data.environment, - secretPath: data.path - }) - ); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment: data.environment, + secretPath: data.path + }); + if (isReplication) { const plan = await licenseService.getPlan(actorOrgId); if (!plan.secretApproval) { @@ -159,6 +167,7 @@ export const secretImportServiceFactory = ({ if (secImport.isReplication && sourceFolder) { await secretQueueService.replicateSecrets({ secretPath: secImport.importPath, + orgId: actorOrgId, projectId, environmentSlug: importEnv.slug, pickOnlyImportIds: [secImport.id], @@ -168,6 +177,7 @@ export const secretImportServiceFactory = ({ } else { await secretQueueService.syncSecrets({ secretPath, + orgId: actorOrgId, projectId, environmentSlug: environment, actorId, @@ -175,6 +185,7 @@ export const secretImportServiceFactory = ({ }); } + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId); return { ...secImport, importEnv }; }; @@ -189,13 +200,15 @@ export const secretImportServiceFactory = ({ data, id }: TUpdateSecretImportDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) @@ -270,6 +283,8 @@ export const secretImportServiceFactory = ({ ); return doc; }); + + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId); return { ...updatedSecImport, importEnv: importedEnv }; }; @@ -283,13 +298,15 @@ export const secretImportServiceFactory = ({ actorAuthMethod, id }: TDeleteSecretImportDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) @@ -335,12 +352,14 @@ export const secretImportServiceFactory = ({ await secretQueueService.syncSecrets({ secretPath, + orgId: actorOrgId, projectId, environmentSlug: environment, actor, actorId }); + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId); return secImport; }; @@ -354,13 +373,14 @@ export const secretImportServiceFactory = ({ path: secretPath, id: secretImportDocId }: TResyncSecretImportReplicationDTO) => { - const { permission, membership } = await permissionService.getProjectPermission( + const { permission, membership } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); // check if user has permission to import into destination path ForbiddenError.from(permission).throwUnlessCan( @@ -392,13 +412,10 @@ export const secretImportServiceFactory = ({ if (!secretImportDoc.isReplication) throw new BadRequestError({ message: "Import is not in replication mode" }); // check if user has permission to import from target path - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: secretImportDoc.importEnv.slug, - secretPath: secretImportDoc.importPath - }) - ); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment: secretImportDoc.importEnv.slug, + secretPath: secretImportDoc.importPath + }); await projectDAL.checkProjectUpgradeStatus(projectId); @@ -410,6 +427,7 @@ export const secretImportServiceFactory = ({ if (membership && sourceFolder) { await secretQueueService.replicateSecrets({ + orgId: actorOrgId, secretPath: secretImportDoc.importPath, projectId, environmentSlug: secretImportDoc.importEnv.slug, @@ -432,13 +450,14 @@ export const secretImportServiceFactory = ({ actorOrgId, search }: TGetSecretImportsDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) @@ -455,6 +474,58 @@ export const secretImportServiceFactory = ({ return count; }; + const getProjectImportMultiEnvCount = async ({ + path: secretPath, + environments, + projectId, + actor, + actorId, + actorAuthMethod, + actorOrgId, + search + }: Omit & { environments: string[] }) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + const filteredEnvironments = []; + for (const environment of environments) { + if ( + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) + ) + ) { + filteredEnvironments.push(environment); + } + } + if (filteredEnvironments.length === 0) { + return 0; + } + + for (const environment of filteredEnvironments) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) + ); + } + + const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, secretPath); + if (!folders?.length) + throw new NotFoundError({ + message: `Folder with path '${secretPath}' not found on environments with slugs '${environments.join(", ")}'` + }); + const counts = await Promise.all( + folders.map((folder) => secretImportDAL.getProjectImportCount({ folderId: folder.id, search })) + ); + + return counts.reduce((sum, count) => sum + count, 0); + }; + const getImports = async ({ path: secretPath, environment, @@ -467,13 +538,14 @@ export const secretImportServiceFactory = ({ limit, offset }: TGetSecretImportsDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) @@ -516,13 +588,14 @@ export const secretImportServiceFactory = ({ }); } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - folder.projectId, + projectId: folder.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, @@ -564,13 +637,14 @@ export const secretImportServiceFactory = ({ actorId, actorOrgId }: TGetSecretsFromImportDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) @@ -581,14 +655,12 @@ export const secretImportServiceFactory = ({ // so anything based on this order will also be in right position const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false }); const allowedImports = secretImports.filter((el) => - permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: el.importEnv.slug, - secretPath: el.importPath - }) - ) + hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment: el.importEnv.slug, + secretPath: el.importPath + }) ); + return fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL }); }; @@ -601,13 +673,14 @@ export const secretImportServiceFactory = ({ actorId, actorOrgId }: TGetSecretsFromImportDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) @@ -627,20 +700,19 @@ export const secretImportServiceFactory = ({ const importedSecrets = await fnSecretsV2FromImports({ secretImports, folderDAL, + viewSecretValue: true, secretDAL: secretV2BridgeDAL, secretImportDAL, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => - permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) - ) + hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }) }); + return importedSecrets; } @@ -651,13 +723,10 @@ export const secretImportServiceFactory = ({ }); const allowedImports = secretImports.filter((el) => - permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: el.importEnv.slug, - secretPath: el.importPath - }) - ) + hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment: el.importEnv.slug, + secretPath: el.importPath + }) ); const importedSecrets = await fnSecretsFromImports({ allowedImports, @@ -668,11 +737,201 @@ export const secretImportServiceFactory = ({ return importedSecrets.map((el) => ({ ...el, secrets: el.secrets.map((encryptedSecret) => - decryptSecretRaw({ ...encryptedSecret, workspace: projectId, environment, secretPath }, botKey) + decryptSecretRaw( + { ...encryptedSecret, workspace: projectId, environment, secretPath, secretValueHidden: false }, + botKey + ) ) })); }; + const getImportsMultiEnv = async ({ + path: secretPath, + environments, + projectId, + actor, + actorId, + actorAuthMethod, + actorOrgId, + search, + limit, + offset + }: Omit & { environments: string[] }) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + const filteredEnvironments = []; + for (const environment of environments) { + if ( + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) + ) + ) { + filteredEnvironments.push(environment); + } + } + if (filteredEnvironments.length === 0) { + return []; + } + + const folders = await folderDAL.findBySecretPathMultiEnv(projectId, filteredEnvironments, secretPath); + if (!folders?.length) + throw new NotFoundError({ + message: `Folder with path '${secretPath}' not found on environments with slugs '${environments.join(", ")}'` + }); + + const secImportsArrays = await Promise.all( + folders.map(async (folder) => { + const imports = await secretImportDAL.find({ folderId: folder.id, search, limit, offset }); + return imports.map((importItem) => ({ + ...importItem, + environment: folder.environment.slug + })); + }) + ); + return secImportsArrays.flat(); + }; + + const getFolderIsImportedBy = async ({ + path: secretPath, + environment, + projectId, + actor, + actorId, + actorAuthMethod, + actorOrgId, + secrets + }: TGetSecretImportsDTO & { + secrets: { secretKey: string; secretValue: string }[] | undefined; + }) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + if ( + permission.cannot( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) + ) + ) { + return []; + } + + const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); + if (!folder) return []; + + const importedBy = await secretImportDAL.getFolderIsImportedBy(secretPath, folder.envId, environment, projectId); + const deepPaths: { path: string; folderId: string }[] = []; + + await Promise.all( + importedBy.map(async (el) => { + const envDeepPaths = await recursivelyGetSecretPaths({ + folderDAL, + projectEnvDAL, + projectId, + environment: el.envSlug, + currentPath: "/" + }); + deepPaths.push(...envDeepPaths); + }) + ); + + const result = importedBy.map((el) => ({ + environment: { + name: el.envName, + slug: el.envSlug + }, + folders: el.folders.map((folderItem) => ({ + folderId: folderItem.folderId, + isImported: folderItem.folderImported, + secrets: folderItem.secrets, + name: deepPaths.find((p) => p.folderId === folderItem.folderId)?.path || `...${folderItem.folderName}` + })) + })); + + // Special case for same folder references as these do not have an entry on the references table + const locallyReferenced = + secrets + ?.filter((secret) => { + return secrets.some( + (otherSecret) => + otherSecret.secretKey !== secret.secretKey && secret.secretValue.includes(`\${${otherSecret.secretKey}}`) + ); + }) + .flatMap((secret) => { + return secrets + .filter( + (otherSecret) => + otherSecret.secretKey !== secret.secretKey && + secret.secretValue.includes(`\${${otherSecret.secretKey}}`) + ) + .map((otherSecret) => ({ + secretId: secret.secretKey, + referencedSecretKey: otherSecret.secretKey + })); + }) || []; + if (locallyReferenced.length > 0) { + const existingEnvIndex = result.findIndex((item) => item.environment.slug === environment); + + if (existingEnvIndex >= 0) { + const existingFolderIndex = result[existingEnvIndex].folders.findIndex( + (folderItem) => folderItem.name === secretPath + ); + + if (existingFolderIndex >= 0) { + if (!result[existingEnvIndex].folders[existingFolderIndex].secrets) { + result[existingEnvIndex].folders[existingFolderIndex].secrets = []; + } + + const existingSecrets = result[existingEnvIndex].folders[existingFolderIndex].secrets || []; + locallyReferenced.forEach((ref) => { + if ( + !existingSecrets.some( + (s) => s.secretId === ref.secretId && s.referencedSecretKey === ref.referencedSecretKey + ) + ) { + existingSecrets.push(ref); + } + }); + } else { + result[existingEnvIndex].folders.push({ + folderId: folder.id, + isImported: false, + secrets: locallyReferenced, + name: secretPath + }); + } + } else { + result.push({ + environment: { + slug: environment, + name: environment + }, + folders: [ + { + folderId: folder.id, + isImported: false, + secrets: locallyReferenced, + name: secretPath + } + ] + }); + } + } + + return result; + }; + return { createImport, updateImport, @@ -683,6 +942,9 @@ export const secretImportServiceFactory = ({ getRawSecretsFromImports, resyncSecretImportReplication, getProjectImportCount, - fnSecretsFromImports + fnSecretsFromImports, + getProjectImportMultiEnvCount, + getImportsMultiEnv, + getFolderIsImportedBy }; }; diff --git a/backend/src/services/secret-import/secret-import-types.ts b/backend/src/services/secret-import/secret-import-types.ts index 638e36cb1..e4490e715 100644 --- a/backend/src/services/secret-import/secret-import-types.ts +++ b/backend/src/services/secret-import/secret-import-types.ts @@ -45,3 +45,29 @@ export type TGetSecretsFromImportDTO = { environment: string; path: string; } & TProjectPermission; + +export type FolderResult = { + envName: string; + folderName: string; + folderId: string; + envSlug: string; +}; + +export type SecretResult = { + secretId: string; + referencedSecretKey: string; +} & FolderResult; + +export type FolderInfo = { + folderName: string; + secrets?: { secretId: string; referencedSecretKey: string }[]; + folderId: string; + folderImported: boolean; + envSlug?: string; +}; + +export type EnvironmentInfo = { + envName: string; + envSlug: string; + folders: FolderInfo[]; +}; diff --git a/backend/src/services/secret-sharing/secret-sharing-dal.ts b/backend/src/services/secret-sharing/secret-sharing-dal.ts index 5c690b266..7cdccd4f8 100644 --- a/backend/src/services/secret-sharing/secret-sharing-dal.ts +++ b/backend/src/services/secret-sharing/secret-sharing-dal.ts @@ -2,17 +2,61 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { TableName, TSecretSharing } from "@app/db/schemas"; -import { DatabaseError } from "@app/lib/errors"; +import { DatabaseError, NotFoundError } from "@app/lib/errors"; import { ormify, selectAllTableCols } from "@app/lib/knex"; import { logger } from "@app/lib/logger"; import { QueueName } from "@app/queue"; +import { SecretSharingType } from "./secret-sharing-types"; + export type TSecretSharingDALFactory = ReturnType; export const secretSharingDALFactory = (db: TDbClient) => { const sharedSecretOrm = ormify(db, TableName.SecretSharing); - const countAllUserOrgSharedSecrets = async ({ orgId, userId }: { orgId: string; userId: string }) => { + const getSecretRequestById = async (id: string) => { + const repDb = db.replicaNode(); + + const secretRequest = await repDb(TableName.SecretSharing) + .leftJoin(TableName.Organization, `${TableName.Organization}.id`, `${TableName.SecretSharing}.orgId`) + .leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SecretSharing}.userId`) + .where(`${TableName.SecretSharing}.id`, id) + .where(`${TableName.SecretSharing}.type`, SecretSharingType.Request) + .select( + repDb.ref("name").withSchema(TableName.Organization).as("orgName"), + repDb.ref("firstName").withSchema(TableName.Users).as("requesterFirstName"), + repDb.ref("lastName").withSchema(TableName.Users).as("requesterLastName"), + repDb.ref("username").withSchema(TableName.Users).as("requesterUsername") + ) + .select(selectAllTableCols(TableName.SecretSharing)) + .first(); + + if (!secretRequest) { + throw new NotFoundError({ + message: `Secret request with ID '${id}' not found` + }); + } + + return { + ...secretRequest, + requester: { + organizationName: secretRequest.orgName, + firstName: secretRequest.requesterFirstName, + lastName: secretRequest.requesterLastName, + username: secretRequest.requesterUsername + } + }; + }; + + const countAllUserOrgSharedSecrets = async ({ + orgId, + userId, + type + }: { + orgId: string; + userId: string; + type: SecretSharingType; + }) => { try { interface CountResult { count: string; @@ -22,6 +66,7 @@ export const secretSharingDALFactory = (db: TDbClient) => { .replicaNode()(TableName.SecretSharing) .where(`${TableName.SecretSharing}.orgId`, orgId) .where(`${TableName.SecretSharing}.userId`, userId) + .where(`${TableName.SecretSharing}.type`, type) .count("*") .first(); @@ -38,6 +83,7 @@ export const secretSharingDALFactory = (db: TDbClient) => { const docs = await (tx || db)(TableName.SecretSharing) .where("expiresAt", "<", today) .andWhere("encryptedValue", "<>", "") + .andWhere("type", SecretSharingType.Share) .update({ encryptedValue: "", tag: "", @@ -50,6 +96,26 @@ export const secretSharingDALFactory = (db: TDbClient) => { } }; + const pruneExpiredSecretRequests = async (tx?: Knex) => { + logger.info(`${QueueName.DailyResourceCleanUp}: pruning expired secret requests started`); + try { + const today = new Date(); + + const docs = await (tx || db)(TableName.SecretSharing) + .whereNotNull("expiresAt") + .andWhere("expiresAt", "<", today) + .andWhere("encryptedSecret", null) + .andWhere("type", SecretSharingType.Request) + .delete(); + + logger.info(`${QueueName.DailyResourceCleanUp}: pruning expired secret requests completed`); + + return docs; + } catch (error) { + throw new DatabaseError({ error, name: "pruneExpiredSecretRequests" }); + } + }; + const findActiveSharedSecrets = async (filters: Partial, tx?: Knex) => { try { const now = new Date(); @@ -57,6 +123,7 @@ export const secretSharingDALFactory = (db: TDbClient) => { .where(filters) .andWhere("expiresAt", ">", now) .andWhere("encryptedValue", "<>", "") + .andWhere("type", SecretSharingType.Share) .select(selectAllTableCols(TableName.SecretSharing)) .orderBy("expiresAt", "asc"); } catch (error) { @@ -86,7 +153,9 @@ export const secretSharingDALFactory = (db: TDbClient) => { ...sharedSecretOrm, countAllUserOrgSharedSecrets, pruneExpiredSharedSecrets, + pruneExpiredSecretRequests, softDeleteById, - findActiveSharedSecrets + findActiveSharedSecrets, + getSecretRequestById }; }; diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index 171ab54db..9649be722 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -1,41 +1,70 @@ import crypto from "node:crypto"; import bcrypt from "bcrypt"; -import { z } from "zod"; import { TSecretSharing } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { SecretSharingAccessType } from "@app/lib/types"; +import { isUuidV4 } from "@app/lib/validator"; import { TKmsServiceFactory } from "../kms/kms-service"; import { TOrgDALFactory } from "../org/org-dal"; +import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; +import { TUserDALFactory } from "../user/user-dal"; import { TSecretSharingDALFactory } from "./secret-sharing-dal"; import { + SecretSharingType, TCreatePublicSharedSecretDTO, + TCreateSecretRequestDTO, TCreateSharedSecretDTO, TDeleteSharedSecretDTO, TGetActiveSharedSecretByIdDTO, - TGetSharedSecretsDTO + TGetSecretRequestByIdDTO, + TGetSharedSecretsDTO, + TRevealSecretRequestValueDTO, + TSetSecretRequestValueDTO } from "./secret-sharing-types"; type TSecretSharingServiceFactoryDep = { permissionService: Pick; secretSharingDAL: TSecretSharingDALFactory; orgDAL: TOrgDALFactory; + userDAL: TUserDALFactory; kmsService: TKmsServiceFactory; + smtpService: TSmtpService; }; export type TSecretSharingServiceFactory = ReturnType; -const isUuidV4 = (uuid: string) => z.string().uuid().safeParse(uuid).success; - export const secretSharingServiceFactory = ({ permissionService, secretSharingDAL, orgDAL, - kmsService + kmsService, + smtpService, + userDAL }: TSecretSharingServiceFactoryDep) => { + const $validateSharedSecretExpiry = (expiresAt: string) => { + if (new Date(expiresAt) < new Date()) { + throw new BadRequestError({ message: "Expiration date cannot be in the past" }); + } + + // Limit Expiry Time to 1 month + const expiryTime = new Date(expiresAt).getTime(); + const currentTime = new Date().getTime(); + const thirtyDays = 30 * 24 * 60 * 60 * 1000; + if (expiryTime - currentTime > thirtyDays) { + throw new BadRequestError({ message: "Expiration date cannot be more than 30 days" }); + } + + const fiveMins = 5 * 60 * 1000; + if (expiryTime - currentTime < fiveMins) { + throw new BadRequestError({ message: "Expiration time cannot be less than 5 mins" }); + } + }; + const createSharedSecret = async ({ actor, actorId, @@ -51,17 +80,13 @@ export const secretSharingServiceFactory = ({ }: TCreateSharedSecretDTO) => { const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" }); + $validateSharedSecretExpiry(expiresAt); - if (new Date(expiresAt) < new Date()) { - throw new BadRequestError({ message: "Expiration date cannot be in the past" }); - } - - // Limit Expiry Time to 1 month - const expiryTime = new Date(expiresAt).getTime(); - const currentTime = new Date().getTime(); - const thirtyDays = 30 * 24 * 60 * 60 * 1000; - if (expiryTime - currentTime > thirtyDays) { - throw new BadRequestError({ message: "Expiration date cannot be more than 30 days" }); + const org = await orgDAL.findOrgById(orgId); + if (!org.allowSecretSharingOutsideOrganization && accessType === SecretSharingAccessType.Anyone) { + throw new BadRequestError({ + message: "Organization does not allow sharing secrets to members outside of this organization" + }); } if (secretValue.length > 10_000) { @@ -69,7 +94,6 @@ export const secretSharingServiceFactory = ({ } const encryptWithRoot = kmsService.encryptWithRootKey(); - const encryptedSecret = encryptWithRoot(Buffer.from(secretValue)); const id = crypto.randomBytes(32).toString("hex"); @@ -82,6 +106,7 @@ export const secretSharingServiceFactory = ({ encryptedValue: null, encryptedSecret, name, + type: SecretSharingType.Share, password: hashedPassword, expiresAt: new Date(expiresAt), expiresAfterViews, @@ -95,6 +120,191 @@ export const secretSharingServiceFactory = ({ return { id: idToReturn }; }; + const createSecretRequest = async ({ + actor, + accessType, + expiresAt, + name, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }: TCreateSecretRequestDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" }); + + $validateSharedSecretExpiry(expiresAt); + + const newSecretRequest = await secretSharingDAL.create({ + type: SecretSharingType.Request, + userId: actorId, + orgId, + name, + encryptedSecret: null, + accessType, + expiresAt: new Date(expiresAt) + }); + + return { id: newSecretRequest.id }; + }; + + const revealSecretRequestValue = async ({ + id, + actor, + actorId, + actorOrgId, + orgId, + actorAuthMethod + }: TRevealSecretRequestValueDTO) => { + const secretRequest = await secretSharingDAL.getSecretRequestById(id); + + if (!secretRequest) { + throw new NotFoundError({ message: `Secret request with ID '${id}' not found` }); + } + + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" }); + + if (secretRequest.userId !== actorId || secretRequest.orgId !== orgId) { + throw new ForbiddenRequestError({ name: "User does not have permission to access this secret request" }); + } + + if (!secretRequest.encryptedSecret) { + throw new BadRequestError({ message: "Secret request has no value set" }); + } + + const decryptWithRoot = kmsService.decryptWithRootKey(); + const decryptedSecret = decryptWithRoot(secretRequest.encryptedSecret); + + return { ...secretRequest, secretValue: decryptedSecret.toString() }; + }; + + const getSecretRequestById = async ({ + id, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TGetSecretRequestByIdDTO) => { + const secretRequest = await secretSharingDAL.getSecretRequestById(id); + + if (!secretRequest) { + throw new NotFoundError({ message: `Secret request with ID '${id}' not found` }); + } + + if (secretRequest.accessType === SecretSharingAccessType.Organization) { + if (!secretRequest.orgId) { + throw new BadRequestError({ message: "No organization ID present on secret request" }); + } + + if (!actorOrgId) { + throw new UnauthorizedError(); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + secretRequest.orgId, + actorAuthMethod, + actorOrgId + ); + if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" }); + } + + if (secretRequest.expiresAt && secretRequest.expiresAt < new Date()) { + throw new ForbiddenRequestError({ + message: "Access denied: Secret request has expired" + }); + } + + return { + ...secretRequest, + isSecretValueSet: Boolean(secretRequest.encryptedSecret) + }; + }; + + const setSecretRequestValue = async ({ + id, + actor, + actorId, + actorAuthMethod, + actorOrgId, + secretValue + }: TSetSecretRequestValueDTO) => { + const appCfg = getConfig(); + + const secretRequest = await secretSharingDAL.getSecretRequestById(id); + + if (!secretRequest) { + throw new NotFoundError({ message: `Secret request with ID '${id}' not found` }); + } + + let respondentUsername: string | undefined; + + if (secretRequest.accessType === SecretSharingAccessType.Organization) { + if (!secretRequest.orgId) { + throw new BadRequestError({ message: "No organization ID present on secret request" }); + } + + if (!actorOrgId) { + throw new UnauthorizedError(); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + secretRequest.orgId, + actorAuthMethod, + actorOrgId + ); + if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" }); + + const user = await userDAL.findById(actorId); + + if (!user) { + throw new NotFoundError({ message: `User with ID '${actorId}' not found` }); + } + + respondentUsername = user.username; + } + + if (secretRequest.encryptedSecret) { + throw new BadRequestError({ message: "Secret request already has a value set" }); + } + + if (secretValue.length > 10_000) { + throw new BadRequestError({ message: "Shared secret value too long" }); + } + + if (secretRequest.expiresAt && secretRequest.expiresAt < new Date()) { + throw new ForbiddenRequestError({ + message: "Access denied: Secret request has expired" + }); + } + + const encryptWithRoot = kmsService.encryptWithRootKey(); + const encryptedSecret = encryptWithRoot(Buffer.from(secretValue)); + + const request = await secretSharingDAL.transaction(async (tx) => { + const updatedRequest = await secretSharingDAL.updateById(id, { encryptedSecret }, tx); + + await smtpService.sendMail({ + recipients: [secretRequest.requesterUsername], + subjectLine: "Secret Request Completed", + substitutions: { + name: secretRequest.name, + respondentUsername, + secretRequestUrl: `${appCfg.SITE_URL}/organization/secret-sharing?selectedTab=request-secret` + }, + template: SmtpTemplates.SecretRequestCompleted + }); + + return updatedRequest; + }); + + return request; + }; + const createPublicSharedSecret = async ({ password, secretValue, @@ -102,17 +312,7 @@ export const secretSharingServiceFactory = ({ expiresAfterViews, accessType }: TCreatePublicSharedSecretDTO) => { - if (new Date(expiresAt) < new Date()) { - throw new BadRequestError({ message: "Expiration date cannot be in the past" }); - } - - // Limit Expiry Time to 1 month - const expiryTime = new Date(expiresAt).getTime(); - const currentTime = new Date().getTime(); - const thirtyDays = 30 * 24 * 60 * 60 * 1000; - if (expiryTime - currentTime > thirtyDays) { - throw new BadRequestError({ message: "Expiration date cannot exceed more than 30 days" }); - } + $validateSharedSecretExpiry(expiresAt); const encryptWithRoot = kmsService.encryptWithRootKey(); const encryptedSecret = encryptWithRoot(Buffer.from(secretValue)); @@ -125,6 +325,7 @@ export const secretSharingServiceFactory = ({ encryptedValue: null, iv: null, tag: null, + type: SecretSharingType.Share, encryptedSecret, password: hashedPassword, expiresAt: new Date(expiresAt), @@ -141,7 +342,8 @@ export const secretSharingServiceFactory = ({ actorAuthMethod, actorOrgId, offset, - limit + limit, + type }: TGetSharedSecretsDTO) => { if (!actorOrgId) throw new ForbiddenRequestError(); @@ -157,14 +359,16 @@ export const secretSharingServiceFactory = ({ const secrets = await secretSharingDAL.find( { userId: actorId, - orgId: actorOrgId + orgId: actorOrgId, + type }, { offset, limit, sort: [["createdAt", "desc"]] } ); const count = await secretSharingDAL.countAllUserOrgSharedSecrets({ orgId: actorOrgId, - userId: actorId + userId: actorId, + type }); return { @@ -191,9 +395,11 @@ export const secretSharingServiceFactory = ({ const sharedSecret = isUuidV4(sharedSecretId) ? await secretSharingDAL.findOne({ id: sharedSecretId, + type: SecretSharingType.Share, hashedHex }) : await secretSharingDAL.findOne({ + type: SecretSharingType.Share, identifier: Buffer.from(sharedSecretId, "base64url").toString("hex") }); @@ -206,8 +412,13 @@ export const secretSharingServiceFactory = ({ const orgName = sharedSecret.orgId ? (await orgDAL.findOrgById(sharedSecret.orgId))?.name : ""; - if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) + if (accessType === SecretSharingAccessType.Organization && orgId === undefined) { + throw new UnauthorizedError(); + } + + if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) { throw new ForbiddenRequestError(); + } // all secrets pass through here, meaning we check if its expired first and then check if it needs verification // or can be safely sent to the client. @@ -253,7 +464,7 @@ export const secretSharingServiceFactory = ({ secret: { ...sharedSecret, ...(decryptedSecretValue && { - secretValue: Buffer.from(decryptedSecretValue).toString() + secretValue: decryptedSecretValue.toString() }), orgName: sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId @@ -269,11 +480,17 @@ export const secretSharingServiceFactory = ({ if (!permission) throw new ForbiddenRequestError({ name: "User does not belong to the specified organization" }); const sharedSecret = isUuidV4(sharedSecretId) - ? await secretSharingDAL.findById(sharedSecretId) - : await secretSharingDAL.findOne({ identifier: sharedSecretId }); + ? await secretSharingDAL.findOne({ id: sharedSecretId, type: deleteSharedSecretInput.type }) + : await secretSharingDAL.findOne({ identifier: sharedSecretId, type: deleteSharedSecretInput.type }); - if (sharedSecret.orgId && sharedSecret.orgId !== orgId) + if (sharedSecret.userId !== actorId) { + throw new ForbiddenRequestError({ + message: "User does not have permission to delete shared secret" + }); + } + if (sharedSecret.orgId && sharedSecret.orgId !== orgId) { throw new ForbiddenRequestError({ message: "User does not have permission to delete shared secret" }); + } const deletedSharedSecret = await secretSharingDAL.deleteById(sharedSecretId); @@ -285,6 +502,11 @@ export const secretSharingServiceFactory = ({ createPublicSharedSecret, getSharedSecrets, deleteSharedSecretById, - getSharedSecretById + getSharedSecretById, + + createSecretRequest, + getSecretRequestById, + setSecretRequestValue, + revealSecretRequestValue }; }; diff --git a/backend/src/services/secret-sharing/secret-sharing-types.ts b/backend/src/services/secret-sharing/secret-sharing-types.ts index 1d9efa1e3..835d70eff 100644 --- a/backend/src/services/secret-sharing/secret-sharing-types.ts +++ b/backend/src/services/secret-sharing/secret-sharing-types.ts @@ -1,8 +1,14 @@ -import { SecretSharingAccessType, TGenericPermission } from "@app/lib/types"; +import { SecretSharingAccessType, TGenericPermission, TOrgPermission } from "@app/lib/types"; import { ActorAuthMethod, ActorType } from "../auth/auth-type"; +export enum SecretSharingType { + Share = "share", + Request = "request" +} + export type TGetSharedSecretsDTO = { + type: SecretSharingType; offset: number; limit: number; } & TGenericPermission; @@ -39,6 +45,26 @@ export type TValidateActiveSharedSecretDTO = TGetActiveSharedSecretByIdDTO & { export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO; +export type TCreateSecretRequestDTO = { + name?: string; + accessType: SecretSharingAccessType; + expiresAt: string; +} & TOrgPermission; + +export type TRevealSecretRequestValueDTO = { + id: string; +} & TOrgPermission; + +export type TGetSecretRequestByIdDTO = { + id: string; +} & Omit; + +export type TSetSecretRequestValueDTO = { + id: string; + secretValue: string; +} & Omit; + export type TDeleteSharedSecretDTO = { sharedSecretId: string; + type: SecretSharingType; } & TSharedSecretPermission; diff --git a/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-constants.ts b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-constants.ts new file mode 100644 index 000000000..37605442d --- /dev/null +++ b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const AWS_PARAMETER_STORE_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "AWS Parameter Store", + destination: SecretSync.AWSParameterStore, + connection: AppConnection.AWS, + canImportSecrets: true +}; diff --git a/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts new file mode 100644 index 000000000..7e77bd256 --- /dev/null +++ b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts @@ -0,0 +1,429 @@ +import AWS, { AWSError } from "aws-sdk"; + +import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; + +import { TAwsParameterStoreSyncWithCredentials } from "./aws-parameter-store-sync-types"; + +type TAWSParameterStoreRecord = Record; +type TAWSParameterStoreMetadataRecord = Record; +type TAWSParameterStoreTagsRecord = Record>; + +const MAX_RETRIES = 5; +const BATCH_SIZE = 10; + +const getSSM = async (secretSync: TAwsParameterStoreSyncWithCredentials) => { + const { destinationConfig, connection } = secretSync; + + const config = await getAwsConnectionConfig(connection, destinationConfig.region); + + const ssm = new AWS.SSM({ + apiVersion: "2014-11-06", + region: destinationConfig.region + }); + + ssm.config.update(config); + + return ssm; +}; + +const sleep = async () => + new Promise((resolve) => { + setTimeout(resolve, 1000); + }); + +const getParametersByPath = async (ssm: AWS.SSM, path: string): Promise => { + const awsParameterStoreSecretsRecord: TAWSParameterStoreRecord = {}; + let hasNext = true; + let nextToken: string | undefined; + let attempt = 0; + + while (hasNext) { + try { + // eslint-disable-next-line no-await-in-loop + const parameters = await ssm + .getParametersByPath({ + Path: path, + Recursive: false, + WithDecryption: true, + MaxResults: BATCH_SIZE, + NextToken: nextToken + }) + .promise(); + + attempt = 0; + + if (parameters.Parameters) { + parameters.Parameters.forEach((parameter) => { + if (parameter.Name) { + // no leading slash if path is '/' + const secKey = path.length > 1 ? parameter.Name.substring(path.length) : parameter.Name; + awsParameterStoreSecretsRecord[secKey] = parameter; + } + }); + } + + hasNext = Boolean(parameters.NextToken); + nextToken = parameters.NextToken; + } catch (e) { + if ((e as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + attempt += 1; + // eslint-disable-next-line no-await-in-loop + await sleep(); + // eslint-disable-next-line no-continue + continue; + } + + throw e; + } + } + + return awsParameterStoreSecretsRecord; +}; + +const getParameterMetadataByPath = async (ssm: AWS.SSM, path: string): Promise => { + const awsParameterStoreMetadataRecord: TAWSParameterStoreMetadataRecord = {}; + let hasNext = true; + let nextToken: string | undefined; + let attempt = 0; + + while (hasNext) { + try { + // eslint-disable-next-line no-await-in-loop + const parameters = await ssm + .describeParameters({ + MaxResults: 10, + NextToken: nextToken, + ParameterFilters: [ + { + Key: "Path", + Option: "OneLevel", + Values: [path] + } + ] + }) + .promise(); + + attempt = 0; + + if (parameters.Parameters) { + parameters.Parameters.forEach((parameter) => { + if (parameter.Name) { + // no leading slash if path is '/' + const secKey = path.length > 1 ? parameter.Name.substring(path.length) : parameter.Name; + awsParameterStoreMetadataRecord[secKey] = parameter; + } + }); + } + + hasNext = Boolean(parameters.NextToken); + nextToken = parameters.NextToken; + } catch (e) { + if ((e as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + attempt += 1; + // eslint-disable-next-line no-await-in-loop + await sleep(); + // eslint-disable-next-line no-continue + continue; + } + + throw e; + } + } + + return awsParameterStoreMetadataRecord; +}; + +const getParameterStoreTagsRecord = async ( + ssm: AWS.SSM, + awsParameterStoreSecretsRecord: TAWSParameterStoreRecord, + needsTagsPermissions: boolean +): Promise<{ shouldManageTags: boolean; awsParameterStoreTagsRecord: TAWSParameterStoreTagsRecord }> => { + const awsParameterStoreTagsRecord: TAWSParameterStoreTagsRecord = {}; + + for await (const entry of Object.entries(awsParameterStoreSecretsRecord)) { + const [key, parameter] = entry; + + if (!parameter.Name) { + // eslint-disable-next-line no-continue + continue; + } + + try { + const tags = await ssm + .listTagsForResource({ + ResourceType: "Parameter", + ResourceId: parameter.Name + }) + .promise(); + + awsParameterStoreTagsRecord[key] = Object.fromEntries(tags.TagList?.map((tag) => [tag.Key, tag.Value]) ?? []); + } catch (e) { + // users aren't required to provide tag permissions to use sync so we handle gracefully if unauthorized + // and they aren't trying to configure tags + if ((e as AWSError).code === "AccessDeniedException") { + if (!needsTagsPermissions) { + return { shouldManageTags: false, awsParameterStoreTagsRecord: {} }; + } + + throw new SecretSyncError({ + message: + "IAM role has inadequate permissions to manage resource tags. Ensure the following polices are present: ssm:ListTagsForResource, ssm:AddTagsToResource, and ssm:RemoveTagsFromResource", + shouldRetry: false + }); + } + + throw e; + } + } + + return { shouldManageTags: true, awsParameterStoreTagsRecord }; +}; + +const processParameterTags = ({ + syncTagsRecord, + awsTagsRecord +}: { + syncTagsRecord: Record; + awsTagsRecord: Record; +}) => { + const tagsToAdd: AWS.SSM.TagList = []; + const tagKeysToRemove: string[] = []; + + for (const syncEntry of Object.entries(syncTagsRecord)) { + const [syncKey, syncValue] = syncEntry; + + if (!(syncKey in awsTagsRecord) || syncValue !== awsTagsRecord[syncKey]) + tagsToAdd.push({ Key: syncKey, Value: syncValue }); + } + + for (const awsKey of Object.keys(awsTagsRecord)) { + if (!(awsKey in syncTagsRecord)) tagKeysToRemove.push(awsKey); + } + + return { tagsToAdd, tagKeysToRemove }; +}; + +const putParameter = async ( + ssm: AWS.SSM, + params: AWS.SSM.PutParameterRequest, + attempt = 0 +): Promise => { + try { + return await ssm.putParameter(params).promise(); + } catch (error) { + if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + await sleep(); + + // retry + return putParameter(ssm, params, attempt + 1); + } + throw error; + } +}; + +const addTagsToParameter = async ( + ssm: AWS.SSM, + params: Omit, + attempt = 0 +): Promise => { + try { + return await ssm.addTagsToResource({ ...params, ResourceType: "Parameter" }).promise(); + } catch (error) { + if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + await sleep(); + + // retry + return addTagsToParameter(ssm, params, attempt + 1); + } + throw error; + } +}; + +const removeTagsFromParameter = async ( + ssm: AWS.SSM, + params: Omit, + attempt = 0 +): Promise => { + try { + return await ssm.removeTagsFromResource({ ...params, ResourceType: "Parameter" }).promise(); + } catch (error) { + if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + await sleep(); + + // retry + return removeTagsFromParameter(ssm, params, attempt + 1); + } + throw error; + } +}; + +const deleteParametersBatch = async ( + ssm: AWS.SSM, + parameters: AWS.SSM.Parameter[], + attempt = 0 +): Promise => { + const results: AWS.SSM.DeleteParameterResult[] = []; + let remainingParams = [...parameters]; + + while (remainingParams.length > 0) { + const batch = remainingParams.slice(0, BATCH_SIZE); + + try { + // eslint-disable-next-line no-await-in-loop + const result = await ssm.deleteParameters({ Names: batch.map((param) => param.Name!) }).promise(); + results.push(result); + remainingParams = remainingParams.slice(BATCH_SIZE); + } catch (error) { + if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + // eslint-disable-next-line no-await-in-loop + await sleep(); + + // Retry the current batch + // eslint-disable-next-line no-await-in-loop + return [...results, ...(await deleteParametersBatch(ssm, remainingParams, attempt + 1))]; + } + throw error; + } + } + + return results; +}; + +export const AwsParameterStoreSyncFns = { + syncSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, secretMap: TSecretMap) => { + const { destinationConfig, syncOptions } = secretSync; + + const ssm = await getSSM(secretSync); + + const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path); + + const awsParameterStoreMetadataRecord = await getParameterMetadataByPath(ssm, destinationConfig.path); + + const { shouldManageTags, awsParameterStoreTagsRecord } = await getParameterStoreTagsRecord( + ssm, + awsParameterStoreSecretsRecord, + Boolean(syncOptions.tags?.length || syncOptions.syncSecretMetadataAsTags) + ); + const syncTagsRecord = Object.fromEntries(syncOptions.tags?.map((tag) => [tag.key, tag.value]) ?? []); + + for await (const entry of Object.entries(secretMap)) { + const [key, { value, secretMetadata }] = entry; + + // skip empty values (not allowed by AWS) + if (!value) { + // eslint-disable-next-line no-continue + continue; + } + + const keyId = syncOptions.keyId ?? "alias/aws/ssm"; + + // create parameter or update if changed + if ( + !(key in awsParameterStoreSecretsRecord) || + value !== awsParameterStoreSecretsRecord[key].Value || + keyId !== awsParameterStoreMetadataRecord[key]?.KeyId + ) { + try { + await putParameter(ssm, { + Name: `${destinationConfig.path}${key}`, + Type: "SecureString", + Value: value, + Overwrite: true, + KeyId: keyId + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + + if (shouldManageTags) { + const { tagsToAdd, tagKeysToRemove } = processParameterTags({ + syncTagsRecord: { + // configured sync tags take preference over secret metadata + ...(syncOptions.syncSecretMetadataAsTags && + Object.fromEntries(secretMetadata?.map((tag) => [tag.key, tag.value]) ?? [])), + ...syncTagsRecord + }, + awsTagsRecord: awsParameterStoreTagsRecord[key] ?? {} + }); + + if (tagsToAdd.length) { + try { + await addTagsToParameter(ssm, { + ResourceId: `${destinationConfig.path}${key}`, + Tags: tagsToAdd + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + + if (tagKeysToRemove.length) { + try { + await removeTagsFromParameter(ssm, { + ResourceId: `${destinationConfig.path}${key}`, + TagKeys: tagKeysToRemove + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + } + } + + if (syncOptions.disableSecretDeletion) return; + + const parametersToDelete: AWS.SSM.Parameter[] = []; + + for (const entry of Object.entries(awsParameterStoreSecretsRecord)) { + const [key, parameter] = entry; + + if (!(key in secretMap) || !secretMap[key].value) { + parametersToDelete.push(parameter); + } + } + + await deleteParametersBatch(ssm, parametersToDelete); + }, + getSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials): Promise => { + const { destinationConfig } = secretSync; + + const ssm = await getSSM(secretSync); + + const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path); + + return Object.fromEntries( + Object.entries(awsParameterStoreSecretsRecord).map(([key, value]) => [key, { value: value.Value ?? "" }]) + ); + }, + removeSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, secretMap: TSecretMap) => { + const { destinationConfig } = secretSync; + + const ssm = await getSSM(secretSync); + + const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path); + + const parametersToDelete: AWS.SSM.Parameter[] = []; + + for (const entry of Object.entries(awsParameterStoreSecretsRecord)) { + const [key, param] = entry; + + if (key in secretMap) { + parametersToDelete.push(param); + } + } + + await deleteParametersBatch(ssm, parametersToDelete); + } +}; diff --git a/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-schemas.ts b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-schemas.ts new file mode 100644 index 000000000..324b78130 --- /dev/null +++ b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-schemas.ts @@ -0,0 +1,134 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { CharacterType, characterValidator } from "@app/lib/validator/validate-string"; +import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const tagFieldCharacterValidator = characterValidator([ + CharacterType.AlphaNumeric, + CharacterType.Spaces, + CharacterType.Period, + CharacterType.Underscore, + CharacterType.Colon, + CharacterType.ForwardSlash, + CharacterType.Equals, + CharacterType.Plus, + CharacterType.Hyphen, + CharacterType.At +]); + +const pathCharacterValidator = characterValidator([ + CharacterType.AlphaNumeric, + CharacterType.Underscore, + CharacterType.Hyphen +]); + +const AwsParameterStoreSyncDestinationConfigSchema = z.object({ + region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.region), + path: z + .string() + .trim() + .min(1, "Parameter Store Path required") + .max(2048, "Cannot exceed 2048 characters") + .refine( + (val) => + val.startsWith("/") && + val.endsWith("/") && + val + .split("/") + .filter(Boolean) + .every((el) => pathCharacterValidator(el)), + 'Invalid path - must follow "/example/path/" format' + ) + .describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.path) +}); + +const AwsParameterStoreSyncOptionsSchema = z.object({ + keyId: z + .string() + .min(1, "Invalid KMS Key ID") + .max(256, "Invalid KMS Key ID") + .refine( + (val) => + characterValidator([ + CharacterType.AlphaNumeric, + CharacterType.Colon, + CharacterType.ForwardSlash, + CharacterType.Underscore, + CharacterType.Hyphen + ])(val), + "Invalid KMS Key ID" + ) + .optional() + .describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.keyId), + tags: z + .object({ + key: z + .string() + .min(1, "Resource tag key required") + .max(128, "Resource tag key cannot exceed 128 characters") + .refine( + (val) => tagFieldCharacterValidator(val), + "Invalid resource tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-" + ), + value: z + .string() + .max(256, "Resource tag value cannot exceed 256 characters") + .refine( + (val) => tagFieldCharacterValidator(val), + "Invalid resource tag value: tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-" + ) + }) + .array() + .max(50) + .refine((items) => new Set(items.map((item) => item.key)).size === items.length, { + message: "Resource tag keys must be unique" + }) + .optional() + .describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.tags), + syncSecretMetadataAsTags: z + .boolean() + .optional() + .describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.syncSecretMetadataAsTags) +}); + +const AwsParameterStoreSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +export const AwsParameterStoreSyncSchema = BaseSecretSyncSchema( + SecretSync.AWSParameterStore, + AwsParameterStoreSyncOptionsConfig, + AwsParameterStoreSyncOptionsSchema +).extend({ + destination: z.literal(SecretSync.AWSParameterStore), + destinationConfig: AwsParameterStoreSyncDestinationConfigSchema +}); + +export const CreateAwsParameterStoreSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.AWSParameterStore, + AwsParameterStoreSyncOptionsConfig, + AwsParameterStoreSyncOptionsSchema +).extend({ + destinationConfig: AwsParameterStoreSyncDestinationConfigSchema +}); + +export const UpdateAwsParameterStoreSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.AWSParameterStore, + AwsParameterStoreSyncOptionsConfig, + AwsParameterStoreSyncOptionsSchema +).extend({ + destinationConfig: AwsParameterStoreSyncDestinationConfigSchema.optional() +}); + +export const AwsParameterStoreSyncListItemSchema = z.object({ + name: z.literal("AWS Parameter Store"), + connection: z.literal(AppConnection.AWS), + destination: z.literal(SecretSync.AWSParameterStore), + canImportSecrets: z.literal(true) +}); diff --git a/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-types.ts b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-types.ts new file mode 100644 index 000000000..dada28435 --- /dev/null +++ b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-types.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +import { TAwsConnection } from "@app/services/app-connection/aws"; + +import { + AwsParameterStoreSyncListItemSchema, + AwsParameterStoreSyncSchema, + CreateAwsParameterStoreSyncSchema +} from "./aws-parameter-store-sync-schemas"; + +export type TAwsParameterStoreSync = z.infer; + +export type TAwsParameterStoreSyncInput = z.infer; + +export type TAwsParameterStoreSyncListItem = z.infer; + +export type TAwsParameterStoreSyncWithCredentials = TAwsParameterStoreSync & { + connection: TAwsConnection; +}; diff --git a/backend/src/services/secret-sync/aws-parameter-store/index.ts b/backend/src/services/secret-sync/aws-parameter-store/index.ts new file mode 100644 index 000000000..20728cd8f --- /dev/null +++ b/backend/src/services/secret-sync/aws-parameter-store/index.ts @@ -0,0 +1,4 @@ +export * from "./aws-parameter-store-sync-constants"; +export * from "./aws-parameter-store-sync-fns"; +export * from "./aws-parameter-store-sync-schemas"; +export * from "./aws-parameter-store-sync-types"; diff --git a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-constants.ts b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-constants.ts new file mode 100644 index 000000000..48fe2f115 --- /dev/null +++ b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const AWS_SECRETS_MANAGER_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "AWS Secrets Manager", + destination: SecretSync.AWSSecretsManager, + connection: AppConnection.AWS, + canImportSecrets: true +}; diff --git a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums.ts b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums.ts new file mode 100644 index 000000000..df8d26238 --- /dev/null +++ b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums.ts @@ -0,0 +1,4 @@ +export enum AwsSecretsManagerSyncMappingBehavior { + OneToOne = "one-to-one", + ManyToOne = "many-to-one" +} diff --git a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts new file mode 100644 index 000000000..7cea12d1b --- /dev/null +++ b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts @@ -0,0 +1,523 @@ +import { UntagResourceCommandOutput } from "@aws-sdk/client-kms"; +import { + BatchGetSecretValueCommand, + CreateSecretCommand, + CreateSecretCommandInput, + DeleteSecretCommand, + DeleteSecretResponse, + DescribeSecretCommand, + DescribeSecretCommandInput, + ListSecretsCommand, + SecretsManagerClient, + TagResourceCommand, + TagResourceCommandOutput, + UntagResourceCommand, + UpdateSecretCommand, + UpdateSecretCommandInput +} from "@aws-sdk/client-secrets-manager"; +import { AWSError } from "aws-sdk"; +import { + CreateSecretResponse, + DescribeSecretResponse, + SecretListEntry, + SecretValueEntry, + Tag +} from "aws-sdk/clients/secretsmanager"; + +import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns"; +import { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; + +import { TAwsSecretsManagerSyncWithCredentials } from "./aws-secrets-manager-sync-types"; + +type TAwsSecretsRecord = Record; +type TAwsSecretValuesRecord = Record; +type TAwsSecretDescriptionsRecord = Record; + +const MAX_RETRIES = 5; +const BATCH_SIZE = 20; + +const getSecretsManagerClient = async (secretSync: TAwsSecretsManagerSyncWithCredentials) => { + const { destinationConfig, connection } = secretSync; + + const config = await getAwsConnectionConfig(connection, destinationConfig.region); + + const secretsManagerClient = new SecretsManagerClient({ + region: config.region, + credentials: config.credentials! + }); + + return secretsManagerClient; +}; + +const sleep = async () => + new Promise((resolve) => { + setTimeout(resolve, 1000); + }); + +const getSecretsRecord = async (client: SecretsManagerClient): Promise => { + const awsSecretsRecord: TAwsSecretsRecord = {}; + let hasNext = true; + let nextToken: string | undefined; + let attempt = 0; + + while (hasNext) { + try { + // eslint-disable-next-line no-await-in-loop + const output = await client.send(new ListSecretsCommand({ NextToken: nextToken })); + + attempt = 0; + + if (output.SecretList) { + output.SecretList.forEach((secretEntry) => { + if (secretEntry.Name) { + awsSecretsRecord[secretEntry.Name] = secretEntry; + } + }); + } + + hasNext = Boolean(output.NextToken); + nextToken = output.NextToken; + } catch (e) { + if ((e as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + attempt += 1; + // eslint-disable-next-line no-await-in-loop + await sleep(); + // eslint-disable-next-line no-continue + continue; + } + + throw e; + } + } + + return awsSecretsRecord; +}; + +const getSecretValuesRecord = async ( + client: SecretsManagerClient, + awsSecretsRecord: TAwsSecretsRecord +): Promise => { + const awsSecretValuesRecord: TAwsSecretValuesRecord = {}; + let attempt = 0; + + const secretIdList = Object.keys(awsSecretsRecord); + + for (let i = 0; i < secretIdList.length; i += BATCH_SIZE) { + const batchSecretIds = secretIdList.slice(i, i + BATCH_SIZE); + let hasNext = true; + let nextToken: string | undefined; + + while (hasNext) { + try { + // eslint-disable-next-line no-await-in-loop + const output = await client.send( + new BatchGetSecretValueCommand({ + SecretIdList: batchSecretIds, + NextToken: nextToken + }) + ); + + attempt = 0; + + if (output.SecretValues) { + output.SecretValues.forEach((secretValueEntry) => { + if (secretValueEntry.Name) { + awsSecretValuesRecord[secretValueEntry.Name] = secretValueEntry; + } + }); + } + + hasNext = Boolean(output.NextToken); + nextToken = output.NextToken; + } catch (e) { + if ((e as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + attempt += 1; + // eslint-disable-next-line no-await-in-loop + await sleep(); + // eslint-disable-next-line no-continue + continue; + } + + throw e; + } + } + } + + return awsSecretValuesRecord; +}; + +const describeSecret = async ( + client: SecretsManagerClient, + input: DescribeSecretCommandInput, + attempt = 0 +): Promise => { + try { + return await client.send(new DescribeSecretCommand(input)); + } catch (error) { + if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + await sleep(); + + // retry + return describeSecret(client, input, attempt + 1); + } + throw error; + } +}; + +const getSecretDescriptionsRecord = async ( + client: SecretsManagerClient, + awsSecretsRecord: TAwsSecretsRecord +): Promise => { + const awsSecretDescriptionsRecord: TAwsSecretValuesRecord = {}; + + for await (const secretKey of Object.keys(awsSecretsRecord)) { + try { + awsSecretDescriptionsRecord[secretKey] = await describeSecret(client, { + SecretId: secretKey + }); + } catch (error) { + throw new SecretSyncError({ + secretKey, + error + }); + } + } + + return awsSecretDescriptionsRecord; +}; + +const createSecret = async ( + client: SecretsManagerClient, + input: CreateSecretCommandInput, + attempt = 0 +): Promise => { + try { + return await client.send(new CreateSecretCommand(input)); + } catch (error) { + if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + await sleep(); + + // retry + return createSecret(client, input, attempt + 1); + } + throw error; + } +}; + +const updateSecret = async ( + client: SecretsManagerClient, + input: UpdateSecretCommandInput, + attempt = 0 +): Promise => { + try { + return await client.send(new UpdateSecretCommand(input)); + } catch (error) { + if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + await sleep(); + + // retry + return updateSecret(client, input, attempt + 1); + } + throw error; + } +}; + +const deleteSecret = async ( + client: SecretsManagerClient, + secretKey: string, + attempt = 0 +): Promise => { + try { + return await client.send(new DeleteSecretCommand({ SecretId: secretKey, ForceDeleteWithoutRecovery: true })); + } catch (error) { + if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + await sleep(); + + // retry + return deleteSecret(client, secretKey, attempt + 1); + } + throw error; + } +}; + +const addTags = async ( + client: SecretsManagerClient, + secretKey: string, + tags: Tag[], + attempt = 0 +): Promise => { + try { + return await client.send(new TagResourceCommand({ SecretId: secretKey, Tags: tags })); + } catch (error) { + if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + await sleep(); + + // retry + return addTags(client, secretKey, tags, attempt + 1); + } + throw error; + } +}; + +const removeTags = async ( + client: SecretsManagerClient, + secretKey: string, + tagKeys: string[], + attempt = 0 +): Promise => { + try { + return await client.send(new UntagResourceCommand({ SecretId: secretKey, TagKeys: tagKeys })); + } catch (error) { + if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) { + await sleep(); + + // retry + return removeTags(client, secretKey, tagKeys, attempt + 1); + } + throw error; + } +}; + +const processTags = ({ + syncTagsRecord, + awsTagsRecord +}: { + syncTagsRecord: Record; + awsTagsRecord: Record; +}) => { + const tagsToAdd: Tag[] = []; + const tagKeysToRemove: string[] = []; + + for (const syncEntry of Object.entries(syncTagsRecord)) { + const [syncKey, syncValue] = syncEntry; + + if (!(syncKey in awsTagsRecord) || syncValue !== awsTagsRecord[syncKey]) + tagsToAdd.push({ Key: syncKey, Value: syncValue }); + } + + for (const awsKey of Object.keys(awsTagsRecord)) { + if (!(awsKey in syncTagsRecord)) tagKeysToRemove.push(awsKey); + } + + return { tagsToAdd, tagKeysToRemove }; +}; + +export const AwsSecretsManagerSyncFns = { + syncSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => { + const { destinationConfig, syncOptions } = secretSync; + + const client = await getSecretsManagerClient(secretSync); + + const awsSecretsRecord = await getSecretsRecord(client); + + const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord); + + const awsDescriptionsRecord = await getSecretDescriptionsRecord(client, awsSecretsRecord); + + const syncTagsRecord = Object.fromEntries(syncOptions.tags?.map((tag) => [tag.key, tag.value]) ?? []); + + const keyId = syncOptions.keyId ?? "alias/aws/secretsmanager"; + + if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) { + for await (const entry of Object.entries(secretMap)) { + const [key, { value, secretMetadata }] = entry; + + // skip secrets that don't have a value set + if (!value) { + // eslint-disable-next-line no-continue + continue; + } + + if (awsSecretsRecord[key]) { + // skip secrets that haven't changed + if (awsValuesRecord[key]?.SecretString !== value || keyId !== awsDescriptionsRecord[key]?.KmsKeyId) { + try { + await updateSecret(client, { + SecretId: key, + SecretString: value, + KmsKeyId: keyId + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + } else { + try { + await createSecret(client, { + Name: key, + SecretString: value, + KmsKeyId: keyId + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + + const { tagsToAdd, tagKeysToRemove } = processTags({ + syncTagsRecord: { + // configured sync tags take preference over secret metadata + ...(syncOptions.syncSecretMetadataAsTags && + Object.fromEntries(secretMetadata?.map((tag) => [tag.key, tag.value]) ?? [])), + ...syncTagsRecord + }, + awsTagsRecord: Object.fromEntries( + awsDescriptionsRecord[key]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? [] + ) + }); + + if (tagsToAdd.length) { + try { + await addTags(client, key, tagsToAdd); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + + if (tagKeysToRemove.length) { + try { + await removeTags(client, key, tagKeysToRemove); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + } + + if (syncOptions.disableSecretDeletion) return; + + for await (const secretKey of Object.keys(awsSecretsRecord)) { + if (!(secretKey in secretMap) || !secretMap[secretKey].value) { + try { + await deleteSecret(client, secretKey); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey + }); + } + } + } + } else { + // Many-To-One Mapping + + const secretValue = JSON.stringify( + Object.fromEntries(Object.entries(secretMap).map(([key, secretData]) => [key, secretData.value])) + ); + + if (awsSecretsRecord[destinationConfig.secretName]) { + await updateSecret(client, { + SecretId: destinationConfig.secretName, + SecretString: secretValue, + KmsKeyId: keyId + }); + } else { + await createSecret(client, { + Name: destinationConfig.secretName, + SecretString: secretValue, + KmsKeyId: keyId + }); + } + + const { tagsToAdd, tagKeysToRemove } = processTags({ + syncTagsRecord, + awsTagsRecord: Object.fromEntries( + awsDescriptionsRecord[destinationConfig.secretName]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? [] + ) + }); + + if (tagsToAdd.length) { + try { + await addTags(client, destinationConfig.secretName, tagsToAdd); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: destinationConfig.secretName + }); + } + } + + if (tagKeysToRemove.length) { + try { + await removeTags(client, destinationConfig.secretName, tagKeysToRemove); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: destinationConfig.secretName + }); + } + } + } + }, + getSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials): Promise => { + const client = await getSecretsManagerClient(secretSync); + + const awsSecretsRecord = await getSecretsRecord(client); + const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord); + + const { destinationConfig } = secretSync; + + if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) { + return Object.fromEntries( + Object.keys(awsSecretsRecord).map((key) => [key, { value: awsValuesRecord[key].SecretString ?? "" }]) + ); + } + + // Many-To-One Mapping + + const secretValueEntry = awsValuesRecord[destinationConfig.secretName]; + + if (!secretValueEntry) return {}; + + try { + const parsedValue = (secretValueEntry.SecretString ? JSON.parse(secretValueEntry.SecretString) : {}) as Record< + string, + string + >; + + return Object.fromEntries(Object.entries(parsedValue).map(([key, value]) => [key, { value }])); + } catch { + throw new SecretSyncError({ + message: + "Failed to import secrets. Invalid format for Many-To-One mapping behavior: requires key/value configuration.", + shouldRetry: false + }); + } + }, + removeSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => { + const { destinationConfig } = secretSync; + + const client = await getSecretsManagerClient(secretSync); + + const awsSecretsRecord = await getSecretsRecord(client); + + if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) { + for await (const secretKey of Object.keys(awsSecretsRecord)) { + if (secretKey in secretMap) { + try { + await deleteSecret(client, secretKey); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey + }); + } + } + } + } else { + await deleteSecret(client, destinationConfig.secretName); + } + } +}; diff --git a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-schemas.ts b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-schemas.ts new file mode 100644 index 000000000..e80964721 --- /dev/null +++ b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-schemas.ts @@ -0,0 +1,172 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { CharacterType, characterValidator } from "@app/lib/validator/validate-string"; +import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums"; +import { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const AwsSecretsManagerSyncDestinationConfigSchema = z + .discriminatedUnion("mappingBehavior", [ + z.object({ + mappingBehavior: z + .literal(AwsSecretsManagerSyncMappingBehavior.OneToOne) + .describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.mappingBehavior) + }), + z.object({ + mappingBehavior: z + .literal(AwsSecretsManagerSyncMappingBehavior.ManyToOne) + .describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.mappingBehavior), + secretName: z + .string() + + .min(1, "Secret name is required") + .max(256, "Secret name cannot exceed 256 characters") + .refine( + (val) => + characterValidator([ + CharacterType.AlphaNumeric, + CharacterType.ForwardSlash, + CharacterType.Underscore, + CharacterType.Plus, + CharacterType.Equals, + CharacterType.Period, + CharacterType.At, + CharacterType.Hyphen + ])(val), + "Secret name must contain only alphanumeric characters and the characters /_+=.@-" + ) + .describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.secretName) + }) + ]) + .and( + z.object({ + region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.region) + }) + ); + +const tagFieldCharacterValidator = characterValidator([ + CharacterType.AlphaNumeric, + CharacterType.Spaces, + CharacterType.Period, + CharacterType.Underscore, + CharacterType.Colon, + CharacterType.ForwardSlash, + CharacterType.Equals, + CharacterType.Plus, + CharacterType.Hyphen, + CharacterType.At +]); + +const AwsSecretsManagerSyncOptionsSchema = z.object({ + keyId: z + .string() + .min(1, "Invalid KMS Key ID") + .max(256, "Invalid KMS Key ID") + .refine( + (val) => + characterValidator([ + CharacterType.AlphaNumeric, + CharacterType.Colon, + CharacterType.ForwardSlash, + CharacterType.Underscore, + CharacterType.Hyphen + ])(val), + "Invalid KMS Key ID" + ) + .optional() + .describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_SECRETS_MANAGER.keyId), + tags: z + .object({ + key: z + .string() + .min(1, "Tag key required") + .max(128, "Tag key cannot exceed 128 characters") + .refine( + (val) => tagFieldCharacterValidator(val), + "Invalid resource tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-" + ), + value: z + .string() + .max(256, "Tag value cannot exceed 256 characters") + .refine( + (val) => tagFieldCharacterValidator(val), + "Invalid resource tag value: tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-" + ) + }) + .array() + .max(50) + .refine((items) => new Set(items.map((item) => item.key)).size === items.length, { + message: "Tag keys must be unique" + }) + .optional() + .describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_SECRETS_MANAGER.tags), + syncSecretMetadataAsTags: z + .boolean() + .optional() + .describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_SECRETS_MANAGER.syncSecretMetadataAsTags) +}); + +const AwsSecretsManagerSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema( + SecretSync.AWSSecretsManager, + AwsSecretsManagerSyncOptionsConfig, + AwsSecretsManagerSyncOptionsSchema +).extend({ + destination: z.literal(SecretSync.AWSSecretsManager), + destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema +}); + +export const CreateAwsSecretsManagerSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.AWSSecretsManager, + AwsSecretsManagerSyncOptionsConfig, + AwsSecretsManagerSyncOptionsSchema +) + .extend({ + destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema + }) + .superRefine((sync, ctx) => { + if ( + sync.destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne && + sync.syncOptions.syncSecretMetadataAsTags + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Syncing secret metadata is not supported with "Many-to-One" mapping behavior.' + }); + } + }); + +export const UpdateAwsSecretsManagerSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.AWSSecretsManager, + AwsSecretsManagerSyncOptionsConfig, + AwsSecretsManagerSyncOptionsSchema +) + .extend({ + destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema.optional() + }) + .superRefine((sync, ctx) => { + if ( + sync.destinationConfig?.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne && + sync.syncOptions.syncSecretMetadataAsTags + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Syncing secret metadata is not supported with "Many-to-One" mapping behavior.' + }); + } + }); + +export const AwsSecretsManagerSyncListItemSchema = z.object({ + name: z.literal("AWS Secrets Manager"), + connection: z.literal(AppConnection.AWS), + destination: z.literal(SecretSync.AWSSecretsManager), + canImportSecrets: z.literal(true) +}); diff --git a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-types.ts b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-types.ts new file mode 100644 index 000000000..848dce6bc --- /dev/null +++ b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-types.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +import { TAwsConnection } from "@app/services/app-connection/aws"; + +import { + AwsSecretsManagerSyncListItemSchema, + AwsSecretsManagerSyncSchema, + CreateAwsSecretsManagerSyncSchema +} from "./aws-secrets-manager-sync-schemas"; + +export type TAwsSecretsManagerSync = z.infer; + +export type TAwsSecretsManagerSyncInput = z.infer; + +export type TAwsSecretsManagerSyncListItem = z.infer; + +export type TAwsSecretsManagerSyncWithCredentials = TAwsSecretsManagerSync & { + connection: TAwsConnection; +}; diff --git a/backend/src/services/secret-sync/aws-secrets-manager/index.ts b/backend/src/services/secret-sync/aws-secrets-manager/index.ts new file mode 100644 index 000000000..ff6ccff98 --- /dev/null +++ b/backend/src/services/secret-sync/aws-secrets-manager/index.ts @@ -0,0 +1,4 @@ +export * from "./aws-secrets-manager-sync-constants"; +export * from "./aws-secrets-manager-sync-fns"; +export * from "./aws-secrets-manager-sync-schemas"; +export * from "./aws-secrets-manager-sync-types"; diff --git a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-constants.ts b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-constants.ts new file mode 100644 index 000000000..07876f088 --- /dev/null +++ b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Azure App Configuration", + destination: SecretSync.AzureAppConfiguration, + connection: AppConnection.AzureAppConfiguration, + canImportSecrets: true +}; diff --git a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts new file mode 100644 index 000000000..64d82c125 --- /dev/null +++ b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts @@ -0,0 +1,216 @@ +/* eslint-disable no-await-in-loop */ +import https from "https"; + +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault"; +import { isAzureKeyVaultReference } from "@app/services/integration-auth/integration-sync-secret-fns"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; + +import { TAzureAppConfigurationSyncWithCredentials } from "./azure-app-configuration-sync-types"; + +type TAzureAppConfigurationSyncFactoryDeps = { + appConnectionDAL: Pick; + kmsService: Pick; +}; + +interface AzureAppConfigKeyValue { + key: string; + value: string; + label?: string; +} + +export const azureAppConfigurationSyncFactory = ({ + kmsService, + appConnectionDAL +}: TAzureAppConfigurationSyncFactoryDeps) => { + const $getCompleteAzureAppConfigValues = async (accessToken: string, baseURL: string, url: string) => { + let result: AzureAppConfigKeyValue[] = []; + let currentUrl = url; + + while (currentUrl) { + const res = await request.get<{ items: AzureAppConfigKeyValue[]; ["@nextLink"]: string }>(currentUrl, { + baseURL, + headers: { + Authorization: `Bearer ${accessToken}` + }, + // we force IPV4 because docker setup fails with ipv6 + httpsAgent: new https.Agent({ + family: 4 + }) + }); + + result = result.concat(res.data.items); + currentUrl = res.data?.["@nextLink"]; + } + + return result; + }; + + const $deleteAzureSecret = async (accessToken: string, configurationUrl: string, key: string, label?: string) => { + await request.delete(`${configurationUrl}/kv/${key}?api-version=2023-11-01`, { + headers: { + Authorization: `Bearer ${accessToken}` + }, + ...(label && + label.length > 0 && { + params: { + label + } + }), + httpsAgent: new https.Agent({ + family: 4 + }) + }); + }; + + const syncSecrets = async (secretSync: TAzureAppConfigurationSyncWithCredentials, secretMap: TSecretMap) => { + if (!secretSync.destinationConfig.configurationUrl.endsWith(".azconfig.io")) { + throw new BadRequestError({ + message: "Invalid Azure App Configuration URL provided." + }); + } + + const { accessToken } = await getAzureConnectionAccessToken(secretSync.connectionId, appConnectionDAL, kmsService); + + const azureAppConfigValuesUrl = `/kv?api-version=2023-11-01${ + secretSync.destinationConfig.label ? `&label=${secretSync.destinationConfig.label}` : "&label=%00" + }`; + + const azureAppConfigValuesUrlAllSecrets = `/kv?api-version=2023-11-01`; + + const azureAppConfigSecretsLabeled = Object.fromEntries( + ( + await $getCompleteAzureAppConfigValues( + accessToken, + secretSync.destinationConfig.configurationUrl, + azureAppConfigValuesUrl + ) + ).map((entry) => [entry.key, entry.value]) + ); + + const azureAppConfigSecrets = Object.fromEntries( + ( + await $getCompleteAzureAppConfigValues( + accessToken, + secretSync.destinationConfig.configurationUrl, + azureAppConfigValuesUrlAllSecrets + ) + ).map((entry) => [ + entry.key, + { + value: entry.value, + label: entry.label + } + ]) + ); + + // add the secrets to azure app config, that are in infisical + for await (const key of Object.keys(secretMap)) { + if (!(key in azureAppConfigSecretsLabeled) || secretMap[key]?.value !== azureAppConfigSecretsLabeled[key]) { + await request.put( + `${secretSync.destinationConfig.configurationUrl}/kv/${key}?api-version=2023-11-01`, + { + value: secretMap[key]?.value, + ...(isAzureKeyVaultReference(secretMap[key]?.value || "") && { + content_type: "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8" + }) + }, + { + ...(secretSync.destinationConfig.label && { + params: { + label: secretSync.destinationConfig.label + } + }), + + headers: { + Authorization: `Bearer ${accessToken}` + }, + httpsAgent: new https.Agent({ + family: 4 + }) + } + ); + } + } + + if (secretSync.syncOptions.disableSecretDeletion) return; + + for await (const key of Object.keys(azureAppConfigSecrets)) { + const azureSecret = azureAppConfigSecrets[key]; + if ( + !(key in secretMap) || + secretMap[key] === null || + (azureSecret.label && azureSecret.label !== secretSync.destinationConfig.label) || + (!azureSecret.label && secretSync.destinationConfig.label) + ) { + await $deleteAzureSecret(accessToken, secretSync.destinationConfig.configurationUrl, key, azureSecret.label); + } + } + }; + + const removeSecrets = async (secretSync: TAzureAppConfigurationSyncWithCredentials, secretMap: TSecretMap) => { + const { accessToken } = await getAzureConnectionAccessToken(secretSync.connectionId, appConnectionDAL, kmsService); + + const azureAppConfigValuesUrl = `/kv?api-version=2023-11-01${ + secretSync.destinationConfig.label ? `&label=${secretSync.destinationConfig.label}` : "&label=%00" + }`; + + const azureAppConfigSecrets = Object.fromEntries( + ( + await $getCompleteAzureAppConfigValues( + accessToken, + secretSync.destinationConfig.configurationUrl, + azureAppConfigValuesUrl + ) + ).map((entry) => [entry.key, entry.value]) + ); + + for await (const infisicalKey of Object.keys(secretMap)) { + if (infisicalKey in azureAppConfigSecrets) { + await $deleteAzureSecret( + accessToken, + secretSync.destinationConfig.configurationUrl, + infisicalKey, + secretSync.destinationConfig.label + ); + } + } + }; + + const getSecrets = async (secretSync: TAzureAppConfigurationSyncWithCredentials) => { + const { accessToken } = await getAzureConnectionAccessToken(secretSync.connectionId, appConnectionDAL, kmsService); + + const secretMap: TSecretMap = {}; + + const azureAppConfigValuesUrl = `/kv?api-version=2023-11-01${ + secretSync.destinationConfig.label ? `&label=${secretSync.destinationConfig.label}` : "&label=%00" + }`; + + const azureAppConfigSecrets = Object.fromEntries( + ( + await $getCompleteAzureAppConfigValues( + accessToken, + secretSync.destinationConfig.configurationUrl, + azureAppConfigValuesUrl + ) + ).map((entry) => [entry.key, entry.value]) + ); + + Object.keys(azureAppConfigSecrets).forEach((key) => { + secretMap[key] = { + value: azureAppConfigSecrets[key] + }; + }); + + return secretMap; + }; + + return { + syncSecrets, + removeSecrets, + getSecrets + }; +}; diff --git a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-schemas.ts b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-schemas.ts new file mode 100644 index 000000000..c39581fda --- /dev/null +++ b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-schemas.ts @@ -0,0 +1,50 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const AzureAppConfigurationSyncDestinationConfigSchema = z.object({ + configurationUrl: z + .string() + .min(1, "App Configuration URL required") + .describe(SecretSyncs.DESTINATION_CONFIG.AZURE_APP_CONFIGURATION.configurationUrl), + label: z.string().optional().describe(SecretSyncs.DESTINATION_CONFIG.AZURE_APP_CONFIGURATION.label) +}); + +const AzureAppConfigurationSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +export const AzureAppConfigurationSyncSchema = BaseSecretSyncSchema( + SecretSync.AzureAppConfiguration, + AzureAppConfigurationSyncOptionsConfig +).extend({ + destination: z.literal(SecretSync.AzureAppConfiguration), + destinationConfig: AzureAppConfigurationSyncDestinationConfigSchema +}); + +export const CreateAzureAppConfigurationSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.AzureAppConfiguration, + AzureAppConfigurationSyncOptionsConfig +).extend({ + destinationConfig: AzureAppConfigurationSyncDestinationConfigSchema +}); + +export const UpdateAzureAppConfigurationSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.AzureAppConfiguration, + AzureAppConfigurationSyncOptionsConfig +).extend({ + destinationConfig: AzureAppConfigurationSyncDestinationConfigSchema.optional() +}); + +export const AzureAppConfigurationSyncListItemSchema = z.object({ + name: z.literal("Azure App Configuration"), + connection: z.literal(AppConnection.AzureAppConfiguration), + destination: z.literal(SecretSync.AzureAppConfiguration), + canImportSecrets: z.literal(true) +}); diff --git a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-types.ts b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-types.ts new file mode 100644 index 000000000..4cfbd5472 --- /dev/null +++ b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-types.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +import { TAzureAppConfigurationConnection } from "@app/services/app-connection/azure-app-configuration"; + +import { + AzureAppConfigurationSyncListItemSchema, + AzureAppConfigurationSyncSchema, + CreateAzureAppConfigurationSyncSchema +} from "./azure-app-configuration-sync-schemas"; + +export type TAzureAppConfigurationSync = z.infer; + +export type TAzureAppConfigurationSyncInput = z.infer; + +export type TAzureAppConfigurationSyncListItem = z.infer; + +export type TAzureAppConfigurationSyncWithCredentials = TAzureAppConfigurationSync & { + connection: TAzureAppConfigurationConnection; +}; diff --git a/backend/src/services/secret-sync/azure-app-configuration/index.ts b/backend/src/services/secret-sync/azure-app-configuration/index.ts new file mode 100644 index 000000000..0ed052ff2 --- /dev/null +++ b/backend/src/services/secret-sync/azure-app-configuration/index.ts @@ -0,0 +1,4 @@ +export * from "./azure-app-configuration-sync-constants"; +export * from "./azure-app-configuration-sync-fns"; +export * from "./azure-app-configuration-sync-schemas"; +export * from "./azure-app-configuration-sync-types"; diff --git a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-constants.ts b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-constants.ts new file mode 100644 index 000000000..9e2f986ce --- /dev/null +++ b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const AZURE_KEY_VAULT_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Azure Key Vault", + destination: SecretSync.AzureKeyVault, + connection: AppConnection.AzureKeyVault, + canImportSecrets: true +}; diff --git a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts new file mode 100644 index 000000000..12f1f2aff --- /dev/null +++ b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts @@ -0,0 +1,255 @@ +/* eslint-disable no-await-in-loop */ +import { AxiosError } from "axios"; + +import { request } from "@app/lib/config/request"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; + +import { SecretSyncError } from "../secret-sync-errors"; +import { GetAzureKeyVaultSecret, TAzureKeyVaultSyncWithCredentials } from "./azure-key-vault-sync-types"; + +type TAzureKeyVaultSyncFactoryDeps = { + appConnectionDAL: Pick; + kmsService: Pick; +}; + +export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzureKeyVaultSyncFactoryDeps) => { + const $getAzureKeyVaultSecrets = async (accessToken: string, vaultBaseUrl: string) => { + const paginateAzureKeyVaultSecrets = async () => { + let result: GetAzureKeyVaultSecret[] = []; + + let currentUrl = `${vaultBaseUrl}/secrets?api-version=7.3`; + + while (currentUrl) { + const res = await request.get<{ value: GetAzureKeyVaultSecret; nextLink: string }>(currentUrl, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + + result = result.concat(res.data.value); + currentUrl = res.data.nextLink; + } + + return result; + }; + + const getAzureKeyVaultSecrets = await paginateAzureKeyVaultSecrets(); + + const enabledAzureKeyVaultSecrets = getAzureKeyVaultSecrets.filter((secret) => secret.attributes.enabled); + + // disabled keys to skip sending updates to + const disabledAzureKeyVaultSecretKeys = getAzureKeyVaultSecrets + .filter(({ attributes }) => !attributes.enabled) + .map((getAzureKeyVaultSecret) => { + return getAzureKeyVaultSecret.id.substring(getAzureKeyVaultSecret.id.lastIndexOf("/") + 1); + }); + + let lastSlashIndex: number; + const res = ( + await Promise.all( + enabledAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => { + if (!lastSlashIndex) { + lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf("/"); + } + + const azureKeyVaultSecret = await request.get( + `${getAzureKeyVaultSecret.id}?api-version=7.3`, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + + return { + ...azureKeyVaultSecret.data, + key: getAzureKeyVaultSecret.id.substring(lastSlashIndex + 1) + }; + }) + ) + ).reduce( + (obj, secret) => ({ + ...obj, + [secret.key]: secret + }), + {} as Record + ); + + return { + vaultSecrets: res, + disabledAzureKeyVaultSecretKeys + }; + }; + + const syncSecrets = async (secretSync: TAzureKeyVaultSyncWithCredentials, secretMap: TSecretMap) => { + const { accessToken } = await getAzureConnectionAccessToken(secretSync.connection.id, appConnectionDAL, kmsService); + + const { vaultSecrets, disabledAzureKeyVaultSecretKeys } = await $getAzureKeyVaultSecrets( + accessToken, + secretSync.destinationConfig.vaultBaseUrl + ); + + const setSecrets: { + key: string; + value: string; + }[] = []; + + const deleteSecrets: string[] = []; + + Object.keys(secretMap).forEach((infisicalKey) => { + const hyphenatedKey = infisicalKey.replaceAll("_", "-"); + if (!(hyphenatedKey in vaultSecrets)) { + // case: secret has been created + setSecrets.push({ + key: hyphenatedKey, + value: secretMap[infisicalKey].value + }); + } else if (secretMap[infisicalKey].value !== vaultSecrets[hyphenatedKey].value) { + // case: secret has been updated + setSecrets.push({ + key: hyphenatedKey, + value: secretMap[infisicalKey].value + }); + } + }); + + Object.keys(vaultSecrets).forEach((key) => { + const underscoredKey = key.replaceAll("-", "_"); + if (!(underscoredKey in secretMap)) { + deleteSecrets.push(key); + } + }); + + const setSecretAzureKeyVault = async ({ key, value }: { key: string; value: string }) => { + let isSecretSet = false; + let syncError: Error | null = null; + let maxTries = 6; + if (disabledAzureKeyVaultSecretKeys.includes(key)) return; + + while (!isSecretSet && maxTries > 0) { + // try to set secret + try { + await request.put( + `${secretSync.destinationConfig.vaultBaseUrl}/secrets/${key}?api-version=7.3`, + { + value + }, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + + isSecretSet = true; + } catch (err) { + syncError = err as Error; + if (err instanceof AxiosError) { + // eslint-disable-next-line + if (err.response?.data?.error?.innererror?.code === "ObjectIsDeletedButRecoverable") { + await request.post( + `${secretSync.destinationConfig.vaultBaseUrl}/deletedsecrets/${key}/recover?api-version=7.3`, + {}, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + + await new Promise((resolve) => { + setTimeout(resolve, 10_000); + }); + } else { + await new Promise((resolve) => { + setTimeout(resolve, 10_000); + }); + maxTries -= 1; + } + } + } + } + + if (!isSecretSet) { + throw new SecretSyncError({ + error: syncError, + secretKey: key + }); + } + }; + + for await (const setSecret of setSecrets) { + const { key, value } = setSecret; + await setSecretAzureKeyVault({ + key, + value + }); + } + + if (secretSync.syncOptions.disableSecretDeletion) return; + + for await (const deleteSecretKey of deleteSecrets.filter( + (secret) => !setSecrets.find((setSecret) => setSecret.key === secret) + )) { + await request.delete(`${secretSync.destinationConfig.vaultBaseUrl}/secrets/${deleteSecretKey}?api-version=7.3`, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + } + }; + + const removeSecrets = async (secretSync: TAzureKeyVaultSyncWithCredentials, secretMap: TSecretMap) => { + const { accessToken } = await getAzureConnectionAccessToken(secretSync.connection.id, appConnectionDAL, kmsService); + + const { vaultSecrets, disabledAzureKeyVaultSecretKeys } = await $getAzureKeyVaultSecrets( + accessToken, + secretSync.destinationConfig.vaultBaseUrl + ); + + for await (const [key] of Object.entries(vaultSecrets)) { + const underscoredKey = key.replaceAll("-", "_"); + + if (underscoredKey in secretMap) { + if (!disabledAzureKeyVaultSecretKeys.includes(underscoredKey)) { + await request.delete(`${secretSync.destinationConfig.vaultBaseUrl}/secrets/${key}?api-version=7.3`, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + } + } + } + }; + + const getSecrets = async (secretSync: TAzureKeyVaultSyncWithCredentials) => { + const { accessToken } = await getAzureConnectionAccessToken(secretSync.connection.id, appConnectionDAL, kmsService); + + const { vaultSecrets, disabledAzureKeyVaultSecretKeys } = await $getAzureKeyVaultSecrets( + accessToken, + secretSync.destinationConfig.vaultBaseUrl + ); + + const secretMap: TSecretMap = {}; + + Object.keys(vaultSecrets).forEach((key) => { + if (!disabledAzureKeyVaultSecretKeys.includes(key)) { + const underscoredKey = key.replaceAll("-", "_"); + secretMap[underscoredKey] = { + value: vaultSecrets[key].value + }; + } + }); + + return secretMap; + }; + + return { + syncSecrets, + removeSecrets, + getSecrets + }; +}; diff --git a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-schemas.ts b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-schemas.ts new file mode 100644 index 000000000..d528f531e --- /dev/null +++ b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-schemas.ts @@ -0,0 +1,50 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const AzureKeyVaultSyncDestinationConfigSchema = z.object({ + vaultBaseUrl: z + .string() + .url("Invalid vault base URL format") + .min(1, "Vault base URL required") + .describe(SecretSyncs.DESTINATION_CONFIG.AZURE_KEY_VAULT.vaultBaseUrl) +}); + +const AzureKeyVaultSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +export const AzureKeyVaultSyncSchema = BaseSecretSyncSchema( + SecretSync.AzureKeyVault, + AzureKeyVaultSyncOptionsConfig +).extend({ + destination: z.literal(SecretSync.AzureKeyVault), + destinationConfig: AzureKeyVaultSyncDestinationConfigSchema +}); + +export const CreateAzureKeyVaultSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.AzureKeyVault, + AzureKeyVaultSyncOptionsConfig +).extend({ + destinationConfig: AzureKeyVaultSyncDestinationConfigSchema +}); + +export const UpdateAzureKeyVaultSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.AzureKeyVault, + AzureKeyVaultSyncOptionsConfig +).extend({ + destinationConfig: AzureKeyVaultSyncDestinationConfigSchema.optional() +}); + +export const AzureKeyVaultSyncListItemSchema = z.object({ + name: z.literal("Azure Key Vault"), + connection: z.literal(AppConnection.AzureKeyVault), + destination: z.literal(SecretSync.AzureKeyVault), + canImportSecrets: z.literal(true) +}); diff --git a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-types.ts b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-types.ts new file mode 100644 index 000000000..d8083d640 --- /dev/null +++ b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-types.ts @@ -0,0 +1,35 @@ +import { z } from "zod"; + +import { TAzureKeyVaultConnection } from "@app/services/app-connection/azure-key-vault"; + +import { + AzureKeyVaultSyncListItemSchema, + AzureKeyVaultSyncSchema, + CreateAzureKeyVaultSyncSchema +} from "./azure-key-vault-sync-schemas"; + +export type TAzureKeyVaultSync = z.infer; + +export type TAzureKeyVaultSyncInput = z.infer; + +export type TAzureKeyVaultSyncListItem = z.infer; + +export type TAzureKeyVaultSyncWithCredentials = TAzureKeyVaultSync & { + connection: TAzureKeyVaultConnection; +}; + +export interface GetAzureKeyVaultSecret { + id: string; // secret URI + value: string; + attributes: { + enabled: boolean; + created: number; + updated: number; + recoveryLevel: string; + recoverableDays: number; + }; +} + +export interface AzureKeyVaultSecret extends GetAzureKeyVaultSecret { + key: string; +} diff --git a/backend/src/services/secret-sync/azure-key-vault/index.ts b/backend/src/services/secret-sync/azure-key-vault/index.ts new file mode 100644 index 000000000..f2a7e036f --- /dev/null +++ b/backend/src/services/secret-sync/azure-key-vault/index.ts @@ -0,0 +1,4 @@ +export * from "./azure-key-vault-sync-constants"; +export * from "./azure-key-vault-sync-fns"; +export * from "./azure-key-vault-sync-schemas"; +export * from "./azure-key-vault-sync-types"; diff --git a/backend/src/services/secret-sync/camunda/camunda-sync-constants.ts b/backend/src/services/secret-sync/camunda/camunda-sync-constants.ts new file mode 100644 index 000000000..7a2bad8f7 --- /dev/null +++ b/backend/src/services/secret-sync/camunda/camunda-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const CAMUNDA_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Camunda", + destination: SecretSync.Camunda, + connection: AppConnection.Camunda, + canImportSecrets: true +}; diff --git a/backend/src/services/secret-sync/camunda/camunda-sync-fns.ts b/backend/src/services/secret-sync/camunda/camunda-sync-fns.ts new file mode 100644 index 000000000..3a52a4939 --- /dev/null +++ b/backend/src/services/secret-sync/camunda/camunda-sync-fns.ts @@ -0,0 +1,173 @@ +import { request } from "@app/lib/config/request"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { getCamundaConnectionAccessToken } from "@app/services/app-connection/camunda"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { + TCamundaCreateSecret, + TCamundaDeleteSecret, + TCamundaListSecrets, + TCamundaListSecretsResponse, + TCamundaPutSecret, + TCamundaSyncWithCredentials +} from "@app/services/secret-sync/camunda/camunda-sync-types"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; + +import { TSecretMap } from "../secret-sync-types"; + +type TCamundaSecretSyncFactoryDeps = { + appConnectionDAL: Pick; + kmsService: Pick; +}; + +const getCamundaSecrets = async ({ accessToken, clusterUUID }: TCamundaListSecrets) => { + const { data } = await request.get( + `${IntegrationUrls.CAMUNDA_API_URL}/clusters/${clusterUUID}/secrets`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + return data; +}; + +const createCamundaSecret = async ({ accessToken, clusterUUID, key, value }: TCamundaCreateSecret) => + request.post( + `${IntegrationUrls.CAMUNDA_API_URL}/clusters/${clusterUUID}/secrets`, + { + secretName: key, + secretValue: value + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + +const deleteCamundaSecret = async ({ accessToken, clusterUUID, key }: TCamundaDeleteSecret) => + request.delete(`${IntegrationUrls.CAMUNDA_API_URL}/clusters/${clusterUUID}/secrets/${key}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + }); + +const updateCamundaSecret = async ({ accessToken, clusterUUID, key, value }: TCamundaPutSecret) => + request.put( + `${IntegrationUrls.CAMUNDA_API_URL}/clusters/${clusterUUID}/secrets/${key}`, + { + secretValue: value + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + +export const camundaSyncFactory = ({ kmsService, appConnectionDAL }: TCamundaSecretSyncFactoryDeps) => { + const syncSecrets = async (secretSync: TCamundaSyncWithCredentials, secretMap: TSecretMap) => { + const { + destinationConfig: { clusterUUID }, + connection + } = secretSync; + + const accessToken = await getCamundaConnectionAccessToken(connection, appConnectionDAL, kmsService); + const camundaSecrets = await getCamundaSecrets({ accessToken, clusterUUID }); + + for await (const entry of Object.entries(secretMap)) { + const [key, { value }] = entry; + + if (!value) { + // eslint-disable-next-line no-continue + continue; + } + + try { + if (camundaSecrets[key] === undefined) { + await createCamundaSecret({ + key, + value, + clusterUUID, + accessToken + }); + } else if (camundaSecrets[key] !== value) { + await updateCamundaSecret({ + key, + value, + clusterUUID, + accessToken + }); + } + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + + if (secretSync.syncOptions.disableSecretDeletion) return; + + for await (const secret of Object.keys(camundaSecrets)) { + if (!(secret in secretMap) || !secretMap[secret].value) { + try { + await deleteCamundaSecret({ + key: secret, + clusterUUID, + accessToken + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: secret + }); + } + } + } + }; + + const removeSecrets = async (secretSync: TCamundaSyncWithCredentials, secretMap: TSecretMap) => { + const { + destinationConfig: { clusterUUID }, + connection + } = secretSync; + + const accessToken = await getCamundaConnectionAccessToken(connection, appConnectionDAL, kmsService); + const camundaSecrets = await getCamundaSecrets({ accessToken, clusterUUID }); + + for await (const secret of Object.keys(camundaSecrets)) { + if (!(secret in secretMap)) { + await deleteCamundaSecret({ + key: secret, + clusterUUID, + accessToken + }); + } + } + }; + + const getSecrets = async (secretSync: TCamundaSyncWithCredentials) => { + const { + destinationConfig: { clusterUUID }, + connection + } = secretSync; + + const accessToken = await getCamundaConnectionAccessToken(connection, appConnectionDAL, kmsService); + const camundaSecrets = await getCamundaSecrets({ accessToken, clusterUUID }); + + return Object.fromEntries(Object.entries(camundaSecrets).map(([key, value]) => [key, { value }])); + }; + + return { + syncSecrets, + removeSecrets, + getSecrets + }; +}; diff --git a/backend/src/services/secret-sync/camunda/camunda-sync-schemas.ts b/backend/src/services/secret-sync/camunda/camunda-sync-schemas.ts new file mode 100644 index 000000000..726b5dfac --- /dev/null +++ b/backend/src/services/secret-sync/camunda/camunda-sync-schemas.ts @@ -0,0 +1,47 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const CamundaSyncDestinationConfigSchema = z.object({ + scope: z.string().trim().min(1, "Camunda scope required").describe(SecretSyncs.DESTINATION_CONFIG.CAMUNDA.scope), + clusterUUID: z + .string() + .min(1, "Camunda cluster UUID is required") + .describe(SecretSyncs.DESTINATION_CONFIG.CAMUNDA.clusterUUID) +}); + +const CamundaSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +export const CamundaSyncSchema = BaseSecretSyncSchema(SecretSync.Camunda, CamundaSyncOptionsConfig).extend({ + destination: z.literal(SecretSync.Camunda), + destinationConfig: CamundaSyncDestinationConfigSchema +}); + +export const CreateCamundaSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.Camunda, + CamundaSyncOptionsConfig +).extend({ + destinationConfig: CamundaSyncDestinationConfigSchema +}); + +export const UpdateCamundaSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.Camunda, + CamundaSyncOptionsConfig +).extend({ + destinationConfig: CamundaSyncDestinationConfigSchema.optional() +}); + +export const CamundaSyncListItemSchema = z.object({ + name: z.literal("Camunda"), + connection: z.literal(AppConnection.Camunda), + destination: z.literal(SecretSync.Camunda), + canImportSecrets: z.literal(true) +}); diff --git a/backend/src/services/secret-sync/camunda/camunda-sync-types.ts b/backend/src/services/secret-sync/camunda/camunda-sync-types.ts new file mode 100644 index 000000000..49eb3262e --- /dev/null +++ b/backend/src/services/secret-sync/camunda/camunda-sync-types.ts @@ -0,0 +1,38 @@ +import { z } from "zod"; + +import { TCamundaConnection } from "@app/services/app-connection/camunda"; + +import { CamundaSyncListItemSchema, CamundaSyncSchema, CreateCamundaSyncSchema } from "./camunda-sync-schemas"; + +export type TCamundaSync = z.infer; + +export type TCamundaSyncInput = z.infer; + +export type TCamundaSyncListItem = z.infer; + +export type TCamundaSyncWithCredentials = TCamundaSync & { + connection: TCamundaConnection; +}; + +export type TCamundaListSecretsResponse = { [key: string]: string }; + +type TBaseCamundaSecretRequest = { + accessToken: string; + clusterUUID: string; +}; + +export type TCamundaListSecrets = TBaseCamundaSecretRequest; + +export type TCamundaCreateSecret = { + key: string; + value?: string; +} & TBaseCamundaSecretRequest; + +export type TCamundaPutSecret = { + key: string; + value?: string; +} & TBaseCamundaSecretRequest; + +export type TCamundaDeleteSecret = { + key: string; +} & TBaseCamundaSecretRequest; diff --git a/backend/src/services/secret-sync/camunda/index.ts b/backend/src/services/secret-sync/camunda/index.ts new file mode 100644 index 000000000..c81d82c99 --- /dev/null +++ b/backend/src/services/secret-sync/camunda/index.ts @@ -0,0 +1,4 @@ +export * from "./camunda-sync-constants"; +export * from "./camunda-sync-fns"; +export * from "./camunda-sync-schemas"; +export * from "./camunda-sync-types"; diff --git a/backend/src/services/secret-sync/databricks/databricks-sync-constants.ts b/backend/src/services/secret-sync/databricks/databricks-sync-constants.ts new file mode 100644 index 000000000..b4ee51a04 --- /dev/null +++ b/backend/src/services/secret-sync/databricks/databricks-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const DATABRICKS_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Databricks", + destination: SecretSync.Databricks, + connection: AppConnection.Databricks, + canImportSecrets: false +}; diff --git a/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts b/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts new file mode 100644 index 000000000..2ee7977a4 --- /dev/null +++ b/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts @@ -0,0 +1,166 @@ +import { request } from "@app/lib/config/request"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { getDatabricksConnectionAccessToken } from "@app/services/app-connection/databricks"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { + TDatabricksDeleteSecret, + TDatabricksListSecretKeys, + TDatabricksListSecretKeysResponse, + TDatabricksPutSecret, + TDatabricksSyncWithCredentials +} from "@app/services/secret-sync/databricks/databricks-sync-types"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps"; + +import { TSecretMap } from "../secret-sync-types"; + +type TDatabricksSecretSyncFactoryDeps = { + appConnectionDAL: Pick; + kmsService: Pick; +}; + +const DATABRICKS_SCOPE_SECRET_LIMIT = 1000; + +const listDatabricksSecrets = async ({ workspaceUrl, scope, accessToken }: TDatabricksListSecretKeys) => { + const { data } = await request.get( + `${removeTrailingSlash(workspaceUrl)}/api/2.0/secrets/list`, + { + params: { + scope + }, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + // not present in response if no secrets exist in scope + return data.secrets ?? []; +}; +const putDatabricksSecret = async ({ workspaceUrl, scope, key, value, accessToken }: TDatabricksPutSecret) => + request.post( + `${removeTrailingSlash(workspaceUrl)}/api/2.0/secrets/put`, + { + scope, + key, + string_value: value + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + +const deleteDatabricksSecrets = async ({ workspaceUrl, scope, key, accessToken }: TDatabricksDeleteSecret) => + request.post( + `${removeTrailingSlash(workspaceUrl)}/api/2.0/secrets/delete`, + { + scope, + key + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + +export const databricksSyncFactory = ({ kmsService, appConnectionDAL }: TDatabricksSecretSyncFactoryDeps) => { + const syncSecrets = async (secretSync: TDatabricksSyncWithCredentials, secretMap: TSecretMap) => { + if (Object.keys(secretSync).length > DATABRICKS_SCOPE_SECRET_LIMIT) { + throw new Error( + `Databricks does not support storing more than ${DATABRICKS_SCOPE_SECRET_LIMIT} secrets per scope.` + ); + } + + const { + destinationConfig: { scope }, + connection + } = secretSync; + + const { workspaceUrl } = connection.credentials; + + const accessToken = await getDatabricksConnectionAccessToken(connection, appConnectionDAL, kmsService); + + for await (const entry of Object.entries(secretMap)) { + const [key, { value }] = entry; + + try { + await putDatabricksSecret({ + key, + value, + workspaceUrl, + scope, + accessToken + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + + const databricksSecretKeys = await listDatabricksSecrets({ + workspaceUrl, + scope, + accessToken + }); + + if (secretSync.syncOptions.disableSecretDeletion) return; + + for await (const secret of databricksSecretKeys) { + if (!(secret.key in secretMap)) { + await deleteDatabricksSecrets({ + key: secret.key, + workspaceUrl, + scope, + accessToken + }); + } + } + }; + + const removeSecrets = async (secretSync: TDatabricksSyncWithCredentials, secretMap: TSecretMap) => { + const { + destinationConfig: { scope }, + connection + } = secretSync; + + const { workspaceUrl } = connection.credentials; + + const accessToken = await getDatabricksConnectionAccessToken(connection, appConnectionDAL, kmsService); + + const databricksSecretKeys = await listDatabricksSecrets({ + workspaceUrl, + scope, + accessToken + }); + + for await (const secret of databricksSecretKeys) { + if (secret.key in secretMap) { + await deleteDatabricksSecrets({ + key: secret.key, + workspaceUrl, + scope, + accessToken + }); + } + } + }; + + const getSecrets = async (secretSync: TDatabricksSyncWithCredentials) => { + throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`); + }; + + return { + syncSecrets, + removeSecrets, + getSecrets + }; +}; diff --git a/backend/src/services/secret-sync/databricks/databricks-sync-schemas.ts b/backend/src/services/secret-sync/databricks/databricks-sync-schemas.ts new file mode 100644 index 000000000..c0f148244 --- /dev/null +++ b/backend/src/services/secret-sync/databricks/databricks-sync-schemas.ts @@ -0,0 +1,43 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const DatabricksSyncDestinationConfigSchema = z.object({ + scope: z.string().trim().min(1, "Databricks scope required").describe(SecretSyncs.DESTINATION_CONFIG.DATABRICKS.scope) +}); + +const DatabricksSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false }; + +export const DatabricksSyncSchema = BaseSecretSyncSchema(SecretSync.Databricks, DatabricksSyncOptionsConfig).extend({ + destination: z.literal(SecretSync.Databricks), + destinationConfig: DatabricksSyncDestinationConfigSchema +}); + +export const CreateDatabricksSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.Databricks, + DatabricksSyncOptionsConfig +).extend({ + destinationConfig: DatabricksSyncDestinationConfigSchema +}); + +export const UpdateDatabricksSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.Databricks, + DatabricksSyncOptionsConfig +).extend({ + destinationConfig: DatabricksSyncDestinationConfigSchema.optional() +}); + +export const DatabricksSyncListItemSchema = z.object({ + name: z.literal("Databricks"), + connection: z.literal(AppConnection.Databricks), + destination: z.literal(SecretSync.Databricks), + canImportSecrets: z.literal(false) +}); diff --git a/backend/src/services/secret-sync/databricks/databricks-sync-types.ts b/backend/src/services/secret-sync/databricks/databricks-sync-types.ts new file mode 100644 index 000000000..41ec06d85 --- /dev/null +++ b/backend/src/services/secret-sync/databricks/databricks-sync-types.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; + +import { TDatabricksConnection } from "@app/services/app-connection/databricks"; + +import { + CreateDatabricksSyncSchema, + DatabricksSyncListItemSchema, + DatabricksSyncSchema +} from "./databricks-sync-schemas"; + +export type TDatabricksSync = z.infer; + +export type TDatabricksSyncInput = z.infer; + +export type TDatabricksSyncListItem = z.infer; + +export type TDatabricksSyncWithCredentials = TDatabricksSync & { + connection: TDatabricksConnection; +}; + +export type TDatabricksListSecretKeysResponse = { + secrets?: { key: string; last_updated_timestamp: number }[]; +}; + +type TBaseDatabricksSecretRequest = { + scope: string; + workspaceUrl: string; + accessToken: string; +}; + +export type TDatabricksListSecretKeys = TBaseDatabricksSecretRequest; + +export type TDatabricksPutSecret = { + key: string; + value?: string; +} & TBaseDatabricksSecretRequest; + +export type TDatabricksDeleteSecret = { + key: string; +} & TBaseDatabricksSecretRequest; diff --git a/backend/src/services/secret-sync/databricks/index.ts b/backend/src/services/secret-sync/databricks/index.ts new file mode 100644 index 000000000..5b4dec07d --- /dev/null +++ b/backend/src/services/secret-sync/databricks/index.ts @@ -0,0 +1,4 @@ +export * from "./databricks-sync-constants"; +export * from "./databricks-sync-fns"; +export * from "./databricks-sync-schemas"; +export * from "./databricks-sync-types"; diff --git a/backend/src/services/secret-sync/gcp/gcp-sync-constants.ts b/backend/src/services/secret-sync/gcp/gcp-sync-constants.ts new file mode 100644 index 000000000..39ae0a9a4 --- /dev/null +++ b/backend/src/services/secret-sync/gcp/gcp-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const GCP_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "GCP Secret Manager", + destination: SecretSync.GCPSecretManager, + connection: AppConnection.GCP, + canImportSecrets: true +}; diff --git a/backend/src/services/secret-sync/gcp/gcp-sync-enums.ts b/backend/src/services/secret-sync/gcp/gcp-sync-enums.ts new file mode 100644 index 000000000..348d2bfa5 --- /dev/null +++ b/backend/src/services/secret-sync/gcp/gcp-sync-enums.ts @@ -0,0 +1,3 @@ +export enum GcpSyncScope { + Global = "global" +} diff --git a/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts b/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts new file mode 100644 index 000000000..a71e29ae4 --- /dev/null +++ b/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts @@ -0,0 +1,229 @@ +import { AxiosError } from "axios"; + +import { request } from "@app/lib/config/request"; +import { logger } from "@app/lib/logger"; +import { getGcpConnectionAuthToken } from "@app/services/app-connection/gcp"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; + +import { SecretSyncError } from "../secret-sync-errors"; +import { TSecretMap } from "../secret-sync-types"; +import { + GCPLatestSecretVersionAccess, + GCPSecret, + GCPSMListSecretsRes, + TGcpSyncWithCredentials +} from "./gcp-sync-types"; + +const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCredentials) => { + const { destinationConfig } = secretSync; + + let gcpSecrets: GCPSecret[] = []; + + const pageSize = 100; + let pageToken: string | undefined; + let hasMorePages = true; + + while (hasMorePages) { + const params = new URLSearchParams({ + pageSize: String(pageSize), + ...(pageToken ? { pageToken } : {}) + }); + + // eslint-disable-next-line no-await-in-loop + const { data: secretsRes } = await request.get( + `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${secretSync.destinationConfig.projectId}/secrets`, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + if (secretsRes.secrets) { + gcpSecrets = gcpSecrets.concat(secretsRes.secrets); + } + + if (!secretsRes.nextPageToken) { + hasMorePages = false; + } + + pageToken = secretsRes.nextPageToken; + } + + const res: { [key: string]: string } = {}; + + for await (const gcpSecret of gcpSecrets) { + const arr = gcpSecret.name.split("/"); + const key = arr[arr.length - 1]; + + try { + const { data: secretLatest } = await request.get( + `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}/versions/latest:access`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8"); + } catch (error) { + // when a secret in GCP has no versions, or is disabled/destroyed, we treat it as if it's a blank value + if ( + error instanceof AxiosError && + (error.response?.status === 404 || + (error.response?.status === 400 && + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + error.response.data.error.status === "FAILED_PRECONDITION" && + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call + error.response.data.error.message.match(/(?:disabled|destroyed)/i))) + ) { + res[key] = ""; + } else { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + } + + return res; +}; + +export const GcpSyncFns = { + syncSecrets: async (secretSync: TGcpSyncWithCredentials, secretMap: TSecretMap) => { + const { destinationConfig, connection } = secretSync; + const accessToken = await getGcpConnectionAuthToken(connection); + + const gcpSecrets = await getGcpSecrets(accessToken, secretSync); + + for await (const key of Object.keys(secretMap)) { + try { + // we do not process secrets with no value because GCP secret manager does not allow it + if (!secretMap[key].value) { + // eslint-disable-next-line no-continue + continue; + } + + if (!(key in gcpSecrets)) { + // case: create secret + await request.post( + `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets`, + { + replication: { + automatic: {} + } + }, + { + params: { + secretId: key + }, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + await request.post( + `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`, + { + payload: { + data: Buffer.from(secretMap[key].value).toString("base64") + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + + for await (const key of Object.keys(gcpSecrets)) { + try { + if (!(key in secretMap) || !secretMap[key].value) { + // eslint-disable-next-line no-continue + if (secretSync.syncOptions.disableSecretDeletion) continue; + + // case: delete secret + await request.delete( + `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } else if (secretMap[key].value !== gcpSecrets[key]) { + if (!secretMap[key].value) { + logger.warn( + `syncSecretsGcpsecretManager: update secret value in gcp where [key=${key}] and [projectId=${destinationConfig.projectId}]` + ); + } + + await request.post( + `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`, + { + payload: { + data: Buffer.from(secretMap[key].value).toString("base64") + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + }, + + getSecrets: async (secretSync: TGcpSyncWithCredentials): Promise => { + const { connection } = secretSync; + const accessToken = await getGcpConnectionAuthToken(connection); + + const gcpSecrets = await getGcpSecrets(accessToken, secretSync); + return Object.fromEntries(Object.entries(gcpSecrets).map(([key, value]) => [key, { value: value ?? "" }])); + }, + + removeSecrets: async (secretSync: TGcpSyncWithCredentials, secretMap: TSecretMap) => { + const { destinationConfig, connection } = secretSync; + const accessToken = await getGcpConnectionAuthToken(connection); + + const gcpSecrets = await getGcpSecrets(accessToken, secretSync); + for await (const [key] of Object.entries(gcpSecrets)) { + if (key in secretMap) { + await request.delete( + `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } + } + } +}; diff --git a/backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts b/backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts new file mode 100644 index 000000000..0643c431a --- /dev/null +++ b/backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts @@ -0,0 +1,46 @@ +import z from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +import { SecretSync } from "../secret-sync-enums"; +import { GcpSyncScope } from "./gcp-sync-enums"; + +const GcpSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +const GcpSyncDestinationConfigSchema = z.object({ + scope: z.literal(GcpSyncScope.Global).describe(SecretSyncs.DESTINATION_CONFIG.GCP.scope), + projectId: z.string().min(1, "Project ID is required").describe(SecretSyncs.DESTINATION_CONFIG.GCP.projectId) +}); + +export const GcpSyncSchema = BaseSecretSyncSchema(SecretSync.GCPSecretManager, GcpSyncOptionsConfig).extend({ + destination: z.literal(SecretSync.GCPSecretManager), + destinationConfig: GcpSyncDestinationConfigSchema +}); + +export const CreateGcpSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.GCPSecretManager, + GcpSyncOptionsConfig +).extend({ + destinationConfig: GcpSyncDestinationConfigSchema +}); + +export const UpdateGcpSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.GCPSecretManager, + GcpSyncOptionsConfig +).extend({ + destinationConfig: GcpSyncDestinationConfigSchema.optional() +}); + +export const GcpSyncListItemSchema = z.object({ + name: z.literal("GCP Secret Manager"), + connection: z.literal(AppConnection.GCP), + destination: z.literal(SecretSync.GCPSecretManager), + canImportSecrets: z.literal(true) +}); diff --git a/backend/src/services/secret-sync/gcp/gcp-sync-types.ts b/backend/src/services/secret-sync/gcp/gcp-sync-types.ts new file mode 100644 index 000000000..1bf6820be --- /dev/null +++ b/backend/src/services/secret-sync/gcp/gcp-sync-types.ts @@ -0,0 +1,33 @@ +import z from "zod"; + +import { TGcpConnection } from "@app/services/app-connection/gcp"; + +import { CreateGcpSyncSchema, GcpSyncListItemSchema, GcpSyncSchema } from "./gcp-sync-schemas"; + +export type TGcpSyncListItem = z.infer; + +export type TGcpSync = z.infer; + +export type TGcpSyncInput = z.infer; + +export type TGcpSyncWithCredentials = TGcpSync & { + connection: TGcpConnection; +}; + +export type GCPSecret = { + name: string; + createTime: string; +}; + +export type GCPSMListSecretsRes = { + secrets?: GCPSecret[]; + totalSize?: number; + nextPageToken?: string; +}; + +export type GCPLatestSecretVersionAccess = { + name: string; + payload: { + data: string; + }; +}; diff --git a/backend/src/services/secret-sync/gcp/index.ts b/backend/src/services/secret-sync/gcp/index.ts new file mode 100644 index 000000000..c92ecc890 --- /dev/null +++ b/backend/src/services/secret-sync/gcp/index.ts @@ -0,0 +1,4 @@ +export * from "./gcp-sync-constants"; +export * from "./gcp-sync-enums"; +export * from "./gcp-sync-schemas"; +export * from "./gcp-sync-types"; diff --git a/backend/src/services/secret-sync/github/github-sync-constants.ts b/backend/src/services/secret-sync/github/github-sync-constants.ts new file mode 100644 index 000000000..f97b96d9e --- /dev/null +++ b/backend/src/services/secret-sync/github/github-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const GITHUB_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "GitHub", + destination: SecretSync.GitHub, + connection: AppConnection.GitHub, + canImportSecrets: false +}; diff --git a/backend/src/services/secret-sync/github/github-sync-enums.ts b/backend/src/services/secret-sync/github/github-sync-enums.ts new file mode 100644 index 000000000..c0109370e --- /dev/null +++ b/backend/src/services/secret-sync/github/github-sync-enums.ts @@ -0,0 +1,11 @@ +export enum GitHubSyncScope { + Repository = "repository", + Organization = "organization", + RepositoryEnvironment = "repository-environment" +} + +export enum GitHubSyncVisibility { + All = "all", + Private = "private", + Selected = "selected" +} diff --git a/backend/src/services/secret-sync/github/github-sync-fns.ts b/backend/src/services/secret-sync/github/github-sync-fns.ts new file mode 100644 index 000000000..1fe922de5 --- /dev/null +++ b/backend/src/services/secret-sync/github/github-sync-fns.ts @@ -0,0 +1,244 @@ +import { Octokit } from "@octokit/rest"; +import sodium from "libsodium-wrappers"; + +import { getGitHubClient } from "@app/services/app-connection/github"; +import { GitHubSyncScope, GitHubSyncVisibility } from "@app/services/secret-sync/github/github-sync-enums"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; + +import { TGitHubPublicKey, TGitHubSecret, TGitHubSecretPayload, TGitHubSyncWithCredentials } from "./github-sync-types"; + +// TODO: rate limit handling + +const getEncryptedSecrets = async (client: Octokit, secretSync: TGitHubSyncWithCredentials) => { + let encryptedSecrets: TGitHubSecret[]; + + const { destinationConfig } = secretSync; + + switch (destinationConfig.scope) { + case GitHubSyncScope.Organization: { + encryptedSecrets = await client.paginate("GET /orgs/{org}/actions/secrets", { + org: destinationConfig.org + }); + break; + } + case GitHubSyncScope.Repository: { + encryptedSecrets = await client.paginate("GET /repos/{owner}/{repo}/actions/secrets", { + owner: destinationConfig.owner, + repo: destinationConfig.repo + }); + + break; + } + case GitHubSyncScope.RepositoryEnvironment: + default: { + encryptedSecrets = await client.paginate("GET /repos/{owner}/{repo}/environments/{environment_name}/secrets", { + owner: destinationConfig.owner, + repo: destinationConfig.repo, + environment_name: destinationConfig.env + }); + break; + } + } + + return encryptedSecrets; +}; + +const getPublicKey = async (client: Octokit, secretSync: TGitHubSyncWithCredentials) => { + let publicKey: TGitHubPublicKey; + + const { destinationConfig } = secretSync; + + switch (destinationConfig.scope) { + case GitHubSyncScope.Organization: { + publicKey = ( + await client.request("GET /orgs/{org}/actions/secrets/public-key", { + org: destinationConfig.org + }) + ).data; + break; + } + case GitHubSyncScope.Repository: { + publicKey = ( + await client.request("GET /repos/{owner}/{repo}/actions/secrets/public-key", { + owner: destinationConfig.owner, + repo: destinationConfig.repo + }) + ).data; + break; + } + case GitHubSyncScope.RepositoryEnvironment: + default: { + publicKey = ( + await client.request("GET /repos/{owner}/{repo}/environments/{environment_name}/secrets/public-key", { + owner: destinationConfig.owner, + repo: destinationConfig.repo, + environment_name: destinationConfig.env + }) + ).data; + break; + } + } + + return publicKey; +}; + +const deleteSecret = async ( + client: Octokit, + secretSync: TGitHubSyncWithCredentials, + encryptedSecret: TGitHubSecret +) => { + const { destinationConfig } = secretSync; + + switch (destinationConfig.scope) { + case GitHubSyncScope.Organization: { + await client.request(`DELETE /orgs/{org}/actions/secrets/{secret_name}`, { + org: destinationConfig.org, + secret_name: encryptedSecret.name + }); + break; + } + case GitHubSyncScope.Repository: { + await client.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", { + owner: destinationConfig.owner, + repo: destinationConfig.repo, + secret_name: encryptedSecret.name + }); + break; + } + case GitHubSyncScope.RepositoryEnvironment: + default: { + await client.request("DELETE /repos/{owner}/{repo}/environments/{environment_name}/secrets/{secret_name}", { + owner: destinationConfig.owner, + repo: destinationConfig.repo, + environment_name: destinationConfig.env, + secret_name: encryptedSecret.name + }); + break; + } + } +}; + +const putSecret = async (client: Octokit, secretSync: TGitHubSyncWithCredentials, payload: TGitHubSecretPayload) => { + const { destinationConfig } = secretSync; + + switch (destinationConfig.scope) { + case GitHubSyncScope.Organization: { + const { visibility, selectedRepositoryIds } = destinationConfig; + + await client.request(`PUT /orgs/{org}/actions/secrets/{secret_name}`, { + org: destinationConfig.org, + ...payload, + visibility, + ...(visibility === GitHubSyncVisibility.Selected && { + selected_repository_ids: selectedRepositoryIds + }) + }); + break; + } + case GitHubSyncScope.Repository: { + await client.request("PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}", { + owner: destinationConfig.owner, + repo: destinationConfig.repo, + ...payload + }); + break; + } + case GitHubSyncScope.RepositoryEnvironment: + default: { + await client.request("PUT /repos/{owner}/{repo}/environments/{environment_name}/secrets/{secret_name}", { + owner: destinationConfig.owner, + repo: destinationConfig.repo, + environment_name: destinationConfig.env, + ...payload + }); + break; + } + } +}; + +export const GithubSyncFns = { + syncSecrets: async (secretSync: TGitHubSyncWithCredentials, secretMap: TSecretMap) => { + switch (secretSync.destinationConfig.scope) { + case GitHubSyncScope.Organization: + if (Object.values(secretMap).length > 1000) { + throw new SecretSyncError({ + message: "GitHub does not support storing more than 1,000 secrets at the organization level.", + shouldRetry: false + }); + } + break; + case GitHubSyncScope.Repository: + case GitHubSyncScope.RepositoryEnvironment: + if (Object.values(secretMap).length > 100) { + throw new SecretSyncError({ + message: "GitHub does not support storing more than 100 secrets at the repository level.", + shouldRetry: false + }); + } + break; + default: + throw new Error( + `Unsupported GitHub Sync scope ${ + (secretSync.destinationConfig as TGitHubSyncWithCredentials["destinationConfig"]).scope + }` + ); + } + + const client = getGitHubClient(secretSync.connection); + + const encryptedSecrets = await getEncryptedSecrets(client, secretSync); + + const publicKey = await getPublicKey(client, secretSync); + + await sodium.ready.then(async () => { + for await (const key of Object.keys(secretMap)) { + // convert secret & base64 key to Uint8Array. + const binaryKey = sodium.from_base64(publicKey.key, sodium.base64_variants.ORIGINAL); + const binarySecretValue = sodium.from_string(secretMap[key].value); + + // encrypt secret using libsodium + const encryptedBytes = sodium.crypto_box_seal(binarySecretValue, binaryKey); + + // convert encrypted Uint8Array to base64 + const encryptedSecretValue = sodium.to_base64(encryptedBytes, sodium.base64_variants.ORIGINAL); + + try { + await putSecret(client, secretSync, { + secret_name: key, + encrypted_value: encryptedSecretValue, + key_id: publicKey.key_id + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + }); + + if (secretSync.syncOptions.disableSecretDeletion) return; + + for await (const encryptedSecret of encryptedSecrets) { + if (!(encryptedSecret.name in secretMap)) { + await deleteSecret(client, secretSync, encryptedSecret); + } + } + }, + getSecrets: async (secretSync: TGitHubSyncWithCredentials) => { + throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`); + }, + removeSecrets: async (secretSync: TGitHubSyncWithCredentials, secretMap: TSecretMap) => { + const client = getGitHubClient(secretSync.connection); + + const encryptedSecrets = await getEncryptedSecrets(client, secretSync); + + for await (const encryptedSecret of encryptedSecrets) { + if (encryptedSecret.name in secretMap) { + await deleteSecret(client, secretSync, encryptedSecret); + } + } + } +}; diff --git a/backend/src/services/secret-sync/github/github-sync-schemas.ts b/backend/src/services/secret-sync/github/github-sync-schemas.ts new file mode 100644 index 000000000..76bbc63a7 --- /dev/null +++ b/backend/src/services/secret-sync/github/github-sync-schemas.ts @@ -0,0 +1,82 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { GitHubSyncScope, GitHubSyncVisibility } from "@app/services/secret-sync/github/github-sync-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const GitHubSyncDestinationConfigSchema = z + .discriminatedUnion("scope", [ + z.object({ + scope: z.literal(GitHubSyncScope.Organization).describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.scope), + org: z.string().min(1, "Organization name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.org), + visibility: z.nativeEnum(GitHubSyncVisibility), + selectedRepositoryIds: z.number().array().optional() + }), + z.object({ + scope: z.literal(GitHubSyncScope.Repository).describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.scope), + owner: z.string().min(1, "Repository owner name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.owner), + repo: z.string().min(1, "Repository name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.repo) + }), + z.object({ + scope: z.literal(GitHubSyncScope.RepositoryEnvironment).describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.scope), + owner: z.string().min(1, "Repository owner name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.owner), + repo: z.string().min(1, "Repository name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.repo), + env: z.string().min(1, "Environment name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.env) + }) + ]) + .superRefine((options, ctx) => { + if (options.scope === GitHubSyncScope.Organization) { + if (options.visibility === GitHubSyncVisibility.Selected) { + if (!options.selectedRepositoryIds?.length) + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Select at least 1 repository", + path: ["selectedRepositoryIds"] + }); + return; + } + + if (options.selectedRepositoryIds?.length) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Selected repositories is only supported for visibility "Selected"`, + path: ["selectedRepositoryIds"] + }); + } + } + }); + +const GitHubSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false }; + +export const GitHubSyncSchema = BaseSecretSyncSchema(SecretSync.GitHub, GitHubSyncOptionsConfig).extend({ + destination: z.literal(SecretSync.GitHub), + destinationConfig: GitHubSyncDestinationConfigSchema +}); + +export const CreateGitHubSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.GitHub, + GitHubSyncOptionsConfig +).extend({ + destinationConfig: GitHubSyncDestinationConfigSchema +}); + +export const UpdateGitHubSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.GitHub, + GitHubSyncOptionsConfig +).extend({ + destinationConfig: GitHubSyncDestinationConfigSchema.optional() +}); + +export const GitHubSyncListItemSchema = z.object({ + name: z.literal("GitHub"), + connection: z.literal(AppConnection.GitHub), + destination: z.literal(SecretSync.GitHub), + canImportSecrets: z.literal(false) +}); diff --git a/backend/src/services/secret-sync/github/github-sync-types.ts b/backend/src/services/secret-sync/github/github-sync-types.ts new file mode 100644 index 000000000..c917a9fa4 --- /dev/null +++ b/backend/src/services/secret-sync/github/github-sync-types.ts @@ -0,0 +1,38 @@ +import { z } from "zod"; + +import { TGitHubConnection } from "@app/services/app-connection/github"; + +import { CreateGitHubSyncSchema, GitHubSyncListItemSchema, GitHubSyncSchema } from "./github-sync-schemas"; + +export type TGitHubSync = z.infer; + +export type TGitHubSyncInput = z.infer; + +export type TGitHubSyncListItem = z.infer; + +export type TGitHubSyncWithCredentials = TGitHubSync & { + connection: TGitHubConnection; +}; + +export type TGitHubSecret = { + name: string; + created_at: string; + updated_at: string; + visibility?: "all" | "private" | "selected"; + selected_repositories_url?: string | undefined; +}; + +export type TGitHubPublicKey = { + key_id: string; + key: string; + id?: number | undefined; + url?: string | undefined; + title?: string | undefined; + created_at?: string | undefined; +}; + +export type TGitHubSecretPayload = { + key_id: string; + secret_name: string; + encrypted_value: string; +}; diff --git a/backend/src/services/secret-sync/github/index.ts b/backend/src/services/secret-sync/github/index.ts new file mode 100644 index 000000000..a136d7780 --- /dev/null +++ b/backend/src/services/secret-sync/github/index.ts @@ -0,0 +1,4 @@ +export * from "./github-sync-constants"; +export * from "./github-sync-fns"; +export * from "./github-sync-schemas"; +export * from "./github-sync-types"; diff --git a/backend/src/services/secret-sync/humanitec/humanitec-sync-constants.ts b/backend/src/services/secret-sync/humanitec/humanitec-sync-constants.ts new file mode 100644 index 000000000..d81cfd041 --- /dev/null +++ b/backend/src/services/secret-sync/humanitec/humanitec-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const HUMANITEC_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Humanitec", + destination: SecretSync.Humanitec, + connection: AppConnection.Humanitec, + canImportSecrets: false +}; diff --git a/backend/src/services/secret-sync/humanitec/humanitec-sync-enums.ts b/backend/src/services/secret-sync/humanitec/humanitec-sync-enums.ts new file mode 100644 index 000000000..eb86fdf4f --- /dev/null +++ b/backend/src/services/secret-sync/humanitec/humanitec-sync-enums.ts @@ -0,0 +1,4 @@ +export enum HumanitecSyncScope { + Application = "application", + Environment = "environment" +} diff --git a/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts b/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts new file mode 100644 index 000000000..5fa0a3d63 --- /dev/null +++ b/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts @@ -0,0 +1,220 @@ +import { request } from "@app/lib/config/request"; +import { logger } from "@app/lib/logger"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; + +import { HumanitecSyncScope } from "./humanitec-sync-enums"; +import { HumanitecSecret, THumanitecSyncWithCredentials } from "./humanitec-sync-types"; + +const getHumanitecSecrets = async (secretSync: THumanitecSyncWithCredentials) => { + const { + destinationConfig, + connection: { + credentials: { apiToken } + } + } = secretSync; + + let url = `${IntegrationUrls.HUMANITEC_API_URL}/orgs/${destinationConfig.org}/apps/${destinationConfig.app}`; + if (destinationConfig.scope === HumanitecSyncScope.Environment) { + url += `/envs/${destinationConfig.env}`; + } + url += "/values"; + + const { data } = await request.get(url, { + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: "application/json" + } + }); + + return data; +}; + +const deleteSecret = async (secretSync: THumanitecSyncWithCredentials, encryptedSecret: HumanitecSecret) => { + const { + destinationConfig, + connection: { + credentials: { apiToken } + } + } = secretSync; + + if (destinationConfig.scope === HumanitecSyncScope.Environment && encryptedSecret.source === "app") { + logger.info( + `Humanitec secret ${encryptedSecret.key} on app ${destinationConfig.app} has no environment override, not deleted as it is an app-level secret` + ); + return; + } + + try { + let url = `${IntegrationUrls.HUMANITEC_API_URL}/orgs/${destinationConfig.org}/apps/${destinationConfig.app}`; + if (destinationConfig.scope === HumanitecSyncScope.Environment) { + url += `/envs/${destinationConfig.env}`; + } + url += `/values/${encryptedSecret.key}`; + + await request.delete(url, { + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: "application/json" + } + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: encryptedSecret.key + }); + } +}; + +const createSecret = async (secretSync: THumanitecSyncWithCredentials, secretMap: TSecretMap, key: string) => { + try { + const { + destinationConfig, + connection: { + credentials: { apiToken } + } + } = secretSync; + + const appLevelSecret = destinationConfig.scope === HumanitecSyncScope.Application ? secretMap[key].value : ""; + await request.post( + `${IntegrationUrls.HUMANITEC_API_URL}/orgs/${destinationConfig.org}/apps/${destinationConfig.app}/values`, + { + key, + value: appLevelSecret, + description: secretMap[key].comment || "", + is_secret: true + }, + { + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: "application/json" + } + } + ); + if (destinationConfig.scope === HumanitecSyncScope.Environment) { + await request.patch( + `${IntegrationUrls.HUMANITEC_API_URL}/orgs/${destinationConfig.org}/apps/${destinationConfig.app}/envs/${destinationConfig.env}/values/${key}`, + { + value: secretMap[key].value, + description: secretMap[key].comment || "" + }, + { + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: "application/json" + } + } + ); + } + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } +}; + +const updateSecret = async ( + secretSync: THumanitecSyncWithCredentials, + secretMap: TSecretMap, + encryptedSecret: HumanitecSecret +) => { + try { + const { + destinationConfig, + connection: { + credentials: { apiToken } + } + } = secretSync; + if (destinationConfig.scope === HumanitecSyncScope.Application) { + await request.patch( + `${IntegrationUrls.HUMANITEC_API_URL}/orgs/${destinationConfig.org}/apps/${destinationConfig.app}/values/${encryptedSecret.key}`, + { + value: secretMap[encryptedSecret.key].value, + description: secretMap[encryptedSecret.key].comment || "" + }, + { + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: "application/json" + } + } + ); + } else if (encryptedSecret.source === "app") { + await request.post( + `${IntegrationUrls.HUMANITEC_API_URL}/orgs/${destinationConfig.org}/apps/${destinationConfig.app}/envs/${destinationConfig.env}/values`, + { + value: secretMap[encryptedSecret.key].value, + description: secretMap[encryptedSecret.key].comment || "", + key: encryptedSecret.key, + is_secret: true + }, + { + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: "application/json" + } + } + ); + } else { + await request.patch( + `${IntegrationUrls.HUMANITEC_API_URL}/orgs/${destinationConfig.org}/apps/${destinationConfig.app}/envs/${destinationConfig.env}/values/${encryptedSecret.key}`, + { + value: secretMap[encryptedSecret.key].value, + description: secretMap[encryptedSecret.key].comment || "" + }, + { + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: "application/json" + } + } + ); + } + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: encryptedSecret.key + }); + } +}; + +export const HumanitecSyncFns = { + syncSecrets: async (secretSync: THumanitecSyncWithCredentials, secretMap: TSecretMap) => { + const humanitecSecrets = await getHumanitecSecrets(secretSync); + const humanitecSecretsKeys = new Map(humanitecSecrets.map((s) => [s.key, s])); + + for await (const key of Object.keys(secretMap)) { + const existingSecret = humanitecSecretsKeys.get(key); + + if (!existingSecret) { + await createSecret(secretSync, secretMap, key); + } else { + await updateSecret(secretSync, secretMap, existingSecret); + } + } + + if (secretSync.syncOptions.disableSecretDeletion) return; + + for await (const humanitecSecret of humanitecSecrets) { + if (!secretMap[humanitecSecret.key]) { + await deleteSecret(secretSync, humanitecSecret); + } + } + }, + getSecrets: async (secretSync: THumanitecSyncWithCredentials): Promise => { + throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`); + }, + + removeSecrets: async (secretSync: THumanitecSyncWithCredentials, secretMap: TSecretMap) => { + const encryptedSecrets = await getHumanitecSecrets(secretSync); + + for await (const encryptedSecret of encryptedSecrets) { + if (encryptedSecret.key in secretMap) { + await deleteSecret(secretSync, encryptedSecret); + } + } + } +}; diff --git a/backend/src/services/secret-sync/humanitec/humanitec-sync-schemas.ts b/backend/src/services/secret-sync/humanitec/humanitec-sync-schemas.ts new file mode 100644 index 000000000..cd90ecfdc --- /dev/null +++ b/backend/src/services/secret-sync/humanitec/humanitec-sync-schemas.ts @@ -0,0 +1,54 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { HumanitecSyncScope } from "@app/services/secret-sync/humanitec/humanitec-sync-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const HumanitecSyncDestinationConfigSchema = z.discriminatedUnion("scope", [ + z.object({ + scope: z.literal(HumanitecSyncScope.Application).describe(SecretSyncs.DESTINATION_CONFIG.HUMANITEC.scope), + org: z.string().min(1, "Org ID is required").describe(SecretSyncs.DESTINATION_CONFIG.HUMANITEC.org), + app: z.string().min(1, "App ID is required").describe(SecretSyncs.DESTINATION_CONFIG.HUMANITEC.app) + }), + z.object({ + scope: z.literal(HumanitecSyncScope.Environment).describe(SecretSyncs.DESTINATION_CONFIG.HUMANITEC.scope), + org: z.string().min(1, "Org ID is required").describe(SecretSyncs.DESTINATION_CONFIG.HUMANITEC.org), + app: z.string().min(1, "App ID is required").describe(SecretSyncs.DESTINATION_CONFIG.HUMANITEC.app), + env: z.string().min(1, "Env ID is required").describe(SecretSyncs.DESTINATION_CONFIG.HUMANITEC.env) + }) +]); + +const HumanitecSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false }; + +export const HumanitecSyncSchema = BaseSecretSyncSchema(SecretSync.Humanitec, HumanitecSyncOptionsConfig).extend({ + destination: z.literal(SecretSync.Humanitec), + destinationConfig: HumanitecSyncDestinationConfigSchema +}); + +export const CreateHumanitecSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.Humanitec, + HumanitecSyncOptionsConfig +).extend({ + destinationConfig: HumanitecSyncDestinationConfigSchema +}); + +export const UpdateHumanitecSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.Humanitec, + HumanitecSyncOptionsConfig +).extend({ + destinationConfig: HumanitecSyncDestinationConfigSchema.optional() +}); + +export const HumanitecSyncListItemSchema = z.object({ + name: z.literal("Humanitec"), + connection: z.literal(AppConnection.Humanitec), + destination: z.literal(SecretSync.Humanitec), + canImportSecrets: z.literal(false) +}); diff --git a/backend/src/services/secret-sync/humanitec/humanitec-sync-types.ts b/backend/src/services/secret-sync/humanitec/humanitec-sync-types.ts new file mode 100644 index 000000000..d49e4401e --- /dev/null +++ b/backend/src/services/secret-sync/humanitec/humanitec-sync-types.ts @@ -0,0 +1,23 @@ +import z from "zod"; + +import { THumanitecConnection } from "@app/services/app-connection/humanitec"; + +import { CreateHumanitecSyncSchema, HumanitecSyncListItemSchema, HumanitecSyncSchema } from "./humanitec-sync-schemas"; + +export type THumanitecSyncListItem = z.infer; + +export type THumanitecSync = z.infer; + +export type THumanitecSyncInput = z.infer; + +export type THumanitecSyncWithCredentials = THumanitecSync & { + connection: THumanitecConnection; +}; + +export type HumanitecSecret = { + description: string; + is_secret: boolean; + key: string; + source: "app" | "env"; + value: string; +}; diff --git a/backend/src/services/secret-sync/humanitec/index.ts b/backend/src/services/secret-sync/humanitec/index.ts new file mode 100644 index 000000000..c1095fda0 --- /dev/null +++ b/backend/src/services/secret-sync/humanitec/index.ts @@ -0,0 +1,5 @@ +export * from "./humanitec-sync-constants"; +export * from "./humanitec-sync-enums"; +export * from "./humanitec-sync-fns"; +export * from "./humanitec-sync-schemas"; +export * from "./humanitec-sync-types"; diff --git a/backend/src/services/secret-sync/secret-sync-dal.ts b/backend/src/services/secret-sync/secret-sync-dal.ts new file mode 100644 index 000000000..617393668 --- /dev/null +++ b/backend/src/services/secret-sync/secret-sync-dal.ts @@ -0,0 +1,202 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { TSecretSyncs } from "@app/db/schemas/secret-syncs"; +import { DatabaseError } from "@app/lib/errors"; +import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex"; +import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; + +export type TSecretSyncDALFactory = ReturnType; + +type SecretSyncFindFilter = Parameters>[0]; + +const baseSecretSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: SecretSyncFindFilter; tx?: Knex }) => { + const query = (tx || db.replicaNode())(TableName.SecretSync) + .leftJoin(TableName.SecretFolder, `${TableName.SecretSync}.folderId`, `${TableName.SecretFolder}.id`) + .leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) + .join(TableName.AppConnection, `${TableName.SecretSync}.connectionId`, `${TableName.AppConnection}.id`) + .select(selectAllTableCols(TableName.SecretSync)) + .select( + // environment + db.ref("name").withSchema(TableName.Environment).as("envName"), + db.ref("id").withSchema(TableName.Environment).as("envId"), + db.ref("slug").withSchema(TableName.Environment).as("envSlug"), + // entire connection + db.ref("name").withSchema(TableName.AppConnection).as("connectionName"), + db.ref("method").withSchema(TableName.AppConnection).as("connectionMethod"), + db.ref("app").withSchema(TableName.AppConnection).as("connectionApp"), + db.ref("orgId").withSchema(TableName.AppConnection).as("connectionOrgId"), + db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("connectionEncryptedCredentials"), + db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"), + db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"), + db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"), + db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"), + db + .ref("isPlatformManagedCredentials") + .withSchema(TableName.AppConnection) + .as("connectionIsPlatformManagedCredentials") + ); + + if (filter) { + /* eslint-disable @typescript-eslint/no-misused-promises */ + void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.SecretSync, filter))); + } + + return query; +}; + +const expandSecretSync = ( + secretSync: Awaited>[number], + folder?: Awaited>[number] +) => { + const { + envId, + envName, + envSlug, + connectionApp, + connectionName, + connectionId, + connectionOrgId, + connectionEncryptedCredentials, + connectionMethod, + connectionDescription, + connectionCreatedAt, + connectionUpdatedAt, + connectionVersion, + connectionIsPlatformManagedCredentials, + ...el + } = secretSync; + + return { + ...el, + connectionId, + environment: envId ? { id: envId, name: envName, slug: envSlug } : null, + connection: { + app: connectionApp, + id: connectionId, + name: connectionName, + orgId: connectionOrgId, + encryptedCredentials: connectionEncryptedCredentials, + method: connectionMethod, + description: connectionDescription, + createdAt: connectionCreatedAt, + updatedAt: connectionUpdatedAt, + version: connectionVersion, + isPlatformManagedCredentials: connectionIsPlatformManagedCredentials + }, + folder: folder + ? { + id: folder.id, + path: folder.path + } + : null + }; +}; + +export const secretSyncDALFactory = ( + db: TDbClient, + folderDAL: Pick +) => { + const secretSyncOrm = ormify(db, TableName.SecretSync); + + const findById = async (id: string, tx?: Knex) => { + try { + const secretSync = await baseSecretSyncQuery({ + filter: { id }, + db, + tx + }).first(); + + if (secretSync) { + // TODO (scott): replace with cached folder path once implemented + const [folderWithPath] = secretSync.folderId + ? await folderDAL.findSecretPathByFolderIds(secretSync.projectId, [secretSync.folderId]) + : []; + return expandSecretSync(secretSync, folderWithPath); + } + } catch (error) { + throw new DatabaseError({ error, name: "Find by ID - Secret Sync" }); + } + }; + + const create = async (data: Parameters<(typeof secretSyncOrm)["create"]>[0]) => { + const secretSync = (await secretSyncOrm.transaction(async (tx) => { + const sync = await secretSyncOrm.create(data, tx); + + return baseSecretSyncQuery({ + filter: { id: sync.id }, + db, + tx + }).first(); + }))!; + + // TODO (scott): replace with cached folder path once implemented + const [folderWithPath] = secretSync.folderId + ? await folderDAL.findSecretPathByFolderIds(secretSync.projectId, [secretSync.folderId]) + : []; + return expandSecretSync(secretSync, folderWithPath); + }; + + const updateById = async (syncId: string, data: Parameters<(typeof secretSyncOrm)["updateById"]>[1]) => { + const secretSync = (await secretSyncOrm.transaction(async (tx) => { + const sync = await secretSyncOrm.updateById(syncId, data, tx); + + return baseSecretSyncQuery({ + filter: { id: sync.id }, + db, + tx + }).first(); + }))!; + + // TODO (scott): replace with cached folder path once implemented + const [folderWithPath] = secretSync.folderId + ? await folderDAL.findSecretPathByFolderIds(secretSync.projectId, [secretSync.folderId]) + : []; + return expandSecretSync(secretSync, folderWithPath); + }; + + const findOne = async (filter: Parameters<(typeof secretSyncOrm)["findOne"]>[0], tx?: Knex) => { + try { + const secretSync = await baseSecretSyncQuery({ filter, db, tx }).first(); + + if (secretSync) { + // TODO (scott): replace with cached folder path once implemented + const [folderWithPath] = secretSync.folderId + ? await folderDAL.findSecretPathByFolderIds(secretSync.projectId, [secretSync.folderId]) + : []; + return expandSecretSync(secretSync, folderWithPath); + } + } catch (error) { + throw new DatabaseError({ error, name: "Find One - Secret Sync" }); + } + }; + + const find = async (filter: Parameters<(typeof secretSyncOrm)["find"]>[0], tx?: Knex) => { + try { + const secretSyncs = await baseSecretSyncQuery({ filter, db, tx }); + + if (!secretSyncs.length) return []; + + const foldersWithPath = await folderDAL.findSecretPathByFolderIds( + secretSyncs[0].projectId, + secretSyncs.filter((sync) => Boolean(sync.folderId)).map((sync) => sync.folderId!) + ); + + // TODO (scott): replace with cached folder path once implemented + const folderRecord: Record = {}; + + foldersWithPath.forEach((folder) => { + if (folder) folderRecord[folder.id] = folder; + }); + + return secretSyncs.map((secretSync) => + expandSecretSync(secretSync, secretSync.folderId ? folderRecord[secretSync.folderId] : undefined) + ); + } catch (error) { + throw new DatabaseError({ error, name: "Find - Secret Sync" }); + } + }; + + return { ...secretSyncOrm, findById, findOne, find, create, updateById }; +}; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts new file mode 100644 index 000000000..86273a4ff --- /dev/null +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -0,0 +1,25 @@ +export enum SecretSync { + AWSParameterStore = "aws-parameter-store", + AWSSecretsManager = "aws-secrets-manager", + GitHub = "github", + GCPSecretManager = "gcp-secret-manager", + AzureKeyVault = "azure-key-vault", + AzureAppConfiguration = "azure-app-configuration", + Databricks = "databricks", + Humanitec = "humanitec", + TerraformCloud = "terraform-cloud", + Camunda = "camunda", + Vercel = "vercel", + Windmill = "windmill" +} + +export enum SecretSyncInitialSyncBehavior { + OverwriteDestination = "overwrite-destination", + ImportPrioritizeSource = "import-prioritize-source", + ImportPrioritizeDestination = "import-prioritize-destination" +} + +export enum SecretSyncImportBehavior { + PrioritizeSource = "prioritize-source", + PrioritizeDestination = "prioritize-destination" +} diff --git a/backend/src/services/secret-sync/secret-sync-errors.ts b/backend/src/services/secret-sync/secret-sync-errors.ts new file mode 100644 index 000000000..859fbb00d --- /dev/null +++ b/backend/src/services/secret-sync/secret-sync-errors.ts @@ -0,0 +1,23 @@ +export class SecretSyncError extends Error { + name: string; + + error?: unknown; + + secretKey?: string; + + shouldRetry?: boolean; + + constructor({ + name, + error, + secretKey, + message, + shouldRetry = true + }: { name?: string; error?: unknown; secretKey?: string; shouldRetry?: boolean; message?: string } = {}) { + super(message); + this.name = name || "SecretSyncError"; + this.error = error; + this.secretKey = secretKey; + this.shouldRetry = shouldRetry; + } +} diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts new file mode 100644 index 000000000..0b821b593 --- /dev/null +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -0,0 +1,284 @@ +import { AxiosError } from "axios"; + +import { + AWS_PARAMETER_STORE_SYNC_LIST_OPTION, + AwsParameterStoreSyncFns +} from "@app/services/secret-sync/aws-parameter-store"; +import { + AWS_SECRETS_MANAGER_SYNC_LIST_OPTION, + AwsSecretsManagerSyncFns +} from "@app/services/secret-sync/aws-secrets-manager"; +import { DATABRICKS_SYNC_LIST_OPTION, databricksSyncFactory } from "@app/services/secret-sync/databricks"; +import { GITHUB_SYNC_LIST_OPTION, GithubSyncFns } from "@app/services/secret-sync/github"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { + TSecretMap, + TSecretSyncListItem, + TSecretSyncWithCredentials +} from "@app/services/secret-sync/secret-sync-types"; + +import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal"; +import { TKmsServiceFactory } from "../kms/kms-service"; +import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFactory } from "./azure-app-configuration"; +import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault"; +import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda"; +import { GCP_SYNC_LIST_OPTION } from "./gcp"; +import { GcpSyncFns } from "./gcp/gcp-sync-fns"; +import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec"; +import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns"; +import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud"; +import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel"; +import { WINDMILL_SYNC_LIST_OPTION, WindmillSyncFns } from "./windmill"; + +const SECRET_SYNC_LIST_OPTIONS: Record = { + [SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION, + [SecretSync.AWSSecretsManager]: AWS_SECRETS_MANAGER_SYNC_LIST_OPTION, + [SecretSync.GitHub]: GITHUB_SYNC_LIST_OPTION, + [SecretSync.GCPSecretManager]: GCP_SYNC_LIST_OPTION, + [SecretSync.AzureKeyVault]: AZURE_KEY_VAULT_SYNC_LIST_OPTION, + [SecretSync.AzureAppConfiguration]: AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, + [SecretSync.Databricks]: DATABRICKS_SYNC_LIST_OPTION, + [SecretSync.Humanitec]: HUMANITEC_SYNC_LIST_OPTION, + [SecretSync.TerraformCloud]: TERRAFORM_CLOUD_SYNC_LIST_OPTION, + [SecretSync.Camunda]: CAMUNDA_SYNC_LIST_OPTION, + [SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION, + [SecretSync.Windmill]: WINDMILL_SYNC_LIST_OPTION +}; + +export const listSecretSyncOptions = () => { + return Object.values(SECRET_SYNC_LIST_OPTIONS).sort((a, b) => a.name.localeCompare(b.name)); +}; + +type TSyncSecretDeps = { + appConnectionDAL: Pick; + kmsService: Pick; +}; + +// const addAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => { +// let secretMap = { ...unprocessedSecretMap }; +// +// const { appendSuffix, prependPrefix } = secretSync.syncOptions; +// +// if (appendSuffix || prependPrefix) { +// secretMap = {}; +// Object.entries(unprocessedSecretMap).forEach(([key, value]) => { +// secretMap[`${prependPrefix || ""}${key}${appendSuffix || ""}`] = value; +// }); +// } +// +// return secretMap; +// }; +// +// const stripAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => { +// let secretMap = { ...unprocessedSecretMap }; +// +// const { appendSuffix, prependPrefix } = secretSync.syncOptions; +// +// if (appendSuffix || prependPrefix) { +// secretMap = {}; +// Object.entries(unprocessedSecretMap).forEach(([key, value]) => { +// let processedKey = key; +// +// if (prependPrefix && processedKey.startsWith(prependPrefix)) { +// processedKey = processedKey.slice(prependPrefix.length); +// } +// +// if (appendSuffix && processedKey.endsWith(appendSuffix)) { +// processedKey = processedKey.slice(0, -appendSuffix.length); +// } +// +// secretMap[processedKey] = value; +// }); +// } +// +// return secretMap; +// }; + +export const SecretSyncFns = { + syncSecrets: ( + secretSync: TSecretSyncWithCredentials, + secretMap: TSecretMap, + { kmsService, appConnectionDAL }: TSyncSecretDeps + ): Promise => { + // const affixedSecretMap = addAffixes(secretSync, secretMap); + + switch (secretSync.destination) { + case SecretSync.AWSParameterStore: + return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap); + case SecretSync.AWSSecretsManager: + return AwsSecretsManagerSyncFns.syncSecrets(secretSync, secretMap); + case SecretSync.GitHub: + return GithubSyncFns.syncSecrets(secretSync, secretMap); + case SecretSync.GCPSecretManager: + return GcpSyncFns.syncSecrets(secretSync, secretMap); + case SecretSync.AzureKeyVault: + return azureKeyVaultSyncFactory({ + appConnectionDAL, + kmsService + }).syncSecrets(secretSync, secretMap); + case SecretSync.AzureAppConfiguration: + return azureAppConfigurationSyncFactory({ + appConnectionDAL, + kmsService + }).syncSecrets(secretSync, secretMap); + case SecretSync.Databricks: + return databricksSyncFactory({ + appConnectionDAL, + kmsService + }).syncSecrets(secretSync, secretMap); + case SecretSync.Humanitec: + return HumanitecSyncFns.syncSecrets(secretSync, secretMap); + case SecretSync.TerraformCloud: + return TerraformCloudSyncFns.syncSecrets(secretSync, secretMap); + case SecretSync.Camunda: + return camundaSyncFactory({ + appConnectionDAL, + kmsService + }).syncSecrets(secretSync, secretMap); + case SecretSync.Vercel: + return VercelSyncFns.syncSecrets(secretSync, secretMap); + case SecretSync.Windmill: + return WindmillSyncFns.syncSecrets(secretSync, secretMap); + default: + throw new Error( + `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` + ); + } + }, + getSecrets: async ( + secretSync: TSecretSyncWithCredentials, + { kmsService, appConnectionDAL }: TSyncSecretDeps + ): Promise => { + let secretMap: TSecretMap; + switch (secretSync.destination) { + case SecretSync.AWSParameterStore: + secretMap = await AwsParameterStoreSyncFns.getSecrets(secretSync); + break; + case SecretSync.AWSSecretsManager: + secretMap = await AwsSecretsManagerSyncFns.getSecrets(secretSync); + break; + case SecretSync.GitHub: + secretMap = await GithubSyncFns.getSecrets(secretSync); + break; + case SecretSync.GCPSecretManager: + secretMap = await GcpSyncFns.getSecrets(secretSync); + break; + case SecretSync.AzureKeyVault: + secretMap = await azureKeyVaultSyncFactory({ + appConnectionDAL, + kmsService + }).getSecrets(secretSync); + break; + case SecretSync.AzureAppConfiguration: + secretMap = await azureAppConfigurationSyncFactory({ + appConnectionDAL, + kmsService + }).getSecrets(secretSync); + break; + case SecretSync.Databricks: + return databricksSyncFactory({ + appConnectionDAL, + kmsService + }).getSecrets(secretSync); + case SecretSync.Humanitec: + secretMap = await HumanitecSyncFns.getSecrets(secretSync); + break; + case SecretSync.TerraformCloud: + secretMap = await TerraformCloudSyncFns.getSecrets(secretSync); + break; + case SecretSync.Camunda: + secretMap = await camundaSyncFactory({ + appConnectionDAL, + kmsService + }).getSecrets(secretSync); + break; + case SecretSync.Vercel: + secretMap = await VercelSyncFns.getSecrets(secretSync); + break; + case SecretSync.Windmill: + secretMap = await WindmillSyncFns.getSecrets(secretSync); + break; + default: + throw new Error( + `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` + ); + } + + return secretMap; + // return stripAffixes(secretSync, secretMap); + }, + removeSecrets: ( + secretSync: TSecretSyncWithCredentials, + secretMap: TSecretMap, + { kmsService, appConnectionDAL }: TSyncSecretDeps + ): Promise => { + // const affixedSecretMap = addAffixes(secretSync, secretMap); + + switch (secretSync.destination) { + case SecretSync.AWSParameterStore: + return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap); + case SecretSync.AWSSecretsManager: + return AwsSecretsManagerSyncFns.removeSecrets(secretSync, secretMap); + case SecretSync.GitHub: + return GithubSyncFns.removeSecrets(secretSync, secretMap); + case SecretSync.GCPSecretManager: + return GcpSyncFns.removeSecrets(secretSync, secretMap); + case SecretSync.AzureKeyVault: + return azureKeyVaultSyncFactory({ + appConnectionDAL, + kmsService + }).removeSecrets(secretSync, secretMap); + case SecretSync.AzureAppConfiguration: + return azureAppConfigurationSyncFactory({ + appConnectionDAL, + kmsService + }).removeSecrets(secretSync, secretMap); + case SecretSync.Databricks: + return databricksSyncFactory({ + appConnectionDAL, + kmsService + }).removeSecrets(secretSync, secretMap); + case SecretSync.Humanitec: + return HumanitecSyncFns.removeSecrets(secretSync, secretMap); + case SecretSync.TerraformCloud: + return TerraformCloudSyncFns.removeSecrets(secretSync, secretMap); + case SecretSync.Camunda: + return camundaSyncFactory({ + appConnectionDAL, + kmsService + }).removeSecrets(secretSync, secretMap); + case SecretSync.Vercel: + return VercelSyncFns.removeSecrets(secretSync, secretMap); + case SecretSync.Windmill: + return WindmillSyncFns.removeSecrets(secretSync, secretMap); + default: + throw new Error( + `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` + ); + } + } +}; + +const MAX_MESSAGE_LENGTH = 1024; + +export const parseSyncErrorMessage = (err: unknown): string => { + let errorMessage: string; + + if (err instanceof SecretSyncError) { + errorMessage = JSON.stringify({ + secretKey: err.secretKey, + error: err.message || parseSyncErrorMessage(err.error) + }); + } else if (err instanceof AxiosError) { + errorMessage = err?.response?.data + ? JSON.stringify(err?.response?.data) + : err?.message ?? "An unknown error occurred."; + } else { + errorMessage = (err as Error)?.message || "An unknown error occurred."; + } + + return errorMessage.length <= MAX_MESSAGE_LENGTH + ? errorMessage + : `${errorMessage.substring(0, MAX_MESSAGE_LENGTH - 3)}...`; +}; diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts new file mode 100644 index 000000000..a9099543d --- /dev/null +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -0,0 +1,32 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +export const SECRET_SYNC_NAME_MAP: Record = { + [SecretSync.AWSParameterStore]: "AWS Parameter Store", + [SecretSync.AWSSecretsManager]: "AWS Secrets Manager", + [SecretSync.GitHub]: "GitHub", + [SecretSync.GCPSecretManager]: "GCP Secret Manager", + [SecretSync.AzureKeyVault]: "Azure Key Vault", + [SecretSync.AzureAppConfiguration]: "Azure App Configuration", + [SecretSync.Databricks]: "Databricks", + [SecretSync.Humanitec]: "Humanitec", + [SecretSync.TerraformCloud]: "Terraform Cloud", + [SecretSync.Camunda]: "Camunda", + [SecretSync.Vercel]: "Vercel", + [SecretSync.Windmill]: "Windmill" +}; + +export const SECRET_SYNC_CONNECTION_MAP: Record = { + [SecretSync.AWSParameterStore]: AppConnection.AWS, + [SecretSync.AWSSecretsManager]: AppConnection.AWS, + [SecretSync.GitHub]: AppConnection.GitHub, + [SecretSync.GCPSecretManager]: AppConnection.GCP, + [SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault, + [SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration, + [SecretSync.Databricks]: AppConnection.Databricks, + [SecretSync.Humanitec]: AppConnection.Humanitec, + [SecretSync.TerraformCloud]: AppConnection.TerraformCloud, + [SecretSync.Camunda]: AppConnection.Camunda, + [SecretSync.Vercel]: AppConnection.Vercel, + [SecretSync.Windmill]: AppConnection.Windmill +}; diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts new file mode 100644 index 000000000..3177b68b1 --- /dev/null +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -0,0 +1,976 @@ +import opentelemetry from "@opentelemetry/api"; +import { AxiosError } from "axios"; +import { Job } from "bullmq"; + +import { ProjectMembershipRole, SecretType } from "@app/db/schemas"; +import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; +import { getConfig } from "@app/lib/config/env"; +import { logger } from "@app/lib/logger"; +import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; +import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; +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 { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; +import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; +import { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal"; +import { TSecretDALFactory } from "@app/services/secret/secret-dal"; +import { createManySecretsRawFnFactory, updateManySecretsRawFnFactory } from "@app/services/secret/secret-fns"; +import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal"; +import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal"; +import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal"; +import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; +import { TSecretImportDALFactory } from "@app/services/secret-import/secret-import-dal"; +import { fnSecretsV2FromImports } from "@app/services/secret-import/secret-import-fns"; +import { TSecretSyncDALFactory } from "@app/services/secret-sync/secret-sync-dal"; +import { + SecretSync, + SecretSyncImportBehavior, + SecretSyncInitialSyncBehavior +} from "@app/services/secret-sync/secret-sync-enums"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { parseSyncErrorMessage, SecretSyncFns } from "@app/services/secret-sync/secret-sync-fns"; +import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps"; +import { + SecretSyncAction, + SecretSyncStatus, + TQueueSecretSyncImportSecretsByIdDTO, + TQueueSecretSyncRemoveSecretsByIdDTO, + TQueueSecretSyncsByPathDTO, + TQueueSecretSyncSyncSecretsByIdDTO, + TQueueSendSecretSyncActionFailedNotificationsDTO, + TSecretMap, + TSecretSyncImportSecretsDTO, + TSecretSyncRaw, + TSecretSyncRemoveSecretsDTO, + TSecretSyncSyncSecretsDTO, + TSecretSyncWithCredentials, + TSendSecretSyncFailedNotificationsJobDTO +} from "@app/services/secret-sync/secret-sync-types"; +import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; +import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal"; +import { expandSecretReferencesFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-fns"; +import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal"; +import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; +import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; + +import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal"; + +export type TSecretSyncQueueFactory = ReturnType; + +type TSecretSyncQueueFactoryDep = { + queueService: Pick; + kmsService: Pick; + appConnectionDAL: Pick; + keyStore: Pick; + folderDAL: TSecretFolderDALFactory; + secretV2BridgeDAL: Pick< + TSecretV2BridgeDALFactory, + | "findByFolderId" + | "find" + | "insertMany" + | "upsertSecretReferences" + | "findBySecretKeys" + | "bulkUpdate" + | "deleteMany" + | "invalidateSecretCacheByProjectId" + >; + secretImportDAL: Pick; + secretSyncDAL: Pick; + auditLogService: Pick; + projectMembershipDAL: Pick; + projectDAL: TProjectDALFactory; + smtpService: Pick; + projectBotDAL: TProjectBotDALFactory; + secretDAL: TSecretDALFactory; + secretVersionDAL: TSecretVersionDALFactory; + secretBlindIndexDAL: TSecretBlindIndexDALFactory; + secretTagDAL: TSecretTagDALFactory; + secretVersionTagDAL: TSecretVersionTagDALFactory; + secretVersionV2BridgeDAL: Pick; + secretVersionTagV2BridgeDAL: Pick; + resourceMetadataDAL: Pick; +}; + +type SecretSyncActionJob = Job< + TQueueSecretSyncSyncSecretsByIdDTO | TQueueSecretSyncImportSecretsByIdDTO | TQueueSecretSyncRemoveSecretsByIdDTO +>; + +const getRequeueDelay = (failureCount?: number) => { + if (!failureCount) return 0; + + const baseDelay = 1000; + const maxDelay = 30000; + + const delay = Math.min(baseDelay * 2 ** failureCount, maxDelay); + + const jitter = delay * (0.5 + Math.random() * 0.5); + + return jitter; +}; + +export const secretSyncQueueFactory = ({ + queueService, + kmsService, + appConnectionDAL, + keyStore, + folderDAL, + secretV2BridgeDAL, + secretImportDAL, + secretSyncDAL, + auditLogService, + projectMembershipDAL, + projectDAL, + smtpService, + projectBotDAL, + secretDAL, + secretVersionDAL, + secretBlindIndexDAL, + secretTagDAL, + secretVersionTagDAL, + secretVersionV2BridgeDAL, + secretVersionTagV2BridgeDAL, + resourceMetadataDAL +}: TSecretSyncQueueFactoryDep) => { + const appCfg = getConfig(); + + const integrationMeter = opentelemetry.metrics.getMeter("SecretSyncs"); + const syncSecretsErrorHistogram = integrationMeter.createHistogram("secret_sync_sync_secrets_errors", { + description: "Secret Sync - sync secrets errors", + unit: "1" + }); + const importSecretsErrorHistogram = integrationMeter.createHistogram("secret_sync_import_secrets_errors", { + description: "Secret Sync - import secrets errors", + unit: "1" + }); + const removeSecretsErrorHistogram = integrationMeter.createHistogram("secret_sync_remove_secrets_errors", { + description: "Secret Sync - remove secrets errors", + unit: "1" + }); + + const $createManySecretsRawFn = createManySecretsRawFnFactory({ + projectDAL, + projectBotDAL, + secretDAL, + secretVersionDAL, + secretBlindIndexDAL, + secretTagDAL, + secretVersionTagDAL, + folderDAL, + kmsService, + secretVersionV2BridgeDAL, + secretV2BridgeDAL, + secretVersionTagV2BridgeDAL, + resourceMetadataDAL + }); + + const $updateManySecretsRawFn = updateManySecretsRawFnFactory({ + projectDAL, + projectBotDAL, + secretDAL, + secretVersionDAL, + secretBlindIndexDAL, + secretTagDAL, + secretVersionTagDAL, + folderDAL, + kmsService, + secretVersionV2BridgeDAL, + secretV2BridgeDAL, + secretVersionTagV2BridgeDAL, + resourceMetadataDAL + }); + + const $getInfisicalSecrets = async ( + secretSync: TSecretSyncRaw | TSecretSyncWithCredentials, + includeImports = true + ) => { + const { projectId, folderId, environment, folder } = secretSync; + + if (!folderId || !environment || !folder) + throw new SecretSyncError({ + message: + "Invalid Secret Sync source configuration: folder no longer exists. Please update source environment and secret path.", + shouldRetry: false + }); + + const secretMap: TSecretMap = {}; + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + + const decryptSecretValue = (value?: Buffer | undefined | null) => + value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""; + + const { expandSecretReferences } = expandSecretReferencesFactory({ + decryptSecretValue, + secretDAL: secretV2BridgeDAL, + folderDAL, + projectId, + canExpandValue: () => true + }); + + const secrets = await secretV2BridgeDAL.findByFolderId({ folderId }); + + await Promise.allSettled( + secrets.map(async (secret) => { + const secretKey = secret.key; + const secretValue = decryptSecretValue(secret.encryptedValue); + const expandedSecretValue = await expandSecretReferences({ + environment: environment.slug, + secretPath: folder.path, + skipMultilineEncoding: secret.skipMultilineEncoding, + value: secretValue + }); + secretMap[secretKey] = { value: expandedSecretValue || "" }; + + if (secret.encryptedComment) { + const commentValue = decryptSecretValue(secret.encryptedComment); + secretMap[secretKey].comment = commentValue; + } + + secretMap[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding); + secretMap[secretKey].secretMetadata = secret.secretMetadata; + }) + ); + + if (!includeImports) return secretMap; + + const secretImports = await secretImportDAL.find({ folderId, isReplication: false }); + + if (secretImports.length) { + const importedSecrets = await fnSecretsV2FromImports({ + decryptor: decryptSecretValue, + folderDAL, + secretDAL: secretV2BridgeDAL, + expandSecretReferences, + secretImportDAL, + secretImports, + hasSecretAccess: () => true, + viewSecretValue: true + }); + + for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { + for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) { + const importedSecret = importedSecrets[i].secrets[j]; + if (!secretMap[importedSecret.key]) { + secretMap[importedSecret.key] = { + skipMultilineEncoding: importedSecret.skipMultilineEncoding, + comment: importedSecret.secretComment, + value: importedSecret.secretValue || "", + secretMetadata: importedSecret.secretMetadata + }; + } + } + } + } + + return secretMap; + }; + + const queueSecretSyncSyncSecretsById = async (payload: TQueueSecretSyncSyncSecretsByIdDTO) => + queueService.queue(QueueName.AppConnectionSecretSync, QueueJobs.SecretSyncSyncSecrets, payload, { + delay: getRequeueDelay(payload.failedToAcquireLockCount), // this is for delaying re-queued jobs if sync is locked + attempts: 5, + backoff: { + type: "exponential", + delay: 3000 + }, + removeOnComplete: true, + removeOnFail: true + }); + + const queueSecretSyncImportSecretsById = async (payload: TQueueSecretSyncImportSecretsByIdDTO) => + queueService.queue(QueueName.AppConnectionSecretSync, QueueJobs.SecretSyncImportSecrets, payload, { + attempts: 1, + removeOnComplete: true, + removeOnFail: true + }); + + const queueSecretSyncRemoveSecretsById = async (payload: TQueueSecretSyncRemoveSecretsByIdDTO) => + queueService.queue(QueueName.AppConnectionSecretSync, QueueJobs.SecretSyncRemoveSecrets, payload, { + attempts: 1, + removeOnComplete: true, + removeOnFail: true + }); + + const $queueSendSecretSyncFailedNotifications = async (payload: TQueueSendSecretSyncActionFailedNotificationsDTO) => { + if (!appCfg.isSmtpConfigured) return; + + await queueService.queue( + QueueName.AppConnectionSecretSync, + QueueJobs.SecretSyncSendActionFailedNotifications, + payload, + { + jobId: `secret-sync-${payload.secretSync.id}-failed-notifications`, + attempts: 5, + delay: 1000 * 60, + backoff: { + type: "exponential", + delay: 3000 + }, + removeOnFail: true, + removeOnComplete: true + } + ); + }; + + const $importSecrets = async ( + secretSync: TSecretSyncWithCredentials, + importBehavior: SecretSyncImportBehavior + ): Promise => { + const { projectId, environment, folder } = secretSync; + + if (!environment || !folder) + throw new Error( + "Invalid Secret Sync source configuration: folder no longer exists. Please update source environment and secret path." + ); + + const importedSecrets = await SecretSyncFns.getSecrets(secretSync, { + appConnectionDAL, + kmsService + }); + + if (!Object.keys(importedSecrets).length) return {}; + + const importedSecretMap: TSecretMap = {}; + + const secretMap = await $getInfisicalSecrets(secretSync, false); + + const secretsToCreate: Parameters[0]["secrets"] = []; + const secretsToUpdate: Parameters[0]["secrets"] = []; + + Object.entries(importedSecrets).forEach(([key, secretData]) => { + const { value, comment = "", skipMultilineEncoding } = secretData; + + const secret = { + secretName: key, + secretValue: value, + type: SecretType.Shared, + secretComment: comment, + skipMultilineEncoding: skipMultilineEncoding ?? undefined + }; + + if (Object.hasOwn(secretMap, key)) { + secretsToUpdate.push(secret); + if (importBehavior === SecretSyncImportBehavior.PrioritizeDestination) importedSecretMap[key] = secretData; + } else { + secretsToCreate.push(secret); + importedSecretMap[key] = secretData; + } + }); + + if (secretsToCreate.length) { + await $createManySecretsRawFn({ + projectId, + path: folder.path, + environment: environment.slug, + secrets: secretsToCreate + }); + } + + if (importBehavior === SecretSyncImportBehavior.PrioritizeDestination && secretsToUpdate.length) { + await $updateManySecretsRawFn({ + projectId, + path: folder.path, + environment: environment.slug, + secrets: secretsToUpdate + }); + } + + if (secretsToUpdate.length || secretsToCreate.length) + await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId); + + return importedSecretMap; + }; + + const $handleSyncSecretsJob = async (job: TSecretSyncSyncSecretsDTO) => { + const { + data: { syncId, auditLogInfo } + } = job; + + const secretSync = await secretSyncDAL.findById(syncId); + + if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`); + + await secretSyncDAL.updateById(syncId, { + syncStatus: SecretSyncStatus.Running + }); + + logger.info( + `SecretSync Sync [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]` + ); + + let isSynced = false; + let syncMessage: string | null = null; + let isFinalAttempt = job.attemptsStarted === job.opts.attempts; + + try { + const { + connection: { orgId, encryptedCredentials } + } = secretSync; + + const credentials = await decryptAppConnectionCredentials({ + orgId, + encryptedCredentials, + kmsService + }); + + const secretSyncWithCredentials = { + ...secretSync, + connection: { + ...secretSync.connection, + credentials + } + } as TSecretSyncWithCredentials; + + const { + lastSyncedAt, + syncOptions: { initialSyncBehavior } + } = secretSyncWithCredentials; + + const secretMap = await $getInfisicalSecrets(secretSync); + + if (!lastSyncedAt && initialSyncBehavior !== SecretSyncInitialSyncBehavior.OverwriteDestination) { + const importedSecretMap = await $importSecrets( + secretSyncWithCredentials, + initialSyncBehavior === SecretSyncInitialSyncBehavior.ImportPrioritizeSource + ? SecretSyncImportBehavior.PrioritizeSource + : SecretSyncImportBehavior.PrioritizeDestination + ); + + Object.entries(importedSecretMap).forEach(([key, secretData]) => { + secretMap[key] = secretData; + }); + } + + await SecretSyncFns.syncSecrets(secretSyncWithCredentials, secretMap, { + appConnectionDAL, + kmsService + }); + + isSynced = true; + } catch (err) { + logger.error( + err, + `SecretSync Sync Error [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]` + ); + + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + syncSecretsErrorHistogram.record(1, { + version: 1, + destination: secretSync.destination, + syncId: secretSync.id, + projectId: secretSync.projectId, + type: err instanceof AxiosError ? "AxiosError" : err?.constructor?.name || "UnknownError", + status: err instanceof AxiosError ? err.response?.status : undefined, + name: err instanceof Error ? err.name : undefined + }); + } + + syncMessage = parseSyncErrorMessage(err); + + if (err instanceof SecretSyncError && !err.shouldRetry) { + isFinalAttempt = true; + } else { + // re-throw so job fails + throw err; + } + } finally { + const ranAt = new Date(); + const syncStatus = isSynced ? SecretSyncStatus.Succeeded : SecretSyncStatus.Failed; + + await auditLogService.createAuditLog({ + projectId: secretSync.projectId, + ...(auditLogInfo ?? { + actor: { + type: ActorType.PLATFORM, + metadata: {} + } + }), + event: { + type: EventType.SECRET_SYNC_SYNC_SECRETS, + metadata: { + syncId: secretSync.id, + syncOptions: secretSync.syncOptions, + destination: secretSync.destination, + destinationConfig: secretSync.destinationConfig, + folderId: secretSync.folderId, + connectionId: secretSync.connectionId, + jobRanAt: ranAt, + jobId: job.id!, + syncStatus, + syncMessage + } + } + }); + + if (isSynced || isFinalAttempt) { + const updatedSecretSync = await secretSyncDAL.updateById(secretSync.id, { + syncStatus, + lastSyncJobId: job.id, + lastSyncMessage: syncMessage, + lastSyncedAt: isSynced ? ranAt : undefined + }); + + if (!isSynced) { + await $queueSendSecretSyncFailedNotifications({ + secretSync: updatedSecretSync, + action: SecretSyncAction.SyncSecrets, + auditLogInfo + }); + } + } + } + + logger.info("SecretSync Sync Job with ID %s Completed", job.id); + }; + + const $handleImportSecretsJob = async (job: TSecretSyncImportSecretsDTO) => { + const { + data: { syncId, auditLogInfo, importBehavior } + } = job; + + const secretSync = await secretSyncDAL.findById(syncId); + + if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`); + + await secretSyncDAL.updateById(syncId, { + importStatus: SecretSyncStatus.Running + }); + + logger.info( + `SecretSync Import [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]` + ); + + let isSuccess = false; + let importMessage: string | null = null; + const isFinalAttempt = job.attemptsStarted === job.opts.attempts; + + try { + const { + connection: { orgId, encryptedCredentials } + } = secretSync; + + const credentials = await decryptAppConnectionCredentials({ + orgId, + encryptedCredentials, + kmsService + }); + + await $importSecrets( + { + ...secretSync, + connection: { + ...secretSync.connection, + credentials + } + } as TSecretSyncWithCredentials, + importBehavior + ); + + isSuccess = true; + } catch (err) { + logger.error( + err, + `SecretSync Import Error [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]` + ); + + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + importSecretsErrorHistogram.record(1, { + version: 1, + destination: secretSync.destination, + syncId: secretSync.id, + projectId: secretSync.projectId, + type: err instanceof AxiosError ? "AxiosError" : err?.constructor?.name || "UnknownError", + status: err instanceof AxiosError ? err.response?.status : undefined, + name: err instanceof Error ? err.name : undefined + }); + } + + importMessage = parseSyncErrorMessage(err); + + // re-throw so job fails + throw err; + } finally { + const ranAt = new Date(); + const importStatus = isSuccess ? SecretSyncStatus.Succeeded : SecretSyncStatus.Failed; + + await auditLogService.createAuditLog({ + projectId: secretSync.projectId, + ...(auditLogInfo ?? { + actor: { + type: ActorType.PLATFORM, + metadata: {} + } + }), + event: { + type: EventType.SECRET_SYNC_IMPORT_SECRETS, + metadata: { + syncId: secretSync.id, + syncOptions: secretSync.syncOptions, + destination: secretSync.destination, + destinationConfig: secretSync.destinationConfig, + folderId: secretSync.folderId, + connectionId: secretSync.connectionId, + jobRanAt: ranAt, + jobId: job.id!, + importStatus, + importMessage, + importBehavior + } + } + }); + + if (isSuccess || isFinalAttempt) { + const updatedSecretSync = await secretSyncDAL.updateById(secretSync.id, { + importStatus, + lastImportJobId: job.id, + lastImportMessage: importMessage, + lastImportedAt: isSuccess ? ranAt : undefined + }); + + if (!isSuccess) { + await $queueSendSecretSyncFailedNotifications({ + secretSync: updatedSecretSync, + action: SecretSyncAction.ImportSecrets, + auditLogInfo + }); + } + } + } + + logger.info("SecretSync Import Job with ID %s Completed", job.id); + }; + + const $handleRemoveSecretsJob = async (job: TSecretSyncRemoveSecretsDTO) => { + const { + data: { syncId, auditLogInfo, deleteSyncOnComplete } + } = job; + + const secretSync = await secretSyncDAL.findById(syncId); + + if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`); + + await secretSyncDAL.updateById(syncId, { + removeStatus: SecretSyncStatus.Running + }); + + logger.info( + `SecretSync Remove [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]` + ); + + let isSuccess = false; + let removeMessage: string | null = null; + const isFinalAttempt = job.attemptsStarted === job.opts.attempts; + + try { + const { + connection: { orgId, encryptedCredentials } + } = secretSync; + + const credentials = await decryptAppConnectionCredentials({ + orgId, + encryptedCredentials, + kmsService + }); + + const secretMap = await $getInfisicalSecrets(secretSync); + + await SecretSyncFns.removeSecrets( + { + ...secretSync, + connection: { + ...secretSync.connection, + credentials + } + } as TSecretSyncWithCredentials, + secretMap, + { + appConnectionDAL, + kmsService + } + ); + + isSuccess = true; + } catch (err) { + logger.error( + err, + `SecretSync Remove Error [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]` + ); + + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + removeSecretsErrorHistogram.record(1, { + version: 1, + destination: secretSync.destination, + syncId: secretSync.id, + projectId: secretSync.projectId, + type: err instanceof AxiosError ? "AxiosError" : err?.constructor?.name || "UnknownError", + status: err instanceof AxiosError ? err.response?.status : undefined, + name: err instanceof Error ? err.name : undefined + }); + } + + removeMessage = parseSyncErrorMessage(err); + + // re-throw so job fails + throw err; + } finally { + const ranAt = new Date(); + const removeStatus = isSuccess ? SecretSyncStatus.Succeeded : SecretSyncStatus.Failed; + + await auditLogService.createAuditLog({ + projectId: secretSync.projectId, + ...(auditLogInfo ?? { + actor: { + type: ActorType.PLATFORM, + metadata: {} + } + }), + event: { + type: EventType.SECRET_SYNC_REMOVE_SECRETS, + metadata: { + syncId: secretSync.id, + syncOptions: secretSync.syncOptions, + destination: secretSync.destination, + destinationConfig: secretSync.destinationConfig, + folderId: secretSync.folderId, + connectionId: secretSync.connectionId, + jobRanAt: ranAt, + jobId: job.id!, + removeStatus, + removeMessage + } + } + }); + + if (isSuccess || isFinalAttempt) { + if (isSuccess && deleteSyncOnComplete) { + await secretSyncDAL.deleteById(secretSync.id); + } else { + const updatedSecretSync = await secretSyncDAL.updateById(secretSync.id, { + removeStatus, + lastRemoveJobId: job.id, + lastRemoveMessage: removeMessage, + lastRemovedAt: isSuccess ? ranAt : undefined + }); + + if (!isSuccess) { + await $queueSendSecretSyncFailedNotifications({ + secretSync: updatedSecretSync, + action: SecretSyncAction.RemoveSecrets, + auditLogInfo + }); + } + } + } + } + + logger.info("SecretSync Remove Job with ID %s Completed", job.id); + }; + + const $sendSecretSyncFailedNotifications = async (job: TSendSecretSyncFailedNotificationsJobDTO) => { + const { + data: { secretSync, auditLogInfo, action } + } = job; + + const { projectId, destination, name, folder, lastSyncMessage, lastRemoveMessage, lastImportMessage, environment } = + secretSync; + + const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId); + const project = await projectDAL.findById(projectId); + + let projectAdmins = projectMembers.filter((member) => + member.roles.some((role) => role.role === ProjectMembershipRole.Admin) + ); + + const triggeredByUserId = + auditLogInfo && auditLogInfo.actor.type === ActorType.USER && auditLogInfo.actor.metadata.userId; + + // only notify triggering user if triggered by admin + if (triggeredByUserId && projectAdmins.map((admin) => admin.userId).includes(triggeredByUserId)) { + projectAdmins = projectAdmins.filter((admin) => admin.userId === triggeredByUserId); + } + + const syncDestination = SECRET_SYNC_NAME_MAP[destination as SecretSync]; + + let actionLabel: string; + let failureMessage: string | null | undefined; + + switch (action) { + case SecretSyncAction.ImportSecrets: + actionLabel = "Import"; + failureMessage = lastImportMessage; + + break; + case SecretSyncAction.RemoveSecrets: + actionLabel = "Remove"; + failureMessage = lastRemoveMessage; + + break; + case SecretSyncAction.SyncSecrets: + default: + actionLabel = `Sync`; + failureMessage = lastSyncMessage; + break; + } + + await smtpService.sendMail({ + recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean), + template: SmtpTemplates.SecretSyncFailed, + subjectLine: `Secret Sync Failed to ${actionLabel} Secrets`, + substitutions: { + syncName: name, + syncDestination, + content: `Your ${syncDestination} Sync named "${name}" failed while attempting to ${action.toLowerCase()} secrets.`, + failureMessage, + secretPath: folder?.path, + environment: environment?.name, + projectName: project.name, + syncUrl: `${appCfg.SITE_URL}/integrations/secret-syncs/${destination}/${secretSync.id}` + } + }); + }; + + const queueSecretSyncsSyncSecretsByPath = async ({ + secretPath, + projectId, + environmentSlug + }: TQueueSecretSyncsByPathDTO) => { + const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, secretPath); + + if (!folder) + throw new Error( + `Could not find folder at path "${secretPath}" for environment with slug "${environmentSlug}" in project with ID "${projectId}"` + ); + + const secretSyncs = await secretSyncDAL.find({ folderId: folder.id, isAutoSyncEnabled: true }); + + await Promise.all(secretSyncs.map((secretSync) => queueSecretSyncSyncSecretsById({ syncId: secretSync.id }))); + }; + + const $handleAcquireLockFailure = async (job: SecretSyncActionJob) => { + const { syncId, auditLogInfo } = job.data; + + switch (job.name) { + case QueueJobs.SecretSyncSyncSecrets: { + const { failedToAcquireLockCount = 0, ...rest } = job.data as TQueueSecretSyncSyncSecretsByIdDTO; + + if (failedToAcquireLockCount < 10) { + await queueSecretSyncSyncSecretsById({ ...rest, failedToAcquireLockCount: failedToAcquireLockCount + 1 }); + return; + } + + const secretSync = await secretSyncDAL.updateById(syncId, { + syncStatus: SecretSyncStatus.Failed, + lastSyncMessage: + "Failed to run job. This typically happens when a sync is already in progress. Please try again.", + lastSyncJobId: job.id + }); + + await $queueSendSecretSyncFailedNotifications({ + secretSync, + action: SecretSyncAction.SyncSecrets, + auditLogInfo + }); + + break; + } + // Scott: the two cases below are unlikely to happen as we check the lock at the API level but including this as a fallback + case QueueJobs.SecretSyncImportSecrets: { + const secretSync = await secretSyncDAL.updateById(syncId, { + importStatus: SecretSyncStatus.Failed, + lastImportMessage: + "Failed to run job. This typically happens when a sync is already in progress. Please try again.", + lastImportJobId: job.id + }); + + await $queueSendSecretSyncFailedNotifications({ + secretSync, + action: SecretSyncAction.ImportSecrets, + auditLogInfo + }); + + break; + } + case QueueJobs.SecretSyncRemoveSecrets: { + const secretSync = await secretSyncDAL.updateById(syncId, { + removeStatus: SecretSyncStatus.Failed, + lastRemoveMessage: + "Failed to run job. This typically happens when a sync is already in progress. Please try again.", + lastRemoveJobId: job.id + }); + + await $queueSendSecretSyncFailedNotifications({ + secretSync, + action: SecretSyncAction.RemoveSecrets, + auditLogInfo + }); + + break; + } + default: + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new Error(`Unhandled Secret Sync Job ${job.name}`); + } + }; + + queueService.start(QueueName.AppConnectionSecretSync, async (job) => { + if (job.name === QueueJobs.SecretSyncSendActionFailedNotifications) { + await $sendSecretSyncFailedNotifications(job as TSendSecretSyncFailedNotificationsJobDTO); + return; + } + + const { syncId } = job.data as + | TQueueSecretSyncSyncSecretsByIdDTO + | TQueueSecretSyncImportSecretsByIdDTO + | TQueueSecretSyncRemoveSecretsByIdDTO; + + let lock: Awaited>; + + try { + lock = await keyStore.acquireLock( + [KeyStorePrefixes.SecretSyncLock(syncId)], + // scott: not sure on this duration; syncs can take excessive amounts of time so we need to keep it locked, + // but should always release below... + 5 * 60 * 1000 + ); + } catch (e) { + logger.info(`SecretSync Failed to acquire lock [syncId=${syncId}] [job=${job.name}]`); + + await $handleAcquireLockFailure(job as SecretSyncActionJob); + + return; + } + + try { + switch (job.name) { + case QueueJobs.SecretSyncSyncSecrets: + await $handleSyncSecretsJob(job as TSecretSyncSyncSecretsDTO); + break; + case QueueJobs.SecretSyncImportSecrets: + await $handleImportSecretsJob(job as TSecretSyncImportSecretsDTO); + break; + case QueueJobs.SecretSyncRemoveSecrets: + await $handleRemoveSecretsJob(job as TSecretSyncRemoveSecretsDTO); + break; + default: + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new Error(`Unhandled Secret Sync Job ${job.name}`); + } + } finally { + await lock.release(); + } + }); + + return { + queueSecretSyncSyncSecretsById, + queueSecretSyncImportSecretsById, + queueSecretSyncRemoveSecretsById, + queueSecretSyncsSyncSecretsByPath + }; +}; diff --git a/backend/src/services/secret-sync/secret-sync-schemas.ts b/backend/src/services/secret-sync/secret-sync-schemas.ts new file mode 100644 index 000000000..50ff3f307 --- /dev/null +++ b/backend/src/services/secret-sync/secret-sync-schemas.ts @@ -0,0 +1,114 @@ +import { AnyZodObject, z } from "zod"; + +import { SecretSyncsSchema } from "@app/db/schemas/secret-syncs"; +import { SecretSyncs } from "@app/lib/api-docs"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { slugSchema } from "@app/server/lib/schemas"; +import { SecretSync, SecretSyncInitialSyncBehavior } from "@app/services/secret-sync/secret-sync-enums"; +import { SECRET_SYNC_CONNECTION_MAP } from "@app/services/secret-sync/secret-sync-maps"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const BaseSyncOptionsSchema = ({ + destination, + syncOptionsConfig: { canImportSecrets }, + merge, + isUpdateSchema +}: { + destination: SecretSync; + syncOptionsConfig: TSyncOptionsConfig; + merge?: T; + isUpdateSchema?: boolean; +}) => { + const baseSchema = z.object({ + initialSyncBehavior: (canImportSecrets + ? z.nativeEnum(SecretSyncInitialSyncBehavior) + : z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination) + ).describe(SecretSyncs.SYNC_OPTIONS(destination).initialSyncBehavior), + disableSecretDeletion: z.boolean().optional().describe(SecretSyncs.SYNC_OPTIONS(destination).disableSecretDeletion) + }); + + const schema = merge ? baseSchema.merge(merge) : baseSchema; + + return ( + isUpdateSchema + ? schema.describe(SecretSyncs.UPDATE(destination).syncOptions).optional() + : schema.describe(SecretSyncs.CREATE(destination).syncOptions) + ) as T extends AnyZodObject ? z.ZodObject> : typeof schema; +}; + +export const BaseSecretSyncSchema = ( + destination: SecretSync, + syncOptionsConfig: TSyncOptionsConfig, + merge?: T +) => + SecretSyncsSchema.omit({ + destination: true, + destinationConfig: true, + syncOptions: true + }).extend({ + // destination needs to be on the extended object for type differentiation + syncOptions: BaseSyncOptionsSchema({ destination, syncOptionsConfig, merge }), + // join properties + projectId: z.string(), + connection: z.object({ + app: z.literal(SECRET_SYNC_CONNECTION_MAP[destination]), + name: z.string(), + id: z.string().uuid() + }), + environment: z.object({ slug: z.string(), name: z.string(), id: z.string().uuid() }).nullable(), + folder: z.object({ id: z.string(), path: z.string() }).nullable() + }); + +export const GenericCreateSecretSyncFieldsSchema = ( + destination: SecretSync, + syncOptionsConfig: TSyncOptionsConfig, + merge?: T +) => + z.object({ + name: slugSchema({ field: "name" }).describe(SecretSyncs.CREATE(destination).name), + projectId: z.string().trim().min(1, "Project ID required").describe(SecretSyncs.CREATE(destination).projectId), + description: z + .string() + .trim() + .max(256, "Description cannot exceed 256 characters") + .nullish() + .describe(SecretSyncs.CREATE(destination).description), + connectionId: z.string().uuid().describe(SecretSyncs.CREATE(destination).connectionId), + environment: slugSchema({ field: "environment", max: 64 }).describe(SecretSyncs.CREATE(destination).environment), + secretPath: z + .string() + .trim() + .min(1, "Secret path required") + .transform(removeTrailingSlash) + .describe(SecretSyncs.CREATE(destination).secretPath), + isAutoSyncEnabled: z.boolean().default(true).describe(SecretSyncs.CREATE(destination).isAutoSyncEnabled), + syncOptions: BaseSyncOptionsSchema({ destination, syncOptionsConfig, merge }) + }); + +export const GenericUpdateSecretSyncFieldsSchema = ( + destination: SecretSync, + syncOptionsConfig: TSyncOptionsConfig, + merge?: T +) => + z.object({ + name: slugSchema({ field: "name" }).describe(SecretSyncs.UPDATE(destination).name).optional(), + connectionId: z.string().uuid().describe(SecretSyncs.UPDATE(destination).connectionId).optional(), + description: z + .string() + .trim() + .max(256, "Description cannot exceed 256 characters") + .nullish() + .describe(SecretSyncs.UPDATE(destination).description), + environment: slugSchema({ field: "environment", max: 64 }) + .optional() + .describe(SecretSyncs.UPDATE(destination).environment), + secretPath: z + .string() + .trim() + .min(1, "Invalid secret path") + .transform(removeTrailingSlash) + .optional() + .describe(SecretSyncs.UPDATE(destination).secretPath), + isAutoSyncEnabled: z.boolean().optional().describe(SecretSyncs.UPDATE(destination).isAutoSyncEnabled), + syncOptions: BaseSyncOptionsSchema({ destination, syncOptionsConfig, merge, isUpdateSchema: true }) + }); diff --git a/backend/src/services/secret-sync/secret-sync-service.ts b/backend/src/services/secret-sync/secret-sync-service.ts new file mode 100644 index 000000000..14a1a1cf0 --- /dev/null +++ b/backend/src/services/secret-sync/secret-sync-service.ts @@ -0,0 +1,530 @@ +import { ForbiddenError } from "@casl/ability"; + +import { ActionProjectType } from "@app/db/schemas"; +import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { + ProjectPermissionSecretActions, + ProjectPermissionSecretSyncActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; +import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; +import { DatabaseErrorCode } from "@app/lib/error-codes"; +import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; +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"; +import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { listSecretSyncOptions } from "@app/services/secret-sync/secret-sync-fns"; +import { + SecretSyncStatus, + TCreateSecretSyncDTO, + TDeleteSecretSyncDTO, + TFindSecretSyncByIdDTO, + TFindSecretSyncByNameDTO, + TListSecretSyncsByProjectId, + TSecretSync, + TTriggerSecretSyncImportSecretsByIdDTO, + TTriggerSecretSyncRemoveSecretsByIdDTO, + TTriggerSecretSyncSyncSecretsByIdDTO, + TUpdateSecretSyncDTO +} from "@app/services/secret-sync/secret-sync-types"; + +import { TSecretSyncDALFactory } from "./secret-sync-dal"; +import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "./secret-sync-maps"; +import { TSecretSyncQueueFactory } from "./secret-sync-queue"; + +type TSecretSyncServiceFactoryDep = { + secretSyncDAL: TSecretSyncDALFactory; + appConnectionService: Pick; + permissionService: Pick; + projectBotService: Pick; + folderDAL: Pick; + keyStore: Pick; + secretSyncQueue: Pick< + TSecretSyncQueueFactory, + "queueSecretSyncSyncSecretsById" | "queueSecretSyncImportSecretsById" | "queueSecretSyncRemoveSecretsById" + >; +}; + +export type TSecretSyncServiceFactory = ReturnType; + +export const secretSyncServiceFactory = ({ + secretSyncDAL, + folderDAL, + permissionService, + appConnectionService, + projectBotService, + secretSyncQueue, + keyStore +}: TSecretSyncServiceFactoryDep) => { + const listSecretSyncsByProjectId = async ( + { projectId, destination }: TListSecretSyncsByProjectId, + actor: OrgServiceActor + ) => { + 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 + ); + + const secretSyncs = await secretSyncDAL.find({ + ...(destination && { destination }), + projectId + }); + + return secretSyncs as TSecretSync[]; + }; + + const findSecretSyncById = async ({ destination, syncId }: TFindSecretSyncByIdDTO, actor: OrgServiceActor) => { + const secretSync = await secretSyncDAL.findById(syncId); + + if (!secretSync) + throw new NotFoundError({ + message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"` + }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager, + projectId: secretSync.projectId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretSyncActions.Read, + ProjectPermissionSub.SecretSyncs + ); + + if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination]) + throw new BadRequestError({ + message: `Secret sync with ID "${secretSync.id}" is not configured for ${SECRET_SYNC_NAME_MAP[destination]}` + }); + + return secretSync as TSecretSync; + }; + + const findSecretSyncByName = async ( + { destination, syncName, projectId }: TFindSecretSyncByNameDTO, + actor: OrgServiceActor + ) => { + // we prevent conflicting names within a project + const secretSync = await secretSyncDAL.findOne({ + name: syncName, + projectId + }); + + if (!secretSync) + throw new NotFoundError({ + message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with name "${syncName}"` + }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager, + projectId: secretSync.projectId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretSyncActions.Read, + ProjectPermissionSub.SecretSyncs + ); + + if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination]) + throw new BadRequestError({ + message: `Secret sync with ID "${secretSync.id}" is not configured for ${SECRET_SYNC_NAME_MAP[destination]}` + }); + + return secretSync as TSecretSync; + }; + + const createSecretSync = async ( + { projectId, secretPath, environment, ...params }: TCreateSecretSyncDTO, + actor: OrgServiceActor + ) => { + const { permission: projectPermission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager, + projectId + }); + + const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); + + if (!shouldUseSecretV2Bridge) + throw new BadRequestError({ message: "Project version does not support Secret Syncs" }); + + ForbiddenError.from(projectPermission).throwUnlessCan( + ProjectPermissionSecretSyncActions.Create, + ProjectPermissionSub.SecretSyncs + ); + + throwIfMissingSecretReadValueOrDescribePermission(projectPermission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath + }); + + const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); + + if (!folder) + throw new BadRequestError({ + message: `Could not find folder with path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"` + }); + + const destinationApp = SECRET_SYNC_CONNECTION_MAP[params.destination]; + + // validates permission to connect and app is valid for sync destination + await appConnectionService.connectAppConnectionById(destinationApp, params.connectionId, actor); + + try { + const secretSync = await secretSyncDAL.create({ + folderId: folder.id, + ...params, + ...(params.isAutoSyncEnabled && { syncStatus: SecretSyncStatus.Pending }), + projectId + }); + + if (secretSync.isAutoSyncEnabled) await secretSyncQueue.queueSecretSyncSyncSecretsById({ syncId: secretSync.id }); + + return secretSync as TSecretSync; + } catch (err) { + if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) { + throw new BadRequestError({ + message: `A Secret Sync with the name "${params.name}" already exists for the project with ID "${folder.projectId}"` + }); + } + + throw err; + } + }; + + const updateSecretSync = async ( + { destination, syncId, secretPath, environment, ...params }: TUpdateSecretSyncDTO, + actor: OrgServiceActor + ) => { + const secretSync = await secretSyncDAL.findById(syncId); + + if (!secretSync) + throw new NotFoundError({ + message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID ${syncId}` + }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager, + projectId: secretSync.projectId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretSyncActions.Edit, + ProjectPermissionSub.SecretSyncs + ); + + if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination]) + throw new BadRequestError({ + message: `Secret sync with ID "${secretSync.id}" is not configured for ${SECRET_SYNC_NAME_MAP[destination]}` + }); + + let { folderId } = secretSync; + + if (params.connectionId) { + const destinationApp = SECRET_SYNC_CONNECTION_MAP[secretSync.destination as SecretSync]; + + // validates permission to connect and app is valid for sync destination + await appConnectionService.connectAppConnectionById(destinationApp, params.connectionId, actor); + } + + if ( + (secretPath && secretPath !== secretSync.folder?.path) || + (environment && environment !== secretSync.environment?.slug) + ) { + const updatedEnvironment = environment ?? secretSync.environment?.slug; + const updatedSecretPath = secretPath ?? secretSync.folder?.path; + + if (!updatedEnvironment || !updatedSecretPath) + throw new BadRequestError({ message: "Must specify both source environment and secret path" }); + + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment: updatedEnvironment, + secretPath: updatedSecretPath + }); + + const newFolder = await folderDAL.findBySecretPath(secretSync.projectId, updatedEnvironment, updatedSecretPath); + + if (!newFolder) + throw new BadRequestError({ + message: `Could not find folder with path "${secretPath}" in environment "${environment}" for project with ID "${secretSync.projectId}"` + }); + + folderId = newFolder.id; + } + + const isAutoSyncEnabled = params.isAutoSyncEnabled ?? secretSync.isAutoSyncEnabled; + + try { + const updatedSecretSync = await secretSyncDAL.updateById(syncId, { + ...params, + ...(isAutoSyncEnabled && folderId && { syncStatus: SecretSyncStatus.Pending }), + folderId + }); + + if (updatedSecretSync.isAutoSyncEnabled) + await secretSyncQueue.queueSecretSyncSyncSecretsById({ syncId: secretSync.id }); + + return updatedSecretSync as TSecretSync; + } catch (err) { + if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) { + throw new BadRequestError({ + message: `A Secret Sync with the name "${params.name}" already exists for the project with ID "${secretSync.projectId}"` + }); + } + + throw err; + } + }; + + const deleteSecretSync = async ( + { destination, syncId, removeSecrets }: TDeleteSecretSyncDTO, + actor: OrgServiceActor + ) => { + const secretSync = await secretSyncDAL.findById(syncId); + + if (!secretSync) + throw new NotFoundError({ + message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"` + }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager, + projectId: secretSync.projectId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretSyncActions.Delete, + ProjectPermissionSub.SecretSyncs + ); + + if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination]) + throw new BadRequestError({ + message: `Secret sync with ID "${secretSync.id}" is not configured for ${SECRET_SYNC_NAME_MAP[destination]}` + }); + + if (removeSecrets) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretSyncActions.RemoveSecrets, + ProjectPermissionSub.SecretSyncs + ); + + if (!secretSync.folderId) + throw new BadRequestError({ + message: `Invalid source configuration: folder no longer exists. Please configure a valid source and try again.` + }); + + const isSyncJobRunning = Boolean(await keyStore.getItem(KeyStorePrefixes.SecretSyncLock(syncId))); + + if (isSyncJobRunning) + throw new BadRequestError({ message: `A job for this sync is already in progress. Please try again shortly.` }); + + await secretSyncQueue.queueSecretSyncRemoveSecretsById({ syncId, deleteSyncOnComplete: true }); + + const updatedSecretSync = await secretSyncDAL.updateById(syncId, { + removeStatus: SecretSyncStatus.Pending + }); + + return updatedSecretSync; + } + + await secretSyncDAL.deleteById(syncId); + + return secretSync as TSecretSync; + }; + + const triggerSecretSyncSyncSecretsById = async ( + { syncId, destination, ...params }: TTriggerSecretSyncSyncSecretsByIdDTO, + actor: OrgServiceActor + ) => { + const secretSync = await secretSyncDAL.findById(syncId); + + if (!secretSync) + throw new NotFoundError({ + message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"` + }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager, + projectId: secretSync.projectId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretSyncActions.SyncSecrets, + ProjectPermissionSub.SecretSyncs + ); + + if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination]) + throw new BadRequestError({ + message: `Secret sync with ID "${secretSync.id}" is not configured for ${SECRET_SYNC_NAME_MAP[destination]}` + }); + + if (!secretSync.folderId) + throw new BadRequestError({ + message: `Invalid source configuration: folder no longer exists. Please configure a valid source and try again.` + }); + + const isSyncJobRunning = Boolean(await keyStore.getItem(KeyStorePrefixes.SecretSyncLock(syncId))); + + if (isSyncJobRunning) + throw new BadRequestError({ message: `A job for this sync is already in progress. Please try again shortly.` }); + + await secretSyncQueue.queueSecretSyncSyncSecretsById({ syncId, ...params }); + + const updatedSecretSync = await secretSyncDAL.updateById(syncId, { + syncStatus: SecretSyncStatus.Pending + }); + + return updatedSecretSync as TSecretSync; + }; + + const triggerSecretSyncImportSecretsById = async ( + { syncId, destination, ...params }: TTriggerSecretSyncImportSecretsByIdDTO, + actor: OrgServiceActor + ) => { + if (!listSecretSyncOptions().find((option) => option.destination === destination)?.canImportSecrets) { + throw new BadRequestError({ + message: `${SECRET_SYNC_NAME_MAP[destination]} does not support importing secrets.` + }); + } + + const secretSync = await secretSyncDAL.findById(syncId); + + if (!secretSync) + throw new NotFoundError({ + message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"` + }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager, + projectId: secretSync.projectId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretSyncActions.ImportSecrets, + ProjectPermissionSub.SecretSyncs + ); + + if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination]) + throw new BadRequestError({ + message: `Secret sync with ID "${secretSync.id}" is not configured for ${SECRET_SYNC_NAME_MAP[destination]}` + }); + + if (!secretSync.folderId) + throw new BadRequestError({ + message: `Invalid source configuration: folder no longer exists. Please configure a valid source and try again.` + }); + + const isSyncJobRunning = Boolean(await keyStore.getItem(KeyStorePrefixes.SecretSyncLock(syncId))); + + if (isSyncJobRunning) + throw new BadRequestError({ message: `A job for this sync is already in progress. Please try again shortly.` }); + + await secretSyncQueue.queueSecretSyncImportSecretsById({ syncId, ...params }); + + const updatedSecretSync = await secretSyncDAL.updateById(syncId, { + importStatus: SecretSyncStatus.Pending + }); + + return updatedSecretSync as TSecretSync; + }; + + const triggerSecretSyncRemoveSecretsById = async ( + { syncId, destination, ...params }: TTriggerSecretSyncRemoveSecretsByIdDTO, + actor: OrgServiceActor + ) => { + const secretSync = await secretSyncDAL.findById(syncId); + + if (!secretSync) + throw new NotFoundError({ + message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"` + }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager, + projectId: secretSync.projectId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretSyncActions.RemoveSecrets, + ProjectPermissionSub.SecretSyncs + ); + + if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination]) + throw new BadRequestError({ + message: `Secret sync with ID "${secretSync.id}" is not configured for ${SECRET_SYNC_NAME_MAP[destination]}` + }); + + if (!secretSync.folderId) + throw new BadRequestError({ + message: `Invalid source configuration: folder no longer exists. Please configure a valid source and try again.` + }); + + const isSyncJobRunning = Boolean(await keyStore.getItem(KeyStorePrefixes.SecretSyncLock(syncId))); + + if (isSyncJobRunning) + throw new BadRequestError({ message: `A job for this sync is already in progress. Please try again shortly.` }); + + await secretSyncQueue.queueSecretSyncRemoveSecretsById({ syncId, ...params }); + + const updatedSecretSync = await secretSyncDAL.updateById(syncId, { + removeStatus: SecretSyncStatus.Pending + }); + + return updatedSecretSync as TSecretSync; + }; + + return { + listSecretSyncOptions, + listSecretSyncsByProjectId, + findSecretSyncById, + findSecretSyncByName, + createSecretSync, + updateSecretSync, + deleteSecretSync, + triggerSecretSyncSyncSecretsById, + triggerSecretSyncImportSecretsById, + triggerSecretSyncRemoveSecretsById + }; +}; diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts new file mode 100644 index 000000000..03e92a57a --- /dev/null +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -0,0 +1,250 @@ +import { Job } from "bullmq"; + +import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types"; +import { QueueJobs } from "@app/queue"; +import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema"; +import { + TAwsSecretsManagerSync, + TAwsSecretsManagerSyncInput, + TAwsSecretsManagerSyncListItem, + TAwsSecretsManagerSyncWithCredentials +} from "@app/services/secret-sync/aws-secrets-manager"; +import { + TCamundaSync, + TCamundaSyncInput, + TCamundaSyncListItem, + TCamundaSyncWithCredentials +} from "@app/services/secret-sync/camunda"; +import { + TDatabricksSync, + TDatabricksSyncInput, + TDatabricksSyncListItem, + TDatabricksSyncWithCredentials +} from "@app/services/secret-sync/databricks"; +import { + TGitHubSync, + TGitHubSyncInput, + TGitHubSyncListItem, + TGitHubSyncWithCredentials +} from "@app/services/secret-sync/github"; +import { TSecretSyncDALFactory } from "@app/services/secret-sync/secret-sync-dal"; +import { SecretSync, SecretSyncImportBehavior } from "@app/services/secret-sync/secret-sync-enums"; +import { + TWindmillSync, + TWindmillSyncInput, + TWindmillSyncListItem, + TWindmillSyncWithCredentials +} from "@app/services/secret-sync/windmill"; + +import { + TAwsParameterStoreSync, + TAwsParameterStoreSyncInput, + TAwsParameterStoreSyncListItem, + TAwsParameterStoreSyncWithCredentials +} from "./aws-parameter-store"; +import { + TAzureAppConfigurationSync, + TAzureAppConfigurationSyncInput, + TAzureAppConfigurationSyncListItem, + TAzureAppConfigurationSyncWithCredentials +} from "./azure-app-configuration"; +import { + TAzureKeyVaultSync, + TAzureKeyVaultSyncInput, + TAzureKeyVaultSyncListItem, + TAzureKeyVaultSyncWithCredentials +} from "./azure-key-vault"; +import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp"; +import { + THumanitecSync, + THumanitecSyncInput, + THumanitecSyncListItem, + THumanitecSyncWithCredentials +} from "./humanitec"; +import { + TTerraformCloudSync, + TTerraformCloudSyncInput, + TTerraformCloudSyncListItem, + TTerraformCloudSyncWithCredentials +} from "./terraform-cloud"; +import { TVercelSync, TVercelSyncInput, TVercelSyncListItem, TVercelSyncWithCredentials } from "./vercel"; + +export type TSecretSync = + | TAwsParameterStoreSync + | TAwsSecretsManagerSync + | TGitHubSync + | TGcpSync + | TAzureKeyVaultSync + | TAzureAppConfigurationSync + | TDatabricksSync + | THumanitecSync + | TTerraformCloudSync + | TCamundaSync + | TVercelSync + | TWindmillSync; + +export type TSecretSyncWithCredentials = + | TAwsParameterStoreSyncWithCredentials + | TAwsSecretsManagerSyncWithCredentials + | TGitHubSyncWithCredentials + | TGcpSyncWithCredentials + | TAzureKeyVaultSyncWithCredentials + | TAzureAppConfigurationSyncWithCredentials + | TDatabricksSyncWithCredentials + | THumanitecSyncWithCredentials + | TTerraformCloudSyncWithCredentials + | TCamundaSyncWithCredentials + | TVercelSyncWithCredentials + | TWindmillSyncWithCredentials; + +export type TSecretSyncInput = + | TAwsParameterStoreSyncInput + | TAwsSecretsManagerSyncInput + | TGitHubSyncInput + | TGcpSyncInput + | TAzureKeyVaultSyncInput + | TAzureAppConfigurationSyncInput + | TDatabricksSyncInput + | THumanitecSyncInput + | TTerraformCloudSyncInput + | TCamundaSyncInput + | TVercelSyncInput + | TWindmillSyncInput; + +export type TSecretSyncListItem = + | TAwsParameterStoreSyncListItem + | TAwsSecretsManagerSyncListItem + | TGitHubSyncListItem + | TGcpSyncListItem + | TAzureKeyVaultSyncListItem + | TAzureAppConfigurationSyncListItem + | TDatabricksSyncListItem + | THumanitecSyncListItem + | TTerraformCloudSyncListItem + | TCamundaSyncListItem + | TVercelSyncListItem + | TWindmillSyncListItem; + +export type TSyncOptionsConfig = { + canImportSecrets: boolean; +}; + +export type TListSecretSyncsByProjectId = { + projectId: string; + destination?: SecretSync; +}; + +export type TFindSecretSyncByIdDTO = { + syncId: string; + destination: SecretSync; +}; + +export type TFindSecretSyncByNameDTO = { + syncName: string; + projectId: string; + destination: SecretSync; +}; + +export type TCreateSecretSyncDTO = Pick & { + destination: SecretSync; + projectId: string; + secretPath: string; + environment: string; + isAutoSyncEnabled?: boolean; +}; + +export type TUpdateSecretSyncDTO = Partial> & { + syncId: string; + destination: SecretSync; +}; + +export type TDeleteSecretSyncDTO = { + destination: SecretSync; + syncId: string; + removeSecrets: boolean; +}; + +export enum SecretSyncStatus { + Pending = "pending", + Running = "running", + Succeeded = "succeeded", + Failed = "failed" +} + +export enum SecretSyncAction { + SyncSecrets = "sync-secrets", + ImportSecrets = "import-secrets", + RemoveSecrets = "remove-secrets" +} + +export type TSecretSyncRaw = NonNullable>>; + +export type TQueueSecretSyncsByPathDTO = { + secretPath: string; + environmentSlug: string; + projectId: string; +}; + +export type TQueueSecretSyncSyncSecretsByIdDTO = { + syncId: string; + failedToAcquireLockCount?: number; + auditLogInfo?: AuditLogInfo; +}; + +export type TTriggerSecretSyncSyncSecretsByIdDTO = { + destination: SecretSync; +} & TQueueSecretSyncSyncSecretsByIdDTO; + +export type TQueueSecretSyncImportSecretsByIdDTO = { + syncId: string; + importBehavior: SecretSyncImportBehavior; + auditLogInfo?: AuditLogInfo; +}; + +export type TTriggerSecretSyncImportSecretsByIdDTO = { + destination: SecretSync; +} & TQueueSecretSyncImportSecretsByIdDTO; + +export type TQueueSecretSyncRemoveSecretsByIdDTO = { + syncId: string; + auditLogInfo?: AuditLogInfo; + deleteSyncOnComplete?: boolean; +}; + +export type TTriggerSecretSyncRemoveSecretsByIdDTO = { + destination: SecretSync; +} & TQueueSecretSyncRemoveSecretsByIdDTO; + +export type TQueueSendSecretSyncActionFailedNotificationsDTO = { + secretSync: TSecretSyncRaw; + auditLogInfo?: AuditLogInfo; + action: SecretSyncAction; +}; + +export type TSecretSyncSyncSecretsDTO = Job; +export type TSecretSyncImportSecretsDTO = Job< + TQueueSecretSyncImportSecretsByIdDTO, + void, + QueueJobs.SecretSyncSyncSecrets +>; +export type TSecretSyncRemoveSecretsDTO = Job< + TQueueSecretSyncRemoveSecretsByIdDTO, + void, + QueueJobs.SecretSyncSyncSecrets +>; + +export type TSendSecretSyncFailedNotificationsJobDTO = Job< + TQueueSendSecretSyncActionFailedNotificationsDTO, + void, + QueueJobs.SecretSyncSendActionFailedNotifications +>; + +export type TSecretMap = Record< + string, + { + value: string; + comment?: string; + skipMultilineEncoding?: boolean | null | undefined; + secretMetadata?: ResourceMetadataDTO; + } +>; diff --git a/backend/src/services/secret-sync/terraform-cloud/index.ts b/backend/src/services/secret-sync/terraform-cloud/index.ts new file mode 100644 index 000000000..2df19747d --- /dev/null +++ b/backend/src/services/secret-sync/terraform-cloud/index.ts @@ -0,0 +1,5 @@ +export * from "./terraform-cloud-sync-constants"; +export * from "./terraform-cloud-sync-enums"; +export * from "./terraform-cloud-sync-fns"; +export * from "./terraform-cloud-sync-schemas"; +export * from "./terraform-cloud-sync-types"; diff --git a/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-constants.ts b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-constants.ts new file mode 100644 index 000000000..edca7d304 --- /dev/null +++ b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const TERRAFORM_CLOUD_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Terraform Cloud", + destination: SecretSync.TerraformCloud, + connection: AppConnection.TerraformCloud, + canImportSecrets: false +}; diff --git a/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-enums.ts b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-enums.ts new file mode 100644 index 000000000..cfd1daf2c --- /dev/null +++ b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-enums.ts @@ -0,0 +1,9 @@ +export enum TerraformCloudSyncScope { + VariableSet = "variable-set", + Workspace = "workspace" +} + +export enum TerraformCloudSyncCategory { + Environment = "env", + Terraform = "terraform" +} diff --git a/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-fns.ts b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-fns.ts new file mode 100644 index 000000000..4cfd7ec05 --- /dev/null +++ b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-fns.ts @@ -0,0 +1,253 @@ +/* eslint-disable no-await-in-loop */ +import { AxiosResponse } from "axios"; + +import { request } from "@app/lib/config/request"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; + +import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps"; +import { TerraformCloudSyncScope } from "./terraform-cloud-sync-enums"; +import { + TerraformCloudApiResponse, + TerraformCloudApiVariable, + TerraformCloudVariable, + TTerraformCloudSyncWithCredentials +} from "./terraform-cloud-sync-types"; + +const getTerraformCloudVariables = async ( + secretSync: TTerraformCloudSyncWithCredentials +): Promise => { + const { + destinationConfig, + connection: { + credentials: { apiToken } + } + } = secretSync; + + let url: string; + let source: TerraformCloudVariable["source"]; + + if (destinationConfig.scope === TerraformCloudSyncScope.VariableSet) { + url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/varsets/${destinationConfig.variableSetId}/relationships/vars`; + source = "varset"; + } else { + url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${destinationConfig.workspaceId}/vars`; + source = "workspace"; + } + + const headers = { + Authorization: `Bearer ${apiToken}`, + "Content-Type": "application/vnd.api+json" + }; + + const fetchAllPages = async (): Promise => { + let results: TerraformCloudApiVariable[] = []; + let nextUrl: string | null = url; + + while (nextUrl) { + const res: AxiosResponse> = await request.get< + TerraformCloudApiResponse + >(nextUrl, { + headers + }); + + if (res.data?.data) { + results = results.concat(res.data.data); + } + + nextUrl = res.data?.links?.next ?? null; + } + + return results; + }; + + const allVariableData = await fetchAllPages(); + + const variables: TerraformCloudVariable[] = allVariableData.map((variable) => ({ + id: variable.id, + key: variable.attributes.key, + value: variable.attributes.value || "", + sensitive: variable.attributes.sensitive, + description: variable.attributes.description || "", + category: variable.attributes.category, + source + })); + + return variables; +}; + +const deleteVariable = async ( + secretSync: TTerraformCloudSyncWithCredentials, + variable: TerraformCloudVariable +): Promise => { + const { + destinationConfig, + connection: { + credentials: { apiToken } + } + } = secretSync; + + try { + let url; + + if (destinationConfig.scope === TerraformCloudSyncScope.VariableSet) { + url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/varsets/${destinationConfig.variableSetId}/relationships/vars/${variable.id}`; + } else { + url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${destinationConfig.workspaceId}/vars/${variable.id}`; + } + + await request.delete(url, { + headers: { + Authorization: `Bearer ${apiToken}`, + "Content-Type": "application/vnd.api+json" + } + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: variable.key + }); + } +}; + +const createVariable = async ( + secretSync: TTerraformCloudSyncWithCredentials, + secretMap: TSecretMap, + key: string +): Promise => { + try { + const { + destinationConfig, + connection: { + credentials: { apiToken } + } + } = secretSync; + + let url; + + if (destinationConfig.scope === TerraformCloudSyncScope.VariableSet) { + url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/varsets/${destinationConfig.variableSetId}/relationships/vars`; + } else { + url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${destinationConfig.workspaceId}/vars`; + } + + await request.post( + url, + { + data: { + type: "vars", + attributes: { + key, + value: secretMap[key].value, + description: secretMap[key].comment || "", + category: secretSync.destinationConfig.category, + sensitive: true + } + } + }, + { + headers: { + Authorization: `Bearer ${apiToken}`, + "Content-Type": "application/vnd.api+json" + } + } + ); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } +}; + +const updateVariable = async ( + secretSync: TTerraformCloudSyncWithCredentials, + secretMap: TSecretMap, + variable: TerraformCloudVariable +): Promise => { + try { + const { + destinationConfig, + connection: { + credentials: { apiToken } + } + } = secretSync; + + let url; + + if (destinationConfig.scope === TerraformCloudSyncScope.VariableSet) { + url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/varsets/${destinationConfig.variableSetId}/relationships/vars/${variable.id}`; + } else { + url = `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${destinationConfig.workspaceId}/vars/${variable.id}`; + } + + await request.patch( + url, + { + data: { + type: "vars", + id: variable.id, + attributes: { + value: secretMap[variable.key].value, + description: secretMap[variable.key].comment || "", + category: secretSync.destinationConfig.category + } + } + }, + { + headers: { + Authorization: `Bearer ${apiToken}`, + "Content-Type": "application/vnd.api+json" + } + } + ); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: variable.key + }); + } +}; + +export const TerraformCloudSyncFns = { + syncSecrets: async (secretSync: TTerraformCloudSyncWithCredentials, secretMap: TSecretMap): Promise => { + const terraformCloudVariables = await getTerraformCloudVariables(secretSync); + const terraformCloudVariablesMap = new Map( + terraformCloudVariables.map((v) => [v.key, v]) + ); + + const secretKeys = Object.keys(secretMap); + for (const key of secretKeys) { + const existingVariable = terraformCloudVariablesMap.get(key); + + if (!existingVariable) { + await createVariable(secretSync, secretMap, key); + } else { + await updateVariable(secretSync, secretMap, existingVariable); + } + } + + if (secretSync.syncOptions.disableSecretDeletion) return; + + for (const terraformCloudVariable of terraformCloudVariables) { + if (!Object.prototype.hasOwnProperty.call(secretMap, terraformCloudVariable.key)) { + await deleteVariable(secretSync, terraformCloudVariable); + } + } + }, + + getSecrets: async (secretSync: TTerraformCloudSyncWithCredentials): Promise => { + throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`); + }, + + removeSecrets: async (secretSync: TTerraformCloudSyncWithCredentials, secretMap: TSecretMap): Promise => { + const terraformCloudVariables = await getTerraformCloudVariables(secretSync); + + for (const variable of terraformCloudVariables) { + if (Object.prototype.hasOwnProperty.call(secretMap, variable.key)) { + await deleteVariable(secretSync, variable); + } + } + } +}; diff --git a/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-schemas.ts b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-schemas.ts new file mode 100644 index 000000000..359d7f4c5 --- /dev/null +++ b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-schemas.ts @@ -0,0 +1,77 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; +import { + TerraformCloudSyncCategory, + TerraformCloudSyncScope +} from "@app/services/secret-sync/terraform-cloud/terraform-cloud-sync-enums"; + +const TerraformCloudSyncDestinationConfigSchema = z.discriminatedUnion("scope", [ + z.object({ + scope: z + .literal(TerraformCloudSyncScope.VariableSet) + .describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.scope), + org: z.string().min(1, "Org ID is required").describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.org), + variableSetName: z + .string() + .min(1, "Variable set name is required") + .describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.variableSetName), + variableSetId: z + .string() + .min(1, "Variable set ID is required") + .describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.variableSetId), + category: z.nativeEnum(TerraformCloudSyncCategory).describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.category) + }), + z.object({ + scope: z.literal(TerraformCloudSyncScope.Workspace).describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.scope), + org: z.string().min(1, "Org ID is required").describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.org), + workspaceName: z + .string() + .min(1, "Workspace name is required") + .describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.workspaceName), + workspaceId: z + .string() + .min(1, "Workspace ID is required") + .describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.workspaceId), + category: z.nativeEnum(TerraformCloudSyncCategory).describe(SecretSyncs.DESTINATION_CONFIG.TERRAFORM_CLOUD.category) + }) +]); + +const TerraformCloudSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false }; + +export const TerraformCloudSyncSchema = BaseSecretSyncSchema( + SecretSync.TerraformCloud, + TerraformCloudSyncOptionsConfig +).extend({ + destination: z.literal(SecretSync.TerraformCloud), + destinationConfig: TerraformCloudSyncDestinationConfigSchema +}); + +export const CreateTerraformCloudSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.TerraformCloud, + TerraformCloudSyncOptionsConfig +).extend({ + destinationConfig: TerraformCloudSyncDestinationConfigSchema +}); + +export const UpdateTerraformCloudSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.TerraformCloud, + TerraformCloudSyncOptionsConfig +).extend({ + destinationConfig: TerraformCloudSyncDestinationConfigSchema.optional() +}); + +export const TerraformCloudSyncListItemSchema = z.object({ + name: z.literal("Terraform Cloud"), + connection: z.literal(AppConnection.TerraformCloud), + destination: z.literal(SecretSync.TerraformCloud), + canImportSecrets: z.literal(false) +}); diff --git a/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-types.ts b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-types.ts new file mode 100644 index 000000000..f68db0d51 --- /dev/null +++ b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-types.ts @@ -0,0 +1,77 @@ +import z from "zod"; + +import { TTerraformCloudConnection } from "@app/services/app-connection/terraform-cloud"; + +import { + CreateTerraformCloudSyncSchema, + TerraformCloudSyncListItemSchema, + TerraformCloudSyncSchema +} from "./terraform-cloud-sync-schemas"; + +export type TTerraformCloudSyncListItem = z.infer; + +export type TTerraformCloudSync = z.infer; + +export type TTerraformCloudSyncInput = z.infer; + +export type TTerraformCloudSyncWithCredentials = TTerraformCloudSync & { + connection: TTerraformCloudConnection; +}; + +export type TerraformCloudApiVariable = { + id: string; + type: string; + attributes: { + key: string; + value: string | null; + sensitive: boolean; + category: "terraform" | "env"; + hcl: boolean; + description: string | null; + }; + relationships: { + workspace?: { + data: { + id: string; + type: string; + }; + }; + project?: { + data: { + id: string; + type: string; + }; + }; + }; +}; + +export type TerraformCloudVariable = { + id: string; + key: string; + value: string; + sensitive: boolean; + description: string; + category: "terraform" | "env"; + source: "varset" | "workspace"; +}; + +export type TerraformCloudApiResponse = { + data: T; + included?: unknown[]; + links?: { + self?: string; + first?: string; + prev?: string; + next?: string; + last?: string; + }; + meta?: { + pagination?: { + current_page: number; + prev_page: number | null; + next_page: number | null; + total_pages: number; + total_count: number; + }; + }; +}; diff --git a/backend/src/services/secret-sync/vercel/index.ts b/backend/src/services/secret-sync/vercel/index.ts new file mode 100644 index 000000000..b8379b5d9 --- /dev/null +++ b/backend/src/services/secret-sync/vercel/index.ts @@ -0,0 +1,5 @@ +export * from "./vercel-sync-constants"; +export * from "./vercel-sync-enums"; +export * from "./vercel-sync-fns"; +export * from "./vercel-sync-schemas"; +export * from "./vercel-sync-types"; diff --git a/backend/src/services/secret-sync/vercel/vercel-sync-constants.ts b/backend/src/services/secret-sync/vercel/vercel-sync-constants.ts new file mode 100644 index 000000000..60b3eb00a --- /dev/null +++ b/backend/src/services/secret-sync/vercel/vercel-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const VERCEL_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Vercel", + destination: SecretSync.Vercel, + connection: AppConnection.Vercel, + canImportSecrets: true +}; diff --git a/backend/src/services/secret-sync/vercel/vercel-sync-enums.ts b/backend/src/services/secret-sync/vercel/vercel-sync-enums.ts new file mode 100644 index 000000000..36c46985b --- /dev/null +++ b/backend/src/services/secret-sync/vercel/vercel-sync-enums.ts @@ -0,0 +1,12 @@ +export enum VercelSyncScope { + Application = "application", + Environment = "environment" +} + +export const VercelEnvironmentType = { + Development: "development", + Preview: "preview", + Production: "production" +} as const; + +export type VercelEnvironment = (typeof VercelEnvironmentType)[keyof typeof VercelEnvironmentType]; diff --git a/backend/src/services/secret-sync/vercel/vercel-sync-fns.ts b/backend/src/services/secret-sync/vercel/vercel-sync-fns.ts new file mode 100644 index 000000000..713971283 --- /dev/null +++ b/backend/src/services/secret-sync/vercel/vercel-sync-fns.ts @@ -0,0 +1,313 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { request } from "@app/lib/config/request"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; + +import { VercelEnvironmentType } from "./vercel-sync-enums"; +import { DefaultVercelEnvType, TVercelSyncWithCredentials, VercelApiSecret } from "./vercel-sync-types"; + +function isVercelDefaultEnvType(value: string): value is DefaultVercelEnvType { + return Object.values(VercelEnvironmentType).map(String).includes(value); +} + +const MAX_RETRIES = 5; + +const sleep = async () => + new Promise((resolve) => { + setTimeout(resolve, 60000); + }); + +const getVercelSecretsWithRetries = async ( + secretSync: TVercelSyncWithCredentials, + attempt = 0 +): Promise => { + const { + destinationConfig, + connection: { + credentials: { apiToken } + } + } = secretSync; + + const params: { [key: string]: string } = { + decrypt: "true", + ...(destinationConfig.branch ? { gitBranch: destinationConfig.branch } : {}) + }; + try { + const { data } = await request.get<{ envs: VercelApiSecret[] }>( + `${IntegrationUrls.VERCEL_API_URL}/v9/projects/${destinationConfig.app}/env?teamId=${destinationConfig.teamId}`, + { + params, + headers: { + Authorization: `Bearer ${apiToken}`, + "Accept-Encoding": "application/json" + } + } + ); + return data.envs; + } catch (error) { + if ((error as { response: { status: number } }).response.status === 429 && attempt < MAX_RETRIES) { + await sleep(); + return await getVercelSecretsWithRetries(secretSync, attempt + 1); + } + throw error; + } +}; + +const getDecryptedVercelSecret = async ( + secretSync: TVercelSyncWithCredentials, + secret: VercelApiSecret, + attempt = 0 +): Promise => { + const { + destinationConfig, + connection: { + credentials: { apiToken } + } + } = secretSync; + + const params: { [key: string]: string } = { + decrypt: "true", + ...(destinationConfig.branch ? { gitBranch: destinationConfig.branch } : {}) + }; + + try { + const { data: decryptedSecret } = await request.get( + `${IntegrationUrls.VERCEL_API_URL}/v9/projects/${destinationConfig.app}/env/${secret.id}?teamId=${destinationConfig.teamId}`, + { + params, + headers: { + Authorization: `Bearer ${apiToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + return decryptedSecret as VercelApiSecret; + } catch (error) { + if ((error as { response: { status: number } }).response.status === 429 && attempt < MAX_RETRIES) { + await sleep(); + return await getDecryptedVercelSecret(secretSync, secret, attempt + 1); + } + throw error; + } +}; + +const getVercelSecrets = async (secretSync: TVercelSyncWithCredentials): Promise => { + const { destinationConfig } = secretSync; + + const secrets = await getVercelSecretsWithRetries(secretSync); + + const filteredSecrets = secrets.filter((secret) => { + if (!isVercelDefaultEnvType(destinationConfig.env)) { + if (secret.customEnvironmentIds?.includes(destinationConfig.env)) { + return true; + } + return false; + } + if (secret.target.includes(destinationConfig.env)) { + // If it's preview environment with a branch specified + if ( + destinationConfig.env === VercelEnvironmentType.Preview && + destinationConfig.branch && + secret.gitBranch && + secret.gitBranch !== destinationConfig.branch + ) { + return false; + } + return true; + } + return false; + }); + + // For secrets of type "encrypted", we need to get their decrypted value + const secretsWithValues = await Promise.all( + filteredSecrets.map(async (secret) => { + if (secret.type === "encrypted") { + const decryptedSecret = await getDecryptedVercelSecret(secretSync, secret); + return decryptedSecret; + } + return secret; + }) + ); + + return secretsWithValues; +}; + +const deleteSecret = async ( + secretSync: TVercelSyncWithCredentials, + vercelSecret: VercelApiSecret, + attempt = 0 +): Promise => { + const { + destinationConfig, + connection: { + credentials: { apiToken } + } + } = secretSync; + + try { + await request.delete( + `${IntegrationUrls.VERCEL_API_URL}/v9/projects/${destinationConfig.app}/env/${vercelSecret.id}?teamId=${destinationConfig.teamId}`, + { + headers: { + Authorization: `Bearer ${apiToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } catch (error) { + if ((error as { response: { status: number } }).response.status === 429 && attempt < MAX_RETRIES) { + await sleep(); + return await deleteSecret(secretSync, vercelSecret, attempt + 1); + } + throw new SecretSyncError({ + error, + secretKey: vercelSecret.key + }); + } +}; + +const createSecret = async ( + secretSync: TVercelSyncWithCredentials, + secretMap: TSecretMap, + key: string, + attempt = 0 +): Promise => { + try { + const { + destinationConfig, + connection: { + credentials: { apiToken } + } + } = secretSync; + + await request.post( + `${IntegrationUrls.VERCEL_API_URL}/v10/projects/${destinationConfig.app}/env?teamId=${destinationConfig.teamId}`, + { + key, + value: secretMap[key].value, + type: "encrypted", + target: isVercelDefaultEnvType(destinationConfig.env) ? [destinationConfig.env] : [], + customEnvironmentIds: !isVercelDefaultEnvType(destinationConfig.env) ? [destinationConfig.env] : [], + ...(destinationConfig.env === VercelEnvironmentType.Preview && destinationConfig.branch + ? { gitBranch: destinationConfig.branch } + : {}) + }, + { + headers: { + Authorization: `Bearer ${apiToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } catch (error) { + if ((error as { response: { status: number } }).response.status === 429 && attempt < MAX_RETRIES) { + await sleep(); + return await createSecret(secretSync, secretMap, key, attempt + 1); + } + throw new SecretSyncError({ + error, + secretKey: key + }); + } +}; + +const updateSecret = async ( + secretSync: TVercelSyncWithCredentials, + secretMap: TSecretMap, + vercelSecret: VercelApiSecret, + attempt = 0 +): Promise => { + try { + const { + destinationConfig, + connection: { + credentials: { apiToken } + } + } = secretSync; + + let target = [...vercelSecret.target]; + if (isVercelDefaultEnvType(destinationConfig.env) && !vercelSecret.target.includes(destinationConfig.env)) { + target = [...target, destinationConfig.env]; + } + let customEnvironmentIds = [...(vercelSecret.customEnvironmentIds || [])]; + if ( + !isVercelDefaultEnvType(destinationConfig.env) && + !vercelSecret.customEnvironmentIds?.includes(destinationConfig.env) + ) { + customEnvironmentIds = [...customEnvironmentIds, destinationConfig.env]; + } + + await request.patch( + `${IntegrationUrls.VERCEL_API_URL}/v9/projects/${destinationConfig.app}/env/${vercelSecret.id}?teamId=${destinationConfig.teamId}`, + { + ...(vercelSecret.type !== "sensitive" && { key: vercelSecret.key }), + value: secretMap[vercelSecret.key].value, + type: vercelSecret.type, + target, + customEnvironmentIds, + ...(destinationConfig.env === VercelEnvironmentType.Preview && destinationConfig.branch + ? { gitBranch: destinationConfig.branch } + : {}) + }, + { + headers: { + Authorization: `Bearer ${apiToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } catch (error) { + if ((error as { response: { status: number } }).response.status === 429 && attempt < MAX_RETRIES) { + await sleep(); + return await updateSecret(secretSync, secretMap, vercelSecret, attempt + 1); + } + throw new SecretSyncError({ + error, + secretKey: vercelSecret.key + }); + } +}; + +export const VercelSyncFns = { + syncSecrets: async (secretSync: TVercelSyncWithCredentials, secretMap: TSecretMap) => { + const vercelSecrets = await getVercelSecrets(secretSync); + const vercelSecretsMap = new Map(vercelSecrets.map((s) => [s.key, s])); + + // Create or update secrets + for await (const key of Object.keys(secretMap)) { + const existingSecret = vercelSecretsMap.get(key); + + if (!existingSecret) { + await createSecret(secretSync, secretMap, key); + } else if (existingSecret.value !== secretMap[key].value) { + await updateSecret(secretSync, secretMap, existingSecret); + } + } + + // Delete secrets if disableSecretDeletion is not set + if (secretSync.syncOptions.disableSecretDeletion) return; + + for await (const vercelSecret of vercelSecrets) { + if (!secretMap[vercelSecret.key]) { + await deleteSecret(secretSync, vercelSecret); + } + } + }, + + getSecrets: async (secretSync: TVercelSyncWithCredentials): Promise => { + const vercelSecrets = await getVercelSecrets(secretSync); + return Object.fromEntries(vercelSecrets.map((s) => [s.key, { value: s.value ?? "" }])); + }, + + removeSecrets: async (secretSync: TVercelSyncWithCredentials, secretMap: TSecretMap) => { + const vercelSecrets = await getVercelSecrets(secretSync); + + for await (const vercelSecret of vercelSecrets) { + if (vercelSecret.key in secretMap) { + await deleteSecret(secretSync, vercelSecret); + } + } + } +}; diff --git a/backend/src/services/secret-sync/vercel/vercel-sync-schemas.ts b/backend/src/services/secret-sync/vercel/vercel-sync-schemas.ts new file mode 100644 index 000000000..84d7a6da4 --- /dev/null +++ b/backend/src/services/secret-sync/vercel/vercel-sync-schemas.ts @@ -0,0 +1,49 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +import { VercelEnvironmentType } from "./vercel-sync-enums"; + +const VercelSyncDestinationConfigSchema = z.object({ + app: z.string().min(1, "App ID is required").describe(SecretSyncs.DESTINATION_CONFIG.VERCEL.app), + appName: z.string().min(1, "App Name is required").describe(SecretSyncs.DESTINATION_CONFIG.VERCEL.appName), + env: z.nativeEnum(VercelEnvironmentType).or(z.string()).describe(SecretSyncs.DESTINATION_CONFIG.VERCEL.env), + branch: z.string().optional().describe(SecretSyncs.DESTINATION_CONFIG.VERCEL.branch), + teamId: z.string().describe(SecretSyncs.DESTINATION_CONFIG.VERCEL.teamId) +}); + +const VercelSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +export const VercelSyncSchema = BaseSecretSyncSchema(SecretSync.Vercel, VercelSyncOptionsConfig).extend({ + destination: z.literal(SecretSync.Vercel), + destinationConfig: VercelSyncDestinationConfigSchema +}); + +export const CreateVercelSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.Vercel, + VercelSyncOptionsConfig +).extend({ + destinationConfig: VercelSyncDestinationConfigSchema +}); + +export const UpdateVercelSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.Vercel, + VercelSyncOptionsConfig +).extend({ + destinationConfig: VercelSyncDestinationConfigSchema.optional() +}); + +export const VercelSyncListItemSchema = z.object({ + name: z.literal("Vercel"), + connection: z.literal(AppConnection.Vercel), + destination: z.literal(SecretSync.Vercel), + canImportSecrets: z.literal(true) +}); diff --git a/backend/src/services/secret-sync/vercel/vercel-sync-types.ts b/backend/src/services/secret-sync/vercel/vercel-sync-types.ts new file mode 100644 index 000000000..d6d2b6433 --- /dev/null +++ b/backend/src/services/secret-sync/vercel/vercel-sync-types.ts @@ -0,0 +1,40 @@ +import z from "zod"; + +import { TVercelConnection } from "@app/services/app-connection/vercel"; + +import { VercelEnvironmentType } from "./vercel-sync-enums"; +import { CreateVercelSyncSchema, VercelSyncListItemSchema, VercelSyncSchema } from "./vercel-sync-schemas"; + +export type TVercelSyncListItem = z.infer; + +export type TVercelSync = z.infer; + +export type TVercelSyncInput = z.infer; + +export type TVercelSyncWithCredentials = TVercelSync & { + connection: TVercelConnection; +}; + +export type VercelSecret = { + description: string; + is_secret: boolean; + key: string; + source: "app" | "env"; + value: string; +}; + +export interface VercelApiSecret { + id: string; + key: string; + value: string; + type: string; + target: string[]; + customEnvironmentIds?: string[]; + gitBranch?: string; + createdAt?: number; + updatedAt?: number; + configurationId?: string; + system?: boolean; +} + +export type DefaultVercelEnvType = (typeof VercelEnvironmentType)[keyof typeof VercelEnvironmentType]; diff --git a/backend/src/services/secret-sync/windmill/index.ts b/backend/src/services/secret-sync/windmill/index.ts new file mode 100644 index 000000000..94897da0f --- /dev/null +++ b/backend/src/services/secret-sync/windmill/index.ts @@ -0,0 +1,4 @@ +export * from "./windmill-sync-constants"; +export * from "./windmill-sync-fns"; +export * from "./windmill-sync-schemas"; +export * from "./windmill-sync-types"; diff --git a/backend/src/services/secret-sync/windmill/windmill-sync-constants.ts b/backend/src/services/secret-sync/windmill/windmill-sync-constants.ts new file mode 100644 index 000000000..d52c704b7 --- /dev/null +++ b/backend/src/services/secret-sync/windmill/windmill-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const WINDMILL_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Windmill", + destination: SecretSync.Windmill, + connection: AppConnection.Windmill, + canImportSecrets: true +}; diff --git a/backend/src/services/secret-sync/windmill/windmill-sync-fns.ts b/backend/src/services/secret-sync/windmill/windmill-sync-fns.ts new file mode 100644 index 000000000..2e2c36740 --- /dev/null +++ b/backend/src/services/secret-sync/windmill/windmill-sync-fns.ts @@ -0,0 +1,241 @@ +import { request } from "@app/lib/config/request"; +import { getWindmillInstanceUrl } from "@app/services/app-connection/windmill"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { + TDeleteWindmillVariable, + TPostWindmillVariable, + TWindmillListVariables, + TWindmillListVariablesResponse, + TWindmillSyncWithCredentials, + TWindmillVariable +} from "@app/services/secret-sync/windmill/windmill-sync-types"; + +import { TSecretMap } from "../secret-sync-types"; + +const PAGE_LIMIT = 100; + +const listWindmillVariables = async ({ instanceUrl, workspace, accessToken, path }: TWindmillListVariables) => { + const variables: Record = {}; + + // windmill paginates but doesn't return if there's more pages so we need to check if page size full + let page: number | null = 1; + + while (page) { + // eslint-disable-next-line no-await-in-loop + const { data: variablesPage } = await request.get( + `${instanceUrl}/api/w/${workspace}/variables/list`, + { + headers: { + Authorization: `Bearer ${accessToken}` + }, + params: { + page, + limit: PAGE_LIMIT, + path_start: path + } + } + ); + + for (const variable of variablesPage) { + const variableName = variable.path.replace(path, ""); + + if (variable.is_secret) { + // eslint-disable-next-line no-await-in-loop + const { data: variableValue } = await request.get( + `${instanceUrl}/api/w/${workspace}/variables/get_value/${variable.path}`, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + + variables[variableName] = { + ...variable, + value: variableValue + }; + } else { + variables[variableName] = variable; + } + } + + if (variablesPage.length >= PAGE_LIMIT) { + page += 1; + } else { + page = null; + } + } + + return variables; +}; + +const createWindmillVariable = async ({ + path, + value, + instanceUrl, + accessToken, + workspace, + description +}: TPostWindmillVariable) => + request.post( + `${instanceUrl}/api/w/${workspace}/variables/create`, + { + path, + value, + is_secret: true, + description + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json" + } + } + ); + +const updateWindmillVariable = async ({ + path, + value, + instanceUrl, + accessToken, + workspace, + description +}: TPostWindmillVariable) => + request.post( + `${instanceUrl}/api/w/${workspace}/variables/update/${path}`, + { + value, + is_secret: true, + description + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json" + } + } + ); + +const deleteWindmillVariable = async ({ path, instanceUrl, accessToken, workspace }: TDeleteWindmillVariable) => + request.delete(`${instanceUrl}/api/w/${workspace}/variables/delete/${path}`, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + +export const WindmillSyncFns = { + syncSecrets: async (secretSync: TWindmillSyncWithCredentials, secretMap: TSecretMap) => { + const { + connection, + destinationConfig: { path }, + syncOptions: { disableSecretDeletion } + } = secretSync; + + // url needs to be lowercase + const workspace = secretSync.destinationConfig.workspace.toLowerCase(); + + const instanceUrl = await getWindmillInstanceUrl(connection); + + const { accessToken } = connection.credentials; + + const variables = await listWindmillVariables({ instanceUrl, accessToken, workspace, path }); + + for await (const entry of Object.entries(secretMap)) { + const [key, { value, comment = "" }] = entry; + + try { + const payload = { + instanceUrl, + workspace, + path: path + key, + value, + accessToken, + description: comment + }; + if (key in variables) { + if (variables[key].value !== value || variables[key].description !== comment) + await updateWindmillVariable(payload); + } else { + await createWindmillVariable(payload); + } + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + + if (disableSecretDeletion) return; + + for await (const [key, variable] of Object.entries(variables)) { + if (!(key in secretMap)) { + try { + await deleteWindmillVariable({ + instanceUrl, + workspace, + path: variable.path, + accessToken + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + } + }, + removeSecrets: async (secretSync: TWindmillSyncWithCredentials, secretMap: TSecretMap) => { + const { + connection, + destinationConfig: { path } + } = secretSync; + + // url needs to be lowercase + const workspace = secretSync.destinationConfig.workspace.toLowerCase(); + + const instanceUrl = await getWindmillInstanceUrl(connection); + + const { accessToken } = connection.credentials; + + const variables = await listWindmillVariables({ instanceUrl, accessToken, workspace, path }); + + for await (const [key, variable] of Object.entries(variables)) { + if (key in secretMap) { + try { + await deleteWindmillVariable({ + path: variable.path, + instanceUrl, + workspace, + accessToken + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + } + }, + getSecrets: async (secretSync: TWindmillSyncWithCredentials) => { + const { + connection, + destinationConfig: { path } + } = secretSync; + + // url needs to be lowercase + const workspace = secretSync.destinationConfig.workspace.toLowerCase(); + + const instanceUrl = await getWindmillInstanceUrl(connection); + + const { accessToken } = connection.credentials; + + const variables = await listWindmillVariables({ instanceUrl, accessToken, workspace, path }); + + return Object.fromEntries( + Object.entries(variables).map(([key, variable]) => [key, { value: variable.value ?? "" }]) + ); + } +}; diff --git a/backend/src/services/secret-sync/windmill/windmill-sync-schemas.ts b/backend/src/services/secret-sync/windmill/windmill-sync-schemas.ts new file mode 100644 index 000000000..5740e21c9 --- /dev/null +++ b/backend/src/services/secret-sync/windmill/windmill-sync-schemas.ts @@ -0,0 +1,66 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { CharacterType, characterValidator } from "@app/lib/validator/validate-string"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const pathCharacterValidator = characterValidator([ + CharacterType.AlphaNumeric, + CharacterType.Underscore, + CharacterType.Hyphen +]); + +const WindmillSyncDestinationConfigSchema = z.object({ + workspace: z.string().trim().min(1, "Workspace required").describe(SecretSyncs.DESTINATION_CONFIG.WINDMILL.workspace), + path: z + .string() + .trim() + .min(1, "Path required") + .refine( + (val) => + (val.startsWith("u/") || val.startsWith("f/")) && + val.endsWith("/") && + val.split("/").length >= 3 && + val + .split("/") + .slice(0, -1) // Remove last empty segment from trailing slash + .every((segment) => segment && pathCharacterValidator(segment)), + 'Invalid path - must follow Windmill path format. ex: "f/folder/path/"' + ) + .describe(SecretSyncs.DESTINATION_CONFIG.WINDMILL.path) +}); + +const WindmillSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +export const WindmillSyncSchema = BaseSecretSyncSchema(SecretSync.Windmill, WindmillSyncOptionsConfig).extend({ + destination: z.literal(SecretSync.Windmill), + destinationConfig: WindmillSyncDestinationConfigSchema +}); + +export const CreateWindmillSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.Windmill, + WindmillSyncOptionsConfig +).extend({ + destinationConfig: WindmillSyncDestinationConfigSchema +}); + +export const UpdateWindmillSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.Windmill, + WindmillSyncOptionsConfig +).extend({ + destinationConfig: WindmillSyncDestinationConfigSchema.optional() +}); + +export const WindmillSyncListItemSchema = z.object({ + name: z.literal("Windmill"), + connection: z.literal(AppConnection.Windmill), + destination: z.literal(SecretSync.Windmill), + canImportSecrets: z.literal(true) +}); diff --git a/backend/src/services/secret-sync/windmill/windmill-sync-types.ts b/backend/src/services/secret-sync/windmill/windmill-sync-types.ts new file mode 100644 index 000000000..9837599d7 --- /dev/null +++ b/backend/src/services/secret-sync/windmill/windmill-sync-types.ts @@ -0,0 +1,39 @@ +import { z } from "zod"; + +import { TWindmillConnection } from "@app/services/app-connection/windmill"; + +import { CreateWindmillSyncSchema, WindmillSyncListItemSchema, WindmillSyncSchema } from "./windmill-sync-schemas"; + +export type TWindmillSync = z.infer; + +export type TWindmillSyncInput = z.infer; + +export type TWindmillSyncListItem = z.infer; + +export type TWindmillSyncWithCredentials = TWindmillSync & { + connection: TWindmillConnection; +}; + +export type TWindmillVariable = { + path: string; + value: string; + is_secret: boolean; + is_oauth: boolean; + description: string; +}; + +export type TWindmillListVariablesResponse = TWindmillVariable[]; + +export type TWindmillListVariables = { + accessToken: string; + instanceUrl: string; + path: string; + workspace: string; + description?: string; +}; + +export type TPostWindmillVariable = TWindmillListVariables & { + value: string; +}; + +export type TDeleteWindmillVariable = TWindmillListVariables; diff --git a/backend/src/services/secret-tag/secret-tag-dal.ts b/backend/src/services/secret-tag/secret-tag-dal.ts index 1df64afa2..3b9151557 100644 --- a/backend/src/services/secret-tag/secret-tag-dal.ts +++ b/backend/src/services/secret-tag/secret-tag-dal.ts @@ -47,6 +47,7 @@ export const secretTagDALFactory = (db: TDbClient) => { throw new DatabaseError({ error, name: "Find all by ids" }); } }; + return { ...secretTagOrm, saveTagsToSecret: secretJnTagOrm.insertMany, diff --git a/backend/src/services/secret-tag/secret-tag-service.ts b/backend/src/services/secret-tag/secret-tag-service.ts index 6cae3997a..0a154a4be 100644 --- a/backend/src/services/secret-tag/secret-tag-service.ts +++ b/backend/src/services/secret-tag/secret-tag-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -23,13 +24,14 @@ export type TSecretTagServiceFactory = ReturnType { const createTag = async ({ slug, actor, color, actorId, actorOrgId, actorAuthMethod, projectId }: TCreateTagDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Tags); const existingTag = await secretTagDAL.findOne({ slug, projectId }); @@ -54,13 +56,14 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe if (existingTag && existingTag.id !== tag.id) throw new BadRequestError({ message: "Tag already exist" }); } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - tag.projectId, + projectId: tag.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags); const updatedTag = await secretTagDAL.updateById(tag.id, { color, slug }); @@ -71,13 +74,14 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe const tag = await secretTagDAL.findById(id); if (!tag) throw new NotFoundError({ message: `Tag with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - tag.projectId, + projectId: tag.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags); const deletedTag = await secretTagDAL.deleteById(tag.id); @@ -88,13 +92,14 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe const tag = await secretTagDAL.findById(id); if (!tag) throw new NotFoundError({ message: `Tag with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - tag.projectId, + projectId: tag.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); return { ...tag, name: tag.slug }; @@ -104,26 +109,28 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe const tag = await secretTagDAL.findOne({ projectId, slug }); if (!tag) throw new NotFoundError({ message: `Tag with slug '${slug}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - tag.projectId, + projectId: tag.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); return { ...tag, name: tag.slug }; }; const getProjectTags = async ({ actor, actorId, actorOrgId, actorAuthMethod, projectId }: TListProjectTagsDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); const tags = await secretTagDAL.find({ projectId }, { sort: [["createdAt", "asc"]] }); diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts index 9ca3e87d3..05fc7cd35 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts @@ -1,8 +1,12 @@ +import { MongoAbility } from "@casl/ability"; import { Knex } from "knex"; import { validate as uuidValidate } from "uuid"; import { TDbClient } from "@app/db"; -import { SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecretsV2Update } from "@app/db/schemas"; +import { ProjectType, SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecretsV2Update } from "@app/db/schemas"; +import { TKeyStoreFactory } from "@app/keystore/keystore"; +import { getConfig } from "@app/lib/config/env"; +import { generateCacheKeyFromData } from "@app/lib/crypto/cache"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; import { buildFindFilter, @@ -14,13 +18,48 @@ import { } from "@app/lib/knex"; import { OrderByDirection } from "@app/lib/types"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; -import { TFindSecretsByFolderIdsFilter } from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; +import type { + TFindSecretsByFolderIdsFilter, + TGetSecretsDTO +} from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; + +export const SecretServiceCacheKeys = { + get productKey() { + const { INFISICAL_PLATFORM_VERSION } = getConfig(); + return `${ProjectType.SecretManager}:${INFISICAL_PLATFORM_VERSION || 0}`; + }, + getSecretDalVersion: (projectId: string) => { + return `${SecretServiceCacheKeys.productKey}:${projectId}:${TableName.SecretV2}-dal-version`; + }, + getSecretsOfServiceLayer: ( + projectId: string, + version: number, + dto: TGetSecretsDTO & { permissionRules: MongoAbility["rules"] } + ) => { + return `${SecretServiceCacheKeys.productKey}:${projectId}:${ + TableName.SecretV2 + }-dal:v${version}:get-secrets-service-layer:${dto.actorId}-${generateCacheKeyFromData(dto)}`; + } +}; export type TSecretV2BridgeDALFactory = ReturnType; +interface TSecretV2DalArg { + db: TDbClient; + keyStore: TKeyStoreFactory; +} -export const secretV2BridgeDALFactory = (db: TDbClient) => { +export const SECRET_DAL_TTL = 5 * 60; +export const SECRET_DAL_VERSION_TTL = 15 * 60; +export const MAX_SECRET_CACHE_BYTES = 25 * 1024 * 1024; +export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { const secretOrm = ormify(db, TableName.SecretV2); + const invalidateSecretCacheByProjectId = async (projectId: string) => { + const secretDalVersionKey = SecretServiceCacheKeys.getSecretDalVersion(projectId); + await keyStore.incrementBy(secretDalVersionKey, 1); + await keyStore.setExpiry(secretDalVersionKey, SECRET_DAL_VERSION_TTL); + }; + const findOne = async (filter: Partial, tx?: Knex) => { try { const docs = await (tx || db)(TableName.SecretV2) @@ -35,15 +74,25 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id` ) + .leftJoin( + TableName.SecretRotationV2SecretMapping, + `${TableName.SecretV2}.id`, + `${TableName.SecretRotationV2SecretMapping}.secretId` + ) .select(selectAllTableCols(TableName.SecretV2)) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) - .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); - + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) + .select(db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping)); const data = sqlNestRelationships({ data: docs, key: "id", - parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }), + parentMapper: (el) => ({ + _id: el.id, + ...SecretsV2Schema.parse(el), + isRotatedSecret: Boolean(el.rotationId), + rotationId: el.rotationId + }), childrenMapper: [ { key: "tagId", @@ -63,7 +112,8 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { } }; - const find = async (filter: TFindFilter, { offset, limit, sort, tx }: TFindOpt = {}) => { + const find = async (filter: TFindFilter, opts: TFindOpt = {}) => { + const { offset, limit, sort, tx } = opts; try { const query = (tx || db)(TableName.SecretV2) // eslint-disable-next-line @typescript-eslint/no-misused-promises @@ -78,10 +128,22 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id` ) + .leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`) + .leftJoin( + TableName.SecretRotationV2SecretMapping, + `${TableName.SecretV2}.id`, + `${TableName.SecretRotationV2SecretMapping}.secretId` + ) + .select( + db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue") + ) .select(selectAllTableCols(TableName.SecretV2)) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) - .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) + .select(db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping)); if (limit) void query.limit(limit); if (offset) void query.offset(offset); if (sort) { @@ -92,7 +154,12 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { const data = sqlNestRelationships({ data: docs, key: "id", - parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }), + parentMapper: (el) => ({ + _id: el.id, + ...SecretsV2Schema.parse(el), + rotationId: el.rotationId, + isRotatedSecret: Boolean(el.rotationId) + }), childrenMapper: [ { key: "tagId", @@ -103,9 +170,19 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { slug, name: slug }) + }, + { + key: "metadataId", + label: "secretMetadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) } ] }); + return data; } catch (error) { throw new DatabaseError({ error, name: `${TableName.SecretV2}: Find` }); @@ -210,9 +287,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { } }; - const findByFolderId = async (folderId: string, userId?: string, tx?: Knex) => { + const findByFolderId = async (dto: { folderId: string; userId?: string; tx?: Knex }) => { try { - // check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo) + const { folderId, tx } = dto; + let { userId } = dto; + // check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo if (userId && !uuidValidate(userId)) { // eslint-disable-next-line userId = undefined; @@ -221,7 +300,9 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { const secs = await (tx || db.replicaNode())(TableName.SecretV2) .where({ folderId }) .where((bd) => { - void bd.whereNull("userId").orWhere({ userId: userId || null }); + void bd + .whereNull(`${TableName.SecretV2}.userId`) + .orWhere({ [`${TableName.SecretV2}.userId` as "userId"]: userId || null }); }) .leftJoin( TableName.SecretV2JnTag, @@ -233,10 +314,16 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id` ) + .leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`) .select(selectAllTableCols(TableName.SecretV2)) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) + .select( + db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue") + ) .orderBy("id", "asc"); const data = sqlNestRelationships({ @@ -253,6 +340,15 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { slug, name: slug }) + }, + { + key: "metadataId", + label: "secretMetadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) } ] }); @@ -300,6 +396,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { } const query = (tx || db.replicaNode())(TableName.SecretV2) + .leftJoin( + TableName.SecretRotationV2SecretMapping, + `${TableName.SecretV2}.id`, + `${TableName.SecretRotationV2SecretMapping}.secretId` + ) .whereIn("folderId", folderIds) .where((bd) => { if (filters?.search) { @@ -336,12 +437,14 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { } }; - const findByFolderIds = async ( - folderIds: string[], - userId?: string, - tx?: Knex, - filters?: TFindSecretsByFolderIdsFilter - ) => { + const findByFolderIds = async (dto: { + folderIds: string[]; + userId?: string; + tx?: Knex; + filters?: TFindSecretsByFolderIdsFilter; + }) => { + const { folderIds, tx, filters } = dto; + let { userId } = dto; try { // check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo) if (userId && !uuidValidate(userId)) { @@ -361,9 +464,15 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { void bd.whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`); } } + + if (filters?.keys) { + void bd.whereIn(`${TableName.SecretV2}.key`, filters.keys); + } }) .where((bd) => { - void bd.whereNull(`${TableName.SecretV2}.userId`).orWhere({ userId: userId || null }); + void bd + .whereNull(`${TableName.SecretV2}.userId`) + .orWhere({ [`${TableName.SecretV2}.userId` as "userId"]: userId || null }); }) .leftJoin( TableName.SecretV2JnTag, @@ -375,13 +484,43 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id` ) + .leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`) + .leftJoin( + TableName.SecretRotationV2SecretMapping, + `${TableName.SecretV2}.id`, + `${TableName.SecretRotationV2SecretMapping}.secretId` + ) + .where((qb) => { + if (filters?.metadataFilter && filters.metadataFilter.length > 0) { + filters.metadataFilter.forEach((meta) => { + void qb.whereExists((subQuery) => { + void subQuery + .select("secretId") + .from(TableName.ResourceMetadata) + .whereRaw(`"${TableName.ResourceMetadata}"."secretId" = "${TableName.SecretV2}"."id"`) + .where(`${TableName.ResourceMetadata}.key`, meta.key) + .where(`${TableName.ResourceMetadata}.value`, meta.value); + }); + }); + } + }) .select( selectAllTableCols(TableName.SecretV2), - db.raw(`DENSE_RANK() OVER (ORDER BY "key" ${filters?.orderDirection ?? OrderByDirection.ASC}) as rank`) + db.raw( + `DENSE_RANK() OVER (ORDER BY "${TableName.SecretV2}".key ${ + filters?.orderDirection ?? OrderByDirection.ASC + }) as rank` + ) ) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) + .select( + db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue") + ) + .select(db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping)) .where((bd) => { const slugs = filters?.tagSlugs?.filter(Boolean); if (slugs && slugs.length > 0) { @@ -410,7 +549,12 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { const data = sqlNestRelationships({ data: secs, key: "id", - parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }), + parentMapper: (el) => ({ + _id: el.id, + ...SecretsV2Schema.parse(el), + rotationId: el.rotationId, + isRotatedSecret: Boolean(el.rotationId) + }), childrenMapper: [ { key: "tagId", @@ -421,9 +565,19 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { slug, name: slug }) + }, + { + key: "metadataId", + label: "secretMetadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) } ] }); + return data; } catch (error) { throw new DatabaseError({ error, name: "get all secret" }); @@ -439,6 +593,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { try { const secrets = await (tx || db.replicaNode())(TableName.SecretV2) .where({ folderId }) + .where((bd) => { query.forEach((el) => { if (el.type === SecretType.Personal && !el.userId) { @@ -450,10 +605,20 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { userId: el.type === SecretType.Personal ? el.userId : null }); }); - }); - return secrets; + }) + .leftJoin( + TableName.SecretRotationV2SecretMapping, + `${TableName.SecretV2}.id`, + `${TableName.SecretRotationV2SecretMapping}.secretId` + ) + .select(selectAllTableCols(TableName.SecretV2)) + .select(db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping)); + return secrets.map((secret) => ({ + ...secret, + isRotatedSecret: Boolean(secret.rotationId) + })); } catch (error) { - throw new DatabaseError({ error, name: "find by blind indexes" }); + throw new DatabaseError({ error, name: "find by secret keys" }); } }; @@ -541,14 +706,25 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id` ) + + .leftJoin(TableName.SecretFolder, `${TableName.SecretV2}.folderId`, `${TableName.SecretFolder}.id`) + .leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) + .leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`) .select(selectAllTableCols(TableName.SecretV2)) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) - .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) + .select( + db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue") + ) + .select(db.ref("projectId").withSchema(TableName.Environment).as("projectId")); + const docs = sqlNestRelationships({ data: rawDocs, key: "id", - parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }), + parentMapper: (el) => ({ _id: el.id, projectId: el.projectId, ...SecretsV2Schema.parse(el) }), childrenMapper: [ { key: "tagId", @@ -559,6 +735,15 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { slug, name: slug }) + }, + { + key: "metadataId", + label: "secretMetadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) } ] }); @@ -584,6 +769,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { findAllProjectSecretValues, countByFolderIds, findOne, - find + find, + invalidateSecretCacheByProjectId }; }; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts index 28fd1c5bb..f42deb8ff 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts @@ -1,18 +1,22 @@ import path from "node:path"; +import RE2 from "re2"; + import { TableName, TSecretFolders, TSecretsV2 } from "@app/db/schemas"; import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; +import { ActorType } from "../auth/auth-type"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; +import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; +import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal"; import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types"; -const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/g; -// akhilmhdh: JS regex with global save state in .test -const INTERPOLATION_SYNTAX_REG_NON_GLOBAL = /\${([^}]+)}/; +const INTERPOLATION_PATTERN_STRING = String.raw`\${([a-zA-Z0-9-_.]+)}`; +const INTERPOLATION_TEST_REGEX = new RE2(INTERPOLATION_PATTERN_STRING); export const shouldUseSecretV2Bridge = (version: number) => version === 3; @@ -33,7 +37,14 @@ export const shouldUseSecretV2Bridge = (version: number) => version === 3; * // ] */ export const getAllSecretReferences = (maybeSecretReference: string) => { - const references = Array.from(maybeSecretReference.matchAll(INTERPOLATION_SYNTAX_REG), (m) => m[1]); + const references = []; + let match; + + const regex = new RE2(INTERPOLATION_PATTERN_STRING, "g"); + // eslint-disable-next-line no-cond-assign + while ((match = regex.exec(maybeSecretReference)) !== null) { + references.push(match[1]); + } const nestedReferences = references .filter((el) => el.includes(".")) @@ -54,11 +65,14 @@ export const getAllSecretReferences = (maybeSecretReference: string) => { export const fnSecretBulkInsert = async ({ // TODO: Pick types here folderId, + orgId, inputSecrets, secretDAL, secretVersionDAL, + resourceMetadataDAL, secretTagDAL, secretVersionTagDAL, + actor, tx }: TFnSecretBulkInsert) => { const sanitizedInputSecrets = inputSecrets.map( @@ -87,10 +101,15 @@ export const fnSecretBulkInsert = async ({ }) ); + const userActorId = actor && actor.type === ActorType.USER ? actor.actorId : undefined; + const identityActorId = actor && actor.type === ActorType.IDENTITY ? actor.actorId : undefined; + const actorType = actor?.type || ActorType.PLATFORM; + const newSecrets = await secretDAL.insertMany( sanitizedInputSecrets.map((el) => ({ ...el, folderId })), tx ); + const newSecretGroupedByKeyName = groupBy(newSecrets, (item) => item.key); const newSecretTags = inputSecrets.flatMap(({ tagIds: secretTags = [], key }) => secretTags.map((tag) => ({ @@ -98,14 +117,19 @@ export const fnSecretBulkInsert = async ({ [`${TableName.SecretV2}Id` as const]: newSecretGroupedByKeyName[key][0].id })) ); + const secretVersions = await secretVersionDAL.insertMany( sanitizedInputSecrets.map((el) => ({ ...el, folderId, + userActorId, + identityActorId, + actorType, secretId: newSecretGroupedByKeyName[el.key][0].id })), tx ); + await secretDAL.upsertSecretReferences( inputSecrets.map(({ references = [], key }) => ({ secretId: newSecretGroupedByKeyName[key][0].id, @@ -113,28 +137,62 @@ export const fnSecretBulkInsert = async ({ })), tx ); + + await resourceMetadataDAL.insertMany( + inputSecrets.flatMap(({ key: secretKey, secretMetadata }) => { + if (secretMetadata) { + return secretMetadata.map(({ key, value }) => ({ + key, + value, + secretId: newSecretGroupedByKeyName[secretKey][0].id, + orgId + })); + } + return []; + }), + tx + ); + if (newSecretTags.length) { const secTags = await secretTagDAL.saveTagsToSecretV2(newSecretTags, tx); const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId); + const newSecretVersionTags = secTags.flatMap(({ secrets_v2Id, secret_tagsId }) => ({ [`${TableName.SecretVersionV2}Id` as const]: secVersionsGroupBySecId[secrets_v2Id][0].id, [`${TableName.SecretTag}Id` as const]: secret_tagsId })); + await secretVersionTagDAL.insertMany(newSecretVersionTags, tx); } - return newSecrets.map((secret) => ({ ...secret, _id: secret.id })); + const secretsWithTags = await secretDAL.find( + { + $in: { + [`${TableName.SecretV2}.id` as "id"]: newSecrets.map((s) => s.id) + } + }, + { tx } + ); + + return secretsWithTags.map((secret) => ({ ...secret, _id: secret.id })); }; export const fnSecretBulkUpdate = async ({ tx, inputSecrets, folderId, + orgId, secretDAL, secretVersionDAL, secretTagDAL, - secretVersionTagDAL + secretVersionTagDAL, + resourceMetadataDAL, + actor }: TFnSecretBulkUpdate) => { + const userActorId = actor && actor?.type === ActorType.USER ? actor?.actorId : undefined; + const identityActorId = actor && actor?.type === ActorType.IDENTITY ? actor?.actorId : undefined; + const actorType = actor?.type || ActorType.PLATFORM; + const sanitizedInputSecrets = inputSecrets.map( ({ filter, @@ -192,7 +250,10 @@ export const fnSecretBulkUpdate = async ({ encryptedValue, reminderRepeatDays, folderId, - secretId + secretId, + userActorId, + identityActorId, + actorType }) ), tx @@ -231,7 +292,43 @@ export const fnSecretBulkUpdate = async ({ } } - return newSecrets.map((secret) => ({ ...secret, _id: secret.id })); + const inputSecretIdsWithMetadata = inputSecrets + .filter((sec) => Boolean(sec.data.secretMetadata)) + .map((sec) => sec.filter.id); + + await resourceMetadataDAL.delete( + { + $in: { + secretId: inputSecretIdsWithMetadata + } + }, + tx + ); + + await resourceMetadataDAL.insertMany( + inputSecrets.flatMap(({ filter: { id }, data: { secretMetadata } }) => { + if (secretMetadata) { + return secretMetadata.map(({ key, value }) => ({ + key, + value, + secretId: id, + orgId + })); + } + return []; + }), + tx + ); + + const secretsWithTags = await secretDAL.find( + { + $in: { + [`${TableName.SecretV2}.id` as "id"]: newSecrets.map((s) => s.id) + } + }, + { tx } + ); + return secretsWithTags.map((secret) => ({ ...secret, _id: secret.id })); }; export const fnSecretBulkDelete = async ({ @@ -267,7 +364,7 @@ export const fnSecretBulkDelete = async ({ interface FolderMap { [parentId: string]: TSecretFolders[]; } -const buildHierarchy = (folders: TSecretFolders[]): FolderMap => { +export const buildHierarchy = (folders: TSecretFolders[]): FolderMap => { const map: FolderMap = {}; map.null = []; // Initialize mapping for root directory @@ -282,7 +379,7 @@ const buildHierarchy = (folders: TSecretFolders[]): FolderMap => { return map; }; -const generatePaths = ( +export const generatePaths = ( map: FolderMap, parentId: string = "null", basePath: string = "", @@ -365,9 +462,8 @@ export const recursivelyGetSecretPaths = async ({ folderId: p.folderId })); - const pathsInCurrentDirectory = paths.filter((folder) => - folder.path.startsWith(currentPath === "/" ? "" : currentPath) - ); + // path relative will start with ../ if its outside directory + const pathsInCurrentDirectory = paths.filter((folder) => !path.relative(currentPath, folder.path).startsWith("..")); return pathsInCurrentDirectory; }; @@ -375,7 +471,7 @@ export const recursivelyGetSecretPaths = async ({ const formatMultiValueEnv = (val?: string) => { if (!val) return ""; if (!val.match("\n")) return val; - return `"${val.replace(/\n/g, "\\n")}"`; + return `"${val.replaceAll("\n", "\\n")}"`; }; type TSecretReferenceTraceNode = { @@ -413,7 +509,7 @@ export const expandSecretReferencesFactory = ({ const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) return { value: "", tags: [] }; - const secrets = await secretDAL.findByFolderId(folder.id); + const secrets = await secretDAL.findByFolderId({ folderId: folder.id }); const decryptedSecret = secrets.reduce>((prev, secret) => { // eslint-disable-next-line no-param-reassign @@ -443,9 +539,17 @@ export const expandSecretReferencesFactory = ({ // eslint-disable-next-line no-continue if (depth > MAX_SECRET_REFERENCE_DEPTH) continue; - const refs = value?.match(INTERPOLATION_SYNTAX_REG); - if (refs) { + const matchRegex = new RE2(INTERPOLATION_PATTERN_STRING, "g"); + const refs = []; + let match; + + // eslint-disable-next-line no-cond-assign + while ((match = matchRegex.exec(value || "")) !== null) { + refs.push(match[0]); + } + + if (refs.length > 0) { for (const interpolationSyntax of refs) { const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1); const entities = interpolationKey.trim().split("."); @@ -465,7 +569,7 @@ export const expandSecretReferencesFactory = ({ const referredValue = await fetchSecret(environment, secretPath, secretKey); if (!canExpandValue(environment, secretPath, secretKey, referredValue.tags)) throw new ForbiddenRequestError({ - message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to.` + message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to read value on.` }); const cacheKey = getCacheUniqueKey(environment, secretPath); @@ -484,7 +588,7 @@ export const expandSecretReferencesFactory = ({ const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey); if (!canExpandValue(secretReferenceEnvironment, secretReferencePath, secretReferenceKey, referedValue.tags)) throw new ForbiddenRequestError({ - message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to.` + message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to read value on.` }); const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath); @@ -504,7 +608,7 @@ export const expandSecretReferencesFactory = ({ trace }; - const shouldExpandMore = INTERPOLATION_SYNTAX_REG_NON_GLOBAL.test(referencedSecretValue); + const shouldExpandMore = INTERPOLATION_TEST_REGEX.test(referencedSecretValue); if (dto.shouldStackTrace) { const stackTraceNode = { ...node, children: [], key: referencedSecretKey, trace: null }; trace?.children.push(stackTraceNode); @@ -518,7 +622,10 @@ export const expandSecretReferencesFactory = ({ } if (referencedSecretValue) { - expandedValue = expandedValue.replaceAll(interpolationSyntax, referencedSecretValue); + expandedValue = expandedValue.replaceAll( + interpolationSyntax, + () => referencedSecretValue // prevents special characters from triggering replacement patterns + ); } } } @@ -535,7 +642,7 @@ export const expandSecretReferencesFactory = ({ }) => { if (!inputSecret.value) return inputSecret.value; - const shouldExpand = Boolean(inputSecret.value?.match(INTERPOLATION_SYNTAX_REG)); + const shouldExpand = INTERPOLATION_TEST_REGEX.test(inputSecret.value); if (!shouldExpand) return inputSecret.value; const { expandedValue } = await recursivelyExpandSecret(inputSecret); @@ -562,30 +669,59 @@ export const reshapeBridgeSecret = ( secret: Omit & { value: string; comment: string; + userActorName?: string | null; + identityActorName?: string | null; + userActorId?: string | null; + identityActorId?: string | null; + membershipId?: string | null; + actorType?: string | null; tags?: { id: string; slug: string; color?: string | null; name: string; }[]; - } + secretMetadata?: ResourceMetadataDTO; + isRotatedSecret?: boolean; + rotationId?: string; + }, + secretValueHidden: boolean ) => ({ secretKey: secret.key, secretPath, workspace: workspaceId, environment, - secretValue: secret.value || "", secretComment: secret.comment || "", version: secret.version, type: secret.type, _id: secret.id, id: secret.id, user: secret.userId, + actor: secret.actorType + ? { + actorType: secret.actorType, + actorId: secret.userActorId || secret.identityActorId, + name: secret.identityActorName || secret.userActorName, + membershipId: secret.membershipId + } + : undefined, tags: secret.tags, skipMultilineEncoding: secret.skipMultilineEncoding, secretReminderRepeatDays: secret.reminderRepeatDays, secretReminderNote: secret.reminderNote, metadata: secret.metadata, + secretMetadata: secret.secretMetadata, createdAt: secret.createdAt, - updatedAt: secret.updatedAt + updatedAt: secret.updatedAt, + isRotatedSecret: secret.isRotatedSecret, + rotationId: secret.rotationId, + ...(secretValueHidden + ? { + secretValue: INFISICAL_SECRET_VALUE_HIDDEN_MASK, + secretValueHidden: true + } + : { + secretValue: secret.value || "", + secretValueHidden: false + }) }); diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 75b7fcca9..ca815c6e1 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -1,13 +1,32 @@ -import { ForbiddenError, PureAbility, subject } from "@casl/ability"; +import { ForbiddenError, MongoAbility, subject } from "@casl/ability"; +import { Knex } from "knex"; import { z } from "zod"; -import { ProjectMembershipRole, SecretsV2Schema, SecretType, TableName } from "@app/db/schemas"; +import { + ActionProjectType, + ProjectMembershipRole, + SecretsV2Schema, + SecretType, + TableName, + TSecretsV2 +} from "@app/db/schemas"; +import { + hasSecretReadValueOrDescribePermission, + throwIfMissingSecretReadValueOrDescribePermission +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSet, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; +import { TKeyStoreFactory } from "@app/keystore/keystore"; +import { DatabaseErrorCode } from "@app/lib/error-codes"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { diff, groupBy } from "@app/lib/fn"; import { setKnexStringValue } from "@app/lib/knex"; @@ -18,28 +37,39 @@ import { ActorType } from "../auth/auth-type"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; +import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; import { TSecretQueueFactory } from "../secret/secret-queue"; +import { TGetASecretByIdDTO } from "../secret/secret-types"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns"; import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal"; -import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal"; import { + MAX_SECRET_CACHE_BYTES, + SECRET_DAL_TTL, + SecretServiceCacheKeys, + TSecretV2BridgeDALFactory +} from "./secret-v2-bridge-dal"; +import { + buildHierarchy, expandSecretReferencesFactory, fnSecretBulkDelete, fnSecretBulkInsert, fnSecretBulkUpdate, + generatePaths, getAllSecretReferences, recursivelyGetSecretPaths, reshapeBridgeSecret } from "./secret-v2-bridge-fns"; import { SecretOperations, + SecretUpdateMode, TBackFillSecretReferencesDTO, TCreateManySecretDTO, TCreateSecretDTO, TDeleteManySecretDTO, TDeleteSecretDTO, + TGetAccessibleSecretsDTO, TGetASecretDTO, TGetSecretReferencesTreeDTO, TGetSecretsDTO, @@ -63,7 +93,13 @@ type TSecretV2BridgeServiceFactoryDep = { projectEnvDAL: Pick; folderDAL: Pick< TSecretFolderDALFactory, - "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findBySecretPathMultiEnv" + | "findBySecretPath" + | "updateById" + | "findById" + | "findByManySecretPath" + | "find" + | "findBySecretPathMultiEnv" + | "findSecretPathByFolderIds" >; secretImportDAL: Pick; secretQueueService: Pick; @@ -74,6 +110,8 @@ type TSecretV2BridgeServiceFactoryDep = { "insertV2Bridge" | "insertApprovalSecretV2Tags" >; snapshotService: Pick; + resourceMetadataDAL: Pick; + keyStore: Pick; }; export type TSecretV2BridgeServiceFactory = ReturnType; @@ -95,17 +133,21 @@ export const secretV2BridgeServiceFactory = ({ secretApprovalPolicyService, secretApprovalRequestDAL, secretApprovalRequestSecretDAL, - kmsService + kmsService, + resourceMetadataDAL, + keyStore }: TSecretV2BridgeServiceFactoryDep) => { const $validateSecretReferences = async ( projectId: string, - permission: PureAbility, - references: ReturnType["nestedReferences"] + permission: MongoAbility, + references: ReturnType["nestedReferences"], + tx?: Knex ) => { if (!references.length) return; const uniqueReferenceEnvironmentSlugs = Array.from(new Set(references.map((el) => el.environment))); - const referencesEnvironments = await projectEnvDAL.findBySlugs(projectId, uniqueReferenceEnvironmentSlugs); + const referencesEnvironments = await projectEnvDAL.findBySlugs(projectId, uniqueReferenceEnvironmentSlugs, tx); + if (referencesEnvironments.length !== uniqueReferenceEnvironmentSlugs.length) throw new BadRequestError({ message: `Referenced environment not found. Missing ${diff( @@ -119,40 +161,50 @@ export const secretV2BridgeServiceFactory = ({ references.map((el) => ({ secretPath: el.secretPath, envId: referencesEnvironmentGroupBySlug[el.environment][0].id - })) + })), + tx ); + const referencesFolderGroupByPath = groupBy(referredFolders.filter(Boolean), (i) => `${i?.envId}-${i?.path}`); - const referredSecrets = await secretDAL.find({ - $complex: { - operator: "or", - value: references.map((el) => { - const folderId = - referencesFolderGroupByPath[`${referencesEnvironmentGroupBySlug[el.environment][0].id}-${el.secretPath}`][0] - ?.id; - if (!folderId) throw new BadRequestError({ message: `Referenced path ${el.secretPath} doesn't exist` }); + const referredSecrets = await secretDAL.find( + { + $complex: { + operator: "or", + value: references.map((el) => { + const folderId = + referencesFolderGroupByPath[ + `${referencesEnvironmentGroupBySlug[el.environment][0].id}-${el.secretPath}` + ][0]?.id; + if (!folderId) throw new BadRequestError({ message: `Referenced path ${el.secretPath} doesn't exist` }); - return { - operator: "and", - value: [ - { - operator: "eq", - field: "folderId", - value: folderId - }, - { - operator: "eq", - field: "key", - value: el.secretKey - } - ] - }; - }) - } - }); + return { + operator: "and", + value: [ + { + operator: "eq", + field: "folderId", + value: folderId + }, + { + operator: "eq", + field: `${TableName.SecretV2}.key` as "key", + value: el.secretKey + } + ] + }; + }) + } + }, + { tx } + ); - if (referredSecrets.length !== references.length) + if ( + referredSecrets.length !== + new Set(references.map(({ secretKey, secretPath, environment }) => `${secretKey}.${secretPath}.${environment}`)) + .size // only count unique references + ) throw new BadRequestError({ - message: `Referenced secret not found. Found only ${diff( + message: `Referenced secret(s) not found: ${diff( references.map((el) => el.secretKey), referredSecrets.map((el) => el.key) ).join(",")}` @@ -160,15 +212,12 @@ export const secretV2BridgeServiceFactory = ({ const referredSecretsGroupBySecretKey = groupBy(referredSecrets, (i) => i.key); references.forEach((el) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: el.environment, - secretPath: el.secretPath, - secretName: el.secretKey, - tags: referredSecretsGroupBySecretKey[el.secretKey][0]?.tags?.map((i) => i.slug) - }) - ); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment: el.environment, + secretPath: el.secretPath, + secretName: el.secretKey, + secretTags: referredSecretsGroupBySecretKey[el.secretKey][0]?.tags?.map((i) => i.slug) + }); }); return referredSecrets; @@ -182,15 +231,17 @@ export const secretV2BridgeServiceFactory = ({ actorAuthMethod, projectId, secretPath, + secretMetadata, ...inputSecret }: TCreateSecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -228,7 +279,7 @@ export const secretV2BridgeServiceFactory = ({ const { secretName, type, ...inputSecretData } = inputSecret; ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -241,15 +292,17 @@ export const secretV2BridgeServiceFactory = ({ const allSecretReferences = nestedReferences.concat( localReferences.map((el) => ({ secretKey: el, secretPath, environment })) ); + await $validateSecretReferences(projectId, permission, allSecretReferences); const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); - const secret = await secretDAL.transaction((tx) => - fnSecretBulkInsert({ + const secret = await secretDAL.transaction(async (tx) => { + const [createdSecret] = await fnSecretBulkInsert({ folderId, + orgId: actorOrgId, inputSecrets: [ { version: 1, @@ -267,21 +320,31 @@ export const secretV2BridgeServiceFactory = ({ key: secretName, userId: inputSecret.type === SecretType.Personal ? actorId : null, tagIds: inputSecret.tagIds, - references: nestedReferences + references: nestedReferences, + secretMetadata } ], + resourceMetadataDAL, secretDAL, secretVersionDAL, secretTagDAL, secretVersionTagDAL, + actor: { + type: actor, + actorId + }, tx - }) - ); + }); + return createdSecret; + }); + + await secretDAL.invalidateSecretCacheByProjectId(projectId); if (inputSecret.type === SecretType.Shared) { await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ secretPath, + orgId: actorOrgId, actorId, actor, projectId, @@ -289,11 +352,17 @@ export const secretV2BridgeServiceFactory = ({ }); } - return reshapeBridgeSecret(projectId, environment, secretPath, { - ...secret[0], - value: inputSecret.secretValue, - comment: inputSecret.secretComment || "" - }); + return reshapeBridgeSecret( + projectId, + environment, + secretPath, + { + ...secret, + value: inputSecret.secretValue, + comment: inputSecret.secretComment || "" + }, + false + ); }; const updateSecret = async ({ @@ -304,15 +373,17 @@ export const secretV2BridgeServiceFactory = ({ actorAuthMethod, projectId, secretPath, + secretMetadata, ...inputSecret }: TUpdateSecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); if (inputSecret.newSecretName === "") { throw new BadRequestError({ message: "New secret name cannot be empty" }); @@ -355,12 +426,14 @@ export const secretV2BridgeServiceFactory = ({ }); if (!sharedSecretToModify) throw new NotFoundError({ message: `Secret with name ${inputSecret.secretName} not found` }); + if (sharedSecretToModify.isRotatedSecret && (inputSecret.newSecretName || inputSecret.secretValue)) + throw new BadRequestError({ message: "Cannot update rotated secret name or value" }); secretId = sharedSecretToModify.id; secret = sharedSecretToModify; } ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -371,18 +444,22 @@ export const secretV2BridgeServiceFactory = ({ // validate tags // fetch all tags and if not same count throw error meaning one was invalid tags - const tags = inputSecret.tagIds ? await secretTagDAL.find({ projectId, $in: { id: inputSecret.tagIds } }) : []; - if ((inputSecret.tagIds || []).length !== tags.length) - throw new NotFoundError({ message: `Tag not found. Found ${tags.map((el) => el.slug).join(",")}` }); + const newTags = inputSecret.tagIds ? await secretTagDAL.find({ projectId, $in: { id: inputSecret.tagIds } }) : []; + if ((inputSecret.tagIds || []).length !== newTags.length) + throw new NotFoundError({ message: `Tag not found. Found ${newTags.map((el) => el.slug).join(",")}` }); + + const tagsToCheck = inputSecret.tagIds ? newTags : secret.tags; // now check with new ids ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, secretName: inputSecret.secretName, - secretTags: tags?.map((el) => el.slug) + ...(tagsToCheck.length && { + secretTags: tagsToCheck.map((el) => el.slug) + }) }) ); @@ -394,12 +471,14 @@ export const secretV2BridgeServiceFactory = ({ }); if (doesNewNameSecretExist) throw new BadRequestError({ message: "Secret with the new name already exist" }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath, secretName: inputSecret.newSecretName, - secretTags: tags?.map((el) => el.slug) + ...(tagsToCheck.length && { + secretTags: tagsToCheck.map((el) => el.slug) + }) }) ); } @@ -410,12 +489,13 @@ export const secretV2BridgeServiceFactory = ({ type: KmsDataKey.SecretManager, projectId }); - const encryptedValue = secretValue - ? { - encryptedValue: secretManagerEncryptor({ plainText: Buffer.from(secretValue) }).cipherTextBlob, - references: getAllSecretReferences(secretValue).nestedReferences - } - : {}; + const encryptedValue = + typeof secretValue === "string" + ? { + encryptedValue: secretManagerEncryptor({ plainText: Buffer.from(secretValue) }).cipherTextBlob, + references: getAllSecretReferences(secretValue).nestedReferences + } + : {}; if (secretValue) { const { nestedReferences, localReferences } = getAllSecretReferences(secretValue); @@ -428,6 +508,8 @@ export const secretV2BridgeServiceFactory = ({ const updatedSecret = await secretDAL.transaction(async (tx) => fnSecretBulkUpdate({ folderId, + orgId: actorOrgId, + resourceMetadataDAL, inputSecrets: [ { filter: { id: secretId }, @@ -441,6 +523,7 @@ export const secretV2BridgeServiceFactory = ({ skipMultilineEncoding: inputSecret.skipMultilineEncoding, key: inputSecret.newSecretName || secretName, tags: inputSecret.tagIds, + secretMetadata, ...encryptedValue } } @@ -449,6 +532,10 @@ export const secretV2BridgeServiceFactory = ({ secretVersionDAL, secretTagDAL, secretVersionTagDAL, + actor: { + type: actor, + actorId + }, tx }) ); @@ -461,6 +548,7 @@ export const secretV2BridgeServiceFactory = ({ projectId }); + await secretDAL.invalidateSecretCacheByProjectId(projectId); if (inputSecret.type === SecretType.Shared) { await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ @@ -468,15 +556,35 @@ export const secretV2BridgeServiceFactory = ({ actorId, actor, projectId, + orgId: actorOrgId, environmentSlug: folder.environment.slug }); } - return reshapeBridgeSecret(projectId, environment, secretPath, { - ...updatedSecret[0], - value: inputSecret.secretValue || "", - comment: inputSecret.secretComment || "" - }); + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath, + secretName: inputSecret.secretName, + ...(tagsToCheck.length && { + secretTags: tagsToCheck.map((el) => el.slug) + }) + } + ); + + return reshapeBridgeSecret( + projectId, + environment, + secretPath, + { + ...updatedSecret[0], + value: inputSecret.secretValue || "", + comment: inputSecret.secretComment || "" + }, + secretValueHidden + ); }; const deleteSecret = async ({ @@ -489,13 +597,14 @@ export const secretV2BridgeServiceFactory = ({ secretPath, ...inputSecret }: TDeleteSecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -521,7 +630,7 @@ export const secretV2BridgeServiceFactory = ({ }); if (!secretToDelete) throw new NotFoundError({ message: "Secret not found" }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, + ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -530,47 +639,80 @@ export const secretV2BridgeServiceFactory = ({ }) ); - const deletedSecret = await secretDAL.transaction(async (tx) => - fnSecretBulkDelete({ - projectId, - folderId, - actorId, - secretDAL, - secretQueueService, - inputSecrets: [ - { - type: inputSecret.type as SecretType, - secretKey: inputSecret.secretName - } - ], - tx - }) - ); + try { + const deletedSecret = await secretDAL.transaction(async (tx) => + fnSecretBulkDelete({ + projectId, + folderId, + actorId, + secretDAL, + secretQueueService, + inputSecrets: [ + { + type: inputSecret.type as SecretType, + secretKey: inputSecret.secretName + } + ], + tx + }) + ); - if (inputSecret.type === SecretType.Shared) { - await snapshotService.performSnapshot(folderId); - await secretQueueService.syncSecrets({ - secretPath, - actorId, - actor, - projectId, - environmentSlug: folder.environment.slug + await secretDAL.invalidateSecretCacheByProjectId(projectId); + if (inputSecret.type === SecretType.Shared) { + await snapshotService.performSnapshot(folderId); + await secretQueueService.syncSecrets({ + secretPath, + actorId, + actor, + projectId, + orgId: actorOrgId, + environmentSlug: folder.environment.slug + }); + } + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId }); - } - const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId - }); - return reshapeBridgeSecret(projectId, environment, secretPath, { - ...deletedSecret[0], - value: deletedSecret[0].encryptedValue - ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString() - : "", - comment: deletedSecret[0].encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString() - : "" - }); + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath, + secretName: secretToDelete.key, + secretTags: secretToDelete.tags?.map((el) => el.slug) + } + ); + + return reshapeBridgeSecret( + projectId, + environment, + secretPath, + { + ...deletedSecret[0], + value: deletedSecret[0].encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString() + : "", + comment: deletedSecret[0].encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString() + : "" + }, + secretValueHidden + ); + } catch (err) { + // deferred errors aren't return as DatabaseError + const error = err as { code: string; table: string }; + if ( + error?.code === DatabaseErrorCode.ForeignKeyViolation && + error?.table === TableName.SecretRotationV2SecretMapping + ) { + throw new BadRequestError({ message: "Cannot delete rotated secrets" }); + } + + throw err; + } }; // get unique secrets count for multiple envs @@ -589,15 +731,15 @@ export const secretV2BridgeServiceFactory = ({ isInternal?: boolean; }) => { if (!isInternal) { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); - - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); } const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); @@ -635,15 +777,15 @@ export const secretV2BridgeServiceFactory = ({ | "environment" | "search" >) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); - - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) return 0; @@ -654,17 +796,23 @@ export const secretV2BridgeServiceFactory = ({ }; const getSecretsByFolderMappings = async ( - { projectId, userId, filters, folderMappings }: TGetSecretsRawByFolderMappingsDTO, + { + projectId, + userId, + filters, + folderMappings, + filterByAction = ProjectPermissionSecretActions.ReadValue + }: TGetSecretsRawByFolderMappingsDTO, projectPermission: Awaited>["permission"] ) => { const groupedFolderMappings = groupBy(folderMappings, (folderMapping) => folderMapping.folderId); - const secrets = await secretDAL.findByFolderIds( - folderMappings.map((folderMapping) => folderMapping.folderId), + const secrets = await secretDAL.findByFolderIds({ + folderIds: folderMappings.map((folderMapping) => folderMapping.folderId), userId, - undefined, + tx: undefined, filters - ); + }); const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, @@ -673,18 +821,28 @@ export const secretV2BridgeServiceFactory = ({ const decryptedSecrets = secrets .filter((el) => - projectPermission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: groupedFolderMappings[el.folderId][0].environment, - secretPath: groupedFolderMappings[el.folderId][0].path, - secretName: el.key, - secretTags: el.tags.map((i) => i.slug) - }) - ) + hasSecretReadValueOrDescribePermission(projectPermission, filterByAction, { + environment: groupedFolderMappings[el.folderId][0].environment, + secretPath: groupedFolderMappings[el.folderId][0].path, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }) ) - .map((secret) => - reshapeBridgeSecret( + + .map((secret) => { + // Note(Daniel): This is only relevant if the filterAction isn't set to ReadValue. This is needed for the frontend. + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + projectPermission, + ProjectPermissionSecretActions.ReadValue, + { + environment: groupedFolderMappings[secret.folderId][0].environment, + secretPath: groupedFolderMappings[secret.folderId][0].path, + secretName: secret.key, + secretTags: secret.tags.map((i) => i.slug) + } + ); + + return reshapeBridgeSecret( projectId, groupedFolderMappings[secret.folderId][0].environment, groupedFolderMappings[secret.folderId][0].path, @@ -696,9 +854,10 @@ export const secretV2BridgeServiceFactory = ({ comment: secret.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() : "" - } - ) - ); + }, + secretValueHidden + ); + }); return decryptedSecrets; }; @@ -718,15 +877,16 @@ export const secretV2BridgeServiceFactory = ({ environments: string[]; isInternal?: boolean; }) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); if (!isInternal) { - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); } const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); @@ -746,7 +906,8 @@ export const secretV2BridgeServiceFactory = ({ projectId, folderMappings, filters: params, - userId: actorId + userId: actorId, + filterByAction: ProjectPermissionSecretActions.DescribeSecret }, permission ); @@ -754,28 +915,67 @@ export const secretV2BridgeServiceFactory = ({ return decryptedSecrets; }; - const getSecrets = async ({ - actorId, - path, - environment, - projectId, - actor, - actorOrgId, - actorAuthMethod, - includeImports, - recursive, - expandSecretReferences: shouldExpandSecretReferences, - ...params - }: TGetSecretsDTO) => { - const { permission } = await permissionService.getProjectPermission( + const getSecrets = async (dto: TGetSecretsDTO) => { + const { + actorId, + path, + environment, + projectId, + actor, + actorOrgId, + viewSecretValue, + actorAuthMethod, + includeImports, + recursive, + expandSecretReferences: shouldExpandSecretReferences, + throwOnMissingReadValuePermission = true, + ...params + } = dto; + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + const cachedSecretDalVersion = await keyStore.getItem(SecretServiceCacheKeys.getSecretDalVersion(projectId)); + const secretDalVersion = Number(cachedSecretDalVersion || 0); + const cacheKey = SecretServiceCacheKeys.getSecretsOfServiceLayer(projectId, secretDalVersion, { + ...dto, + permissionRules: permission.rules + }); + + const { decryptor: secretManagerDecryptor, encryptor: secretManagerEncryptor } = + await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + + const encryptedCachedSecrets = await keyStore.getItem(cacheKey); + if (encryptedCachedSecrets) { + try { + await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL); + const cachedSecrets = secretManagerDecryptor({ cipherTextBlob: Buffer.from(encryptedCachedSecrets, "base64") }); + const { secrets, imports = [] } = JSON.parse(cachedSecrets.toString("utf8")) as { + secrets: typeof decryptedSecrets; + imports: typeof importedSecrets; + }; + return { + secrets: secrets.map((el) => ({ + ...el, + createdAt: new Date(el.createdAt), + updatedAt: new Date(el.updatedAt) + })), + imports + }; + } catch (err) { + logger.error(err, "Secret service layer cache miss"); + await keyStore.deleteItem(cacheKey); + } + } let paths: { folderId: string; path: string }[] = []; @@ -800,41 +1000,92 @@ export const secretV2BridgeServiceFactory = ({ const groupedPaths = groupBy(paths, (p) => p.folderId); - const secrets = await secretDAL.findByFolderIds( - paths.map((p) => p.folderId), - actorId, - undefined, - params - ); - - const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId + const secrets = await secretDAL.findByFolderIds({ + folderIds: paths.map((p) => p.folderId), + userId: actorId, + tx: undefined, + filters: params }); + // scott: if any of this changes it also needs to be mirrored in secret rotation for getting dashboard secrets const decryptedSecrets = secrets - .filter((el) => - permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { + .filter((el) => { + const canDescribeSecret = hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.DescribeSecret, + { environment, secretPath: groupedPaths[el.folderId][0].path, secretName: el.key, secretTags: el.tags.map((i) => i.slug) - }) - ) - ) - .map((secret) => - reshapeBridgeSecret(projectId, environment, groupedPaths[secret.folderId][0].path, { - ...secret, - value: secret.encryptedValue - ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() - : "", - comment: secret.encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() - : "" - }) - ); + } + ); + + if (!canDescribeSecret) { + return false; + } + + if (viewSecretValue) { + // Recursive secret, should be filtered out + if (groupedPaths[el.folderId][0].path !== path) { + const canReadRecursiveSecretValue = hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath: groupedPaths[el.folderId][0].path, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + } + ); + + if (!canReadRecursiveSecretValue) { + return false; + } + } + + if (throwOnMissingReadValuePermission) { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: groupedPaths[el.folderId][0].path, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }); + } + // Else, we do nothing. Because we don't want to filter out the secret, OR throw an error. + // If the user doesn't have access to read the value, in the below map function, we mask the secret value and return the secret with a hidden value. + } + + return canDescribeSecret; + }) + .map((secret) => { + const isPersonalSecret = secret.userId === actorId && secret.type === SecretType.Personal; + + const secretValueHidden = + !viewSecretValue || + !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: groupedPaths[secret.folderId][0].path, + secretName: secret.key, + secretTags: secret.tags.map((i) => i.slug) + }); + + return reshapeBridgeSecret( + projectId, + environment, + groupedPaths[secret.folderId][0].path, + { + ...secret, + value: secret.encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() + : "", + comment: secret.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() + : "" + }, + secretValueHidden && !isPersonalSecret + ); + }); const { expandSecretReferences } = expandSecretReferencesFactory({ projectId, @@ -842,15 +1093,12 @@ export const secretV2BridgeServiceFactory = ({ secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => - permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) - ) + hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }) }); if (shouldExpandSecretReferences) { @@ -874,36 +1122,131 @@ export const secretV2BridgeServiceFactory = ({ } if (!includeImports) { - return { - secrets: decryptedSecrets - }; + const payload = { secrets: decryptedSecrets, imports: [] }; + const encryptedUpdatedCachedSecrets = secretManagerEncryptor({ + plainText: Buffer.from(JSON.stringify(payload)) + }).cipherTextBlob; + if (encryptedUpdatedCachedSecrets.byteLength < MAX_SECRET_CACHE_BYTES) { + await keyStore.setItemWithExpiry(cacheKey, SECRET_DAL_TTL, encryptedUpdatedCachedSecrets.toString("base64")); + } + return payload; } const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId)); const allowedImports = secretImports.filter(({ isReplication }) => !isReplication); const importedSecrets = await fnSecretsV2FromImports({ + viewSecretValue, secretImports: allowedImports, secretDAL, folderDAL, secretImportDAL, expandSecretReferences, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), - hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => - permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { + hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { + const canDescribe = hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.DescribeSecret, + { environment: expandEnvironment, secretPath: expandSecretPath, secretName: expandSecretKey, secretTags: expandSecretTags - }) - ) + } + ); + + const canReadValue = hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + } + ); + + return viewSecretValue ? canDescribe && canReadValue : canDescribe; + } }); - return { - secrets: decryptedSecrets, - imports: importedSecrets - }; + const payload = { secrets: decryptedSecrets, imports: importedSecrets }; + const encryptedUpdatedCachedSecrets = secretManagerEncryptor({ + plainText: Buffer.from(JSON.stringify(payload)) + }).cipherTextBlob; + if (encryptedUpdatedCachedSecrets.byteLength < MAX_SECRET_CACHE_BYTES) { + await keyStore.setItemWithExpiry(cacheKey, SECRET_DAL_TTL, encryptedUpdatedCachedSecrets.toString("base64")); + } + return payload; + }; + + const getSecretById = async ({ actorId, actor, actorOrgId, actorAuthMethod, secretId }: TGetASecretByIdDTO) => { + const secret = await secretDAL.findOneWithTags({ + [`${TableName.SecretV2}.id` as "id"]: secretId + }); + + if (!secret) { + throw new NotFoundError({ + message: `Secret with ID '${secretId}' not found`, + name: "GetSecretById" + }); + } + + const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(secret.projectId, [secret.folderId]); + + if (!folderWithPath) { + throw new NotFoundError({ + message: `Folder with id '${secret.folderId}' not found`, + name: "GetSecretById" + }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: secret.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment: folderWithPath.environmentSlug, + secretPath: folderWithPath.path, + secretName: secret.key, + secretTags: secret.tags.map((i) => i.slug) + }); + + if (secret.type === SecretType.Personal && secret.userId !== actorId) { + throw new ForbiddenRequestError({ + message: "You are not allowed to access this secret", + name: "GetSecretById" + }); + } + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: secret.projectId + }); + + const secretValue = secret.encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() + : ""; + + const secretComment = secret.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() + : ""; + + return reshapeBridgeSecret( + secret.projectId, + folderWithPath.environmentSlug, + folderWithPath.path, + { + ...secret, + value: secretValue, + comment: secretComment + }, + false + ); }; const getSecretByName = async ({ @@ -917,16 +1260,18 @@ export const secretV2BridgeServiceFactory = ({ type, secretName, version, + viewSecretValue, includeImports, expandSecretReferences: shouldExpandSecretReferences }: TGetASecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) @@ -953,89 +1298,117 @@ export const secretV2BridgeServiceFactory = ({ ? secretDAL.findOneWithTags({ folderId, type: secretType, - key: secretName, - userId: secretType === SecretType.Personal ? actorId : null + [`${TableName.SecretV2}.key` as "key"]: secretName, + [`${TableName.SecretV2}.userId` as "userId"]: secretType === SecretType.Personal ? actorId : null }) : secretVersionDAL .findOne({ folderId, + version, type: secretType, userId: secretType === SecretType.Personal ? actorId : null, key: secretName }) .then((el) => - SecretsV2Schema.extend({ - tags: z - .object({ slug: z.string(), name: z.string(), id: z.string(), color: z.string() }) - .array() - .default([]) - .optional() - }).parse({ - ...el, - id: el.secretId - }) + el + ? SecretsV2Schema.extend({ + tags: z + .object({ slug: z.string(), name: z.string(), id: z.string(), color: z.string() }) + .array() + .default([]) + .optional() + }).parse({ + ...el, + id: el.secretId + }) + : undefined )); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: path, - secretName, - secretTags: (secret?.tags || []).map((el) => el.slug) - }) - ); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath: path, + secretName, + secretTags: (secret?.tags || []).map((el) => el.slug) + }); + // this will throw if the user doesn't have read value permission no matter what + // because if its an expansion, it will fully depend on the value. const { expandSecretReferences } = expandSecretReferencesFactory({ projectId, folderDAL, secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), - canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => - permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) - ) + canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { + return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }); + } }); // now if secret is not found // then search for imported secrets // here we consider the import order also thus starting from bottom + + // currently filters out the secrets that the user doesn't have access to read value on if (!secret && includeImports) { const secretImports = await secretImportDAL.find({ folderId, isReplication: false }); const importedSecrets = await fnSecretsV2FromImports({ secretImports, + viewSecretValue, secretDAL, folderDAL, secretImportDAL, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), - expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined, - hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => - permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretKey, - secretTags: expandSecretTags - }) - ) + expandSecretReferences: shouldExpandSecretReferences && viewSecretValue ? expandSecretReferences : undefined, + hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => { + return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }); + } }); for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) { const importedSecret = importedSecrets[i].secrets[j]; if (secretName === importedSecret.key) { - return reshapeBridgeSecret(projectId, importedSecrets[i].environment, importedSecrets[i].secretPath, { - ...importedSecret, - value: importedSecret.secretValue || "", - comment: importedSecret.secretComment || "" - }); + let secretValueHidden = true; + + if (viewSecretValue) { + if ( + !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment: importedSecret.environment, + secretPath: importedSecrets[i].secretPath, + secretName: importedSecret.key, + secretTags: (importedSecret.secretTags || []).map((el) => el.slug) + }) && + secretType !== SecretType.Personal + ) { + throw new ForbiddenRequestError({ + message: `You do not have permission to view secret import value on secret with name '${secretName}'`, + name: "ForbiddenReadSecretError" + }); + } + + secretValueHidden = false; + } + + return reshapeBridgeSecret( + projectId, + importedSecrets[i].environment, + importedSecrets[i].secretPath, + { + ...importedSecret, + value: importedSecret.secretValue || "", + comment: importedSecret.secretComment || "" + }, + secretValueHidden + ); } } } @@ -1045,7 +1418,7 @@ export const secretV2BridgeServiceFactory = ({ let secretValue = secret.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() : ""; - if (shouldExpandSecretReferences && secretValue) { + if (shouldExpandSecretReferences && secretValue && viewSecretValue) { // eslint-disable-next-line const expandedSecretValue = await expandSecretReferences({ environment, @@ -1057,13 +1430,40 @@ export const secretV2BridgeServiceFactory = ({ secretValue = expandedSecretValue || ""; } - return reshapeBridgeSecret(projectId, environment, path, { - ...secret, - value: secretValue, - comment: secret.encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() - : "" - }); + let secretValueHidden = true; + + if (viewSecretValue) { + if ( + !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: path, + secretName, + secretTags: (secret?.tags || []).map((el) => el.slug) + }) && + secretType !== SecretType.Personal + ) { + throw new ForbiddenRequestError({ + message: `You do not have permission to view secret value on secret with name '${secretName}'`, + name: "ForbiddenReadSecretError" + }); + } + + secretValueHidden = false; + } + + return reshapeBridgeSecret( + projectId, + environment, + path, + { + ...secret, + value: secretValue, + comment: secret.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() + : "" + }, + secretValueHidden + ); }; const createManySecret = async ({ @@ -1076,13 +1476,14 @@ export const secretV2BridgeServiceFactory = ({ projectId, secrets: inputSecrets }: TCreateManySecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -1104,7 +1505,7 @@ export const secretV2BridgeServiceFactory = ({ value: [ { operator: "eq", - field: "key", + field: `${TableName.SecretV2}.key` as "key", value: el.secretKey }, { @@ -1130,7 +1531,7 @@ export const secretV2BridgeServiceFactory = ({ inputSecrets.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1161,7 +1562,7 @@ export const secretV2BridgeServiceFactory = ({ const newSecrets = await secretDAL.transaction(async (tx) => fnSecretBulkInsert({ inputSecrets: inputSecrets.map((el) => { - const references = secretReferencesGroupByInputSecretKey[el.secretKey].nestedReferences; + const references = secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences; return { version: 1, @@ -1176,34 +1577,59 @@ export const secretV2BridgeServiceFactory = ({ key: el.secretKey, tagIds: el.tagIds, references, + secretMetadata: el.secretMetadata, type: SecretType.Shared }; }), folderId, + orgId: actorOrgId, secretDAL, + resourceMetadataDAL, secretVersionDAL, secretTagDAL, secretVersionTagDAL, + actor: { + type: actor, + actorId + }, tx }) ); - + await secretDAL.invalidateSecretCacheByProjectId(projectId); await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ actor, actorId, secretPath, projectId, + orgId: actorOrgId, environmentSlug: folder.environment.slug }); - return newSecrets.map((el) => - reshapeBridgeSecret(projectId, environment, secretPath, { - ...el, - value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", - comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" - }) - ); + return newSecrets.map((el) => { + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath, + secretName: el.key, + secretTags: el.tags?.map((i) => i.slug) + } + ); + + return reshapeBridgeSecret( + projectId, + environment, + secretPath, + { + ...el, + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" + }, + secretValueHidden + ); + }); }; const updateManySecret = async ({ @@ -1213,203 +1639,347 @@ export const secretV2BridgeServiceFactory = ({ actorAuthMethod, environment, projectId, - secretPath, - secrets: inputSecrets + secretPath: defaultSecretPath = "/", + secrets: inputSecrets, + mode: updateMode }: TUpdateManySecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); - const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); - if (!folder) + const secretsToUpdateGroupByPath = groupBy(inputSecrets, (el) => el.secretPath || defaultSecretPath); + const projectEnvironment = await projectEnvDAL.findOne({ projectId, slug: environment }); + if (!projectEnvironment) { throw new NotFoundError({ - message: `Folder with path '${secretPath}' in environment with slug '${environment}' not found`, - name: "UpdateManySecret" - }); - const folderId = folder.id; - - const secretsToUpdate = await secretDAL.find({ - folderId, - $complex: { - operator: "and", - value: [ - { - operator: "or", - value: inputSecrets.map((el) => ({ - operator: "and", - value: [ - { - operator: "eq", - field: "key", - value: el.secretKey - }, - { - operator: "eq", - field: "type", - value: SecretType.Shared - } - ] - })) - } - ] - } - }); - if (secretsToUpdate.length !== inputSecrets.length) - throw new NotFoundError({ message: `Secret does not exist: ${secretsToUpdate.map((el) => el.key).join(",")}` }); - const secretsToUpdateInDBGroupedByKey = groupBy(secretsToUpdate, (i) => i.key); - - secretsToUpdate.forEach((el) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName: el.key, - secretTags: el.tags.map((i) => i.slug) - }) - ); - }); - - // get all tags - const sanitizedTagIds = inputSecrets.flatMap(({ tagIds = [] }) => tagIds); - const tags = sanitizedTagIds.length ? await secretTagDAL.findManyTagsById(projectId, sanitizedTagIds) : []; - if (tags.length !== sanitizedTagIds.length) throw new NotFoundError({ message: "Tag not found" }); - const tagsGroupByID = groupBy(tags, (i) => i.id); - - // check again to avoid non authorized tags are removed - inputSecrets.forEach((el) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName: el.secretKey, - secretTags: (el.tagIds || []).map((i) => tagsGroupByID[i][0].slug) - }) - ); - }); - - // now find any secret that needs to update its name - // same process as above - const secretsWithNewName = inputSecrets.filter(({ newSecretName }) => Boolean(newSecretName)); - if (secretsWithNewName.length) { - const secrets = await secretDAL.find({ - folderId, - $complex: { - operator: "and", - value: [ - { - operator: "or", - value: secretsWithNewName.map((el) => ({ - operator: "and", - value: [ - { - operator: "eq", - field: "key", - value: el.secretKey - }, - { - operator: "eq", - field: "type", - value: SecretType.Shared - } - ] - })) - } - ] - } - }); - if (secrets.length) - throw new BadRequestError({ - message: `Secret with new name already exists: ${secretsWithNewName.map((el) => el.newSecretName).join(",")}` - }); - - secretsWithNewName.forEach((el) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName: el.newSecretName as string, - secretTags: (el.tagIds || []).map((i) => tagsGroupByID[i][0].slug) - }) - ); + message: `Environment with slug '${environment}' in project with ID '${projectId}' not found` }); } - // now get all secret references made and validate the permission - const secretReferencesGroupByInputSecretKey: Record> = {}; - const secretReferences: TSecretReference[] = []; - inputSecrets.forEach((el) => { - if (el.secretValue) { - const references = getAllSecretReferences(el.secretValue); - secretReferencesGroupByInputSecretKey[el.secretKey] = references; - secretReferences.push(...references.nestedReferences); - references.localReferences.forEach((localRefKey) => { - secretReferences.push({ secretKey: localRefKey, secretPath, environment }); - }); - } - }); - await $validateSecretReferences(projectId, permission, secretReferences); + + const folders = await folderDAL.findByManySecretPath( + Object.keys(secretsToUpdateGroupByPath).map((el) => ({ envId: projectEnvironment.id, secretPath: el })) + ); + if (folders.length !== Object.keys(secretsToUpdateGroupByPath).length) + throw new NotFoundError({ + message: `Folder with path '${null}' in environment with slug '${environment}' not found`, + name: "UpdateManySecret" + }); const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); - const secrets = await secretDAL.transaction(async (tx) => - fnSecretBulkUpdate({ - folderId, - tx, - inputSecrets: inputSecrets.map((el) => { - const originalSecret = secretsToUpdateInDBGroupedByKey[el.secretKey][0]; - const encryptedValue = - typeof el.secretValue !== "undefined" - ? { - encryptedValue: secretManagerEncryptor({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob, - references: secretReferencesGroupByInputSecretKey[el.secretKey].nestedReferences - } - : {}; + const updatedSecrets: Array< + TSecretsV2 & { + secretPath: string; + tags: { + id: string; + slug: string; + color?: string | null; + name: string; + }[]; + } + > = []; + await secretDAL.transaction(async (tx) => { + for await (const folder of folders) { + if (!folder) throw new NotFoundError({ message: "Folder not found" }); - return { - filter: { id: originalSecret.id, type: SecretType.Shared }, - data: { - reminderRepeatDays: el.secretReminderRepeatDays, - encryptedComment: setKnexStringValue( - el.secretComment, - (value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob - ), - reminderNote: el.secretReminderNote, - skipMultilineEncoding: el.skipMultilineEncoding, - key: el.newSecretName || el.secretKey, - tags: el.tagIds, - ...encryptedValue + const folderId = folder.id; + const secretPath = folder.path; + let secretsToUpdate = secretsToUpdateGroupByPath[secretPath]; + const secretsToUpdateInDB = await secretDAL.find( + { + folderId, + $complex: { + operator: "and", + value: [ + { + operator: "or", + value: secretsToUpdate.map((el) => ({ + operator: "and", + value: [ + { + operator: "eq", + field: `${TableName.SecretV2}.key` as "key", + value: el.secretKey + }, + { + operator: "eq", + field: "type", + value: SecretType.Shared + } + ] + })) + } + ] } - }; - }), - secretDAL, - secretVersionDAL, - secretTagDAL, - secretVersionTagDAL - }) - ); - await snapshotService.performSnapshot(folderId); - await secretQueueService.syncSecrets({ - actor, - actorId, - secretPath, - projectId, - environmentSlug: folder.environment.slug + }, + { tx } + ); + if (secretsToUpdateInDB.length !== secretsToUpdate.length && updateMode === SecretUpdateMode.FailOnNotFound) + throw new NotFoundError({ + message: `Secret does not exist: ${diff( + secretsToUpdate.map((el) => el.secretKey), + secretsToUpdateInDB.map((el) => el.key) + ).join(", ")} in path ${folder.path}` + }); + + const secretsToUpdateInDBGroupedByKey = groupBy(secretsToUpdateInDB, (i) => i.key); + const secretsToCreate = secretsToUpdate.filter((el) => !secretsToUpdateInDBGroupedByKey?.[el.secretKey]); + secretsToUpdate = secretsToUpdate.filter((el) => secretsToUpdateInDBGroupedByKey?.[el.secretKey]); + + secretsToUpdateInDB.forEach((el) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.Edit, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }) + ); + + if (el.isRotatedSecret) { + const input = secretsToUpdateGroupByPath[secretPath].find((i) => i.secretKey === el.key); + + if (input && (input.newSecretName || input.secretValue)) + throw new BadRequestError({ message: `Cannot update rotated secret name or value: ${el.key}` }); + } + }); + + // get all tags + const sanitizedTagIds = secretsToUpdate.flatMap(({ tagIds = [] }) => tagIds); + const tags = sanitizedTagIds.length ? await secretTagDAL.findManyTagsById(projectId, sanitizedTagIds, tx) : []; + if (tags.length !== sanitizedTagIds.length) throw new NotFoundError({ message: "Tag not found" }); + const tagsGroupByID = groupBy(tags, (i) => i.id); + + // check create permission allowed in upsert mode + if (updateMode === SecretUpdateMode.Upsert) { + secretsToCreate.forEach((el) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.Create, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: el.secretKey, + secretTags: (el.tagIds || []).map((i) => tagsGroupByID[i][0].slug) + }) + ); + }); + } + + // check again to avoid non authorized tags are removed + secretsToUpdate.forEach((el) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.Edit, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: el.secretKey, + secretTags: (el.tagIds || []).map((i) => tagsGroupByID[i][0].slug) + }) + ); + }); + + // now find any secret that needs to update its name + // same process as above + const secretsWithNewName = secretsToUpdate.filter(({ newSecretName }) => Boolean(newSecretName)); + if (secretsWithNewName.length) { + const secrets = await secretDAL.find( + { + folderId, + $complex: { + operator: "and", + value: [ + { + operator: "or", + value: secretsWithNewName.map((el) => ({ + operator: "and", + value: [ + { + operator: "eq", + field: `${TableName.SecretV2}.key` as "key", + value: el.secretKey + }, + { + operator: "eq", + field: "type", + value: SecretType.Shared + } + ] + })) + } + ] + } + }, + { tx } + ); + if (secrets.length) + throw new BadRequestError({ + message: `Secret with new name already exists: ${secretsWithNewName + .map((el) => el.newSecretName) + .join(", ")}` + }); + + secretsWithNewName.forEach((el) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretActions.Create, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: el.newSecretName as string, + secretTags: (el.tagIds || []).map((i) => tagsGroupByID[i][0].slug) + }) + ); + }); + } + // now get all secret references made and validate the permission + const secretReferencesGroupByInputSecretKey: Record> = {}; + const secretReferences: TSecretReference[] = []; + secretsToUpdate.concat(SecretUpdateMode.Upsert === updateMode ? secretsToCreate : []).forEach((el) => { + if (el.secretValue) { + const references = getAllSecretReferences(el.secretValue); + secretReferencesGroupByInputSecretKey[el.secretKey] = references; + secretReferences.push(...references.nestedReferences); + references.localReferences.forEach((localRefKey) => { + secretReferences.push({ secretKey: localRefKey, secretPath, environment }); + }); + } + }); + await $validateSecretReferences(projectId, permission, secretReferences, tx); + + const bulkUpdatedSecrets = await fnSecretBulkUpdate({ + folderId, + orgId: actorOrgId, + tx, + inputSecrets: secretsToUpdate.map((el) => { + const originalSecret = secretsToUpdateInDBGroupedByKey[el.secretKey][0]; + const encryptedValue = + typeof el.secretValue !== "undefined" + ? { + encryptedValue: secretManagerEncryptor({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob, + references: secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences + } + : {}; + + return { + filter: { id: originalSecret.id, type: SecretType.Shared }, + data: { + reminderRepeatDays: el.secretReminderRepeatDays, + encryptedComment: setKnexStringValue( + el.secretComment, + (value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob + ), + reminderNote: el.secretReminderNote, + skipMultilineEncoding: el.skipMultilineEncoding, + key: el.newSecretName || el.secretKey, + tags: el.tagIds, + secretMetadata: el.secretMetadata, + ...encryptedValue + } + }; + }), + secretDAL, + secretVersionDAL, + secretTagDAL, + secretVersionTagDAL, + actor: { + type: actor, + actorId + }, + resourceMetadataDAL + }); + + updatedSecrets.push(...bulkUpdatedSecrets.map((el) => ({ ...el, secretPath: folder.path }))); + if (updateMode === SecretUpdateMode.Upsert) { + const bulkInsertedSecrets = await fnSecretBulkInsert({ + inputSecrets: secretsToCreate.map((el) => { + const references = secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences; + + return { + version: 1, + encryptedComment: setKnexStringValue( + el.secretComment, + (value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob + ), + encryptedValue: el.secretValue + ? secretManagerEncryptor({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob + : undefined, + skipMultilineEncoding: el.skipMultilineEncoding, + key: el.secretKey, + tagIds: el.tagIds, + references, + secretMetadata: el.secretMetadata, + type: SecretType.Shared + }; + }), + folderId, + orgId: actorOrgId, + secretDAL, + resourceMetadataDAL, + secretVersionDAL, + secretTagDAL, + secretVersionTagDAL, + actor: { + type: actor, + actorId + }, + tx + }); + + updatedSecrets.push(...bulkInsertedSecrets.map((el) => ({ ...el, secretPath: folder.path }))); + } + } }); - return secrets.map((el) => - reshapeBridgeSecret(projectId, environment, secretPath, { - ...el, - value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", - comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" - }) + await secretDAL.invalidateSecretCacheByProjectId(projectId); + await Promise.allSettled(folders.map((el) => (el?.id ? snapshotService.performSnapshot(el.id) : undefined))); + await Promise.allSettled( + folders.map((el) => + el + ? secretQueueService.syncSecrets({ + actor, + actorId, + secretPath: el.path, + projectId, + orgId: actorOrgId, + environmentSlug: environment + }) + : undefined + ) ); + + return updatedSecrets.map((el) => { + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath: el.secretPath, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + } + ); + + return { + ...reshapeBridgeSecret( + projectId, + environment, + el.secretPath, + { + ...el, + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + comment: el.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() + : "" + }, + secretValueHidden + ) + }; + }); }; const deleteManySecret = async ({ @@ -1422,13 +1992,14 @@ export const secretV2BridgeServiceFactory = ({ actorAuthMethod, actorOrgId }: TDeleteManySecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -1450,7 +2021,7 @@ export const secretV2BridgeServiceFactory = ({ value: [ { operator: "eq", - field: "key", + field: `${TableName.SecretV2}.key` as "key", value: el.secretKey }, { @@ -1470,7 +2041,7 @@ export const secretV2BridgeServiceFactory = ({ }); secretsToDelete.forEach((el) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, + ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath, @@ -1480,41 +2051,77 @@ export const secretV2BridgeServiceFactory = ({ ); }); - const secretsDeleted = await secretDAL.transaction(async (tx) => - fnSecretBulkDelete({ - secretDAL, - secretQueueService, - inputSecrets: inputSecrets.map(({ type, secretKey }) => ({ - secretKey, - type: type || SecretType.Shared - })), - projectId, - folderId, + try { + const secretsDeleted = await secretDAL.transaction(async (tx) => + fnSecretBulkDelete({ + secretDAL, + secretQueueService, + inputSecrets: inputSecrets.map(({ type, secretKey }) => ({ + secretKey, + type: type || SecretType.Shared + })), + projectId, + folderId, + actorId, + tx + }) + ); + + await secretDAL.invalidateSecretCacheByProjectId(projectId); + await snapshotService.performSnapshot(folderId); + await secretQueueService.syncSecrets({ + actor, actorId, - tx - }) - ); + secretPath, + projectId, + orgId: actorOrgId, + environmentSlug: folder.environment.slug + }); - // await snapshotService.performSnapshot(folderId); - await secretQueueService.syncSecrets({ - actor, - actorId, - secretPath, - projectId, - environmentSlug: folder.environment.slug - }); + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + return secretsDeleted.map((el) => { + const secretToDeleteMatch = secretsToDelete.find( + (i) => i.key === el.key && (i.type || SecretType.Shared) === el.type + ); - const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId - }); - return secretsDeleted.map((el) => - reshapeBridgeSecret(projectId, environment, secretPath, { - ...el, - value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", - comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" - }) - ); + const secretValueHidden = + !secretToDeleteMatch || + !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath, + secretName: el.key, + secretTags: secretToDeleteMatch.tags?.map((i) => i.slug) + }); + + return reshapeBridgeSecret( + projectId, + environment, + secretPath, + { + ...el, + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + comment: el.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() + : "" + }, + secretValueHidden + ); + }); + } catch (err) { + // deferred errors aren't return as DatabaseError + const error = err as { code: string; table: string }; + if ( + error?.code === DatabaseErrorCode.ForeignKeyViolation && + error?.table === TableName.SecretRotationV2SecretMapping + ) { + throw new BadRequestError({ message: "Cannot delete rotated secrets" }); + } + + throw err; + } }; const getSecretVersions = async ({ @@ -1527,31 +2134,62 @@ export const secretV2BridgeServiceFactory = ({ secretId }: TGetSecretVersionsDTO) => { const secret = await secretDAL.findById(secretId); + if (!secret) throw new NotFoundError({ message: `Secret with ID '${secretId}' not found` }); const folder = await folderDAL.findById(secret.folderId); if (!folder) throw new NotFoundError({ message: `Folder with ID '${secret.folderId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(folder.projectId, [folder.id]); + + if (!folderWithPath) { + throw new NotFoundError({ message: `Folder with ID '${folder.id}' not found` }); + } + + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - folder.projectId, + projectId: folder.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId: folder.projectId }); - const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] }); - return secretVersions.map((el) => - reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", { - ...el, - value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", - comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" - }) - ); + const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors(secretId, folder.projectId, { + offset, + limit, + sort: [["createdAt", "desc"]] + }); + return secretVersions.map((el) => { + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment: folder.environment.envSlug, + secretPath: folderWithPath.path, + secretName: el.key, + ...(el.tags?.length && { + secretTags: el.tags.map((tag) => tag.slug) + }) + } + ); + + return reshapeBridgeSecret( + folder.projectId, + folder.environment.envSlug, + folderWithPath.path, + { + ...el, + value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", + comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" + }, + secretValueHidden + ); + }); }; // this is a backfilling API for secret references @@ -1564,13 +2202,14 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId, actorAuthMethod }: TBackFillSecretReferencesDTO) => { - const { hasRole } = await permissionService.getProjectPermission( + const { hasRole } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); if (!hasRole(ProjectMembershipRole.Admin)) throw new ForbiddenRequestError({ message: "Only admins are allowed to take this action" }); @@ -1611,13 +2250,14 @@ export const secretV2BridgeServiceFactory = ({ actorAuthMethod, actorOrgId }: TMoveSecretsDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); const sourceFolder = await folderDAL.findBySecretPath(projectId, sourceEnvironment, sourceSecretPath); if (!sourceFolder) { @@ -1644,16 +2284,42 @@ export const secretV2BridgeServiceFactory = ({ [`${TableName.SecretV2}.id` as "id"]: secretIds } }); + + const sourceActions = [ + ProjectPermissionSecretActions.Delete, + ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.DescribeSecret + ] as const; + const destinationActions = [ProjectPermissionSecretActions.Create, ProjectPermissionSecretActions.Edit] as const; + sourceSecrets.forEach((secret) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { - environment: sourceEnvironment, - secretPath: sourceSecretPath, - secretName: secret.key, - secretTags: secret.tags.map((el) => el.slug) - }) - ); + if (secret.isRotatedSecret) { + throw new BadRequestError({ message: `Cannot move rotated secret: ${secret.key}` }); + } + + for (const sourceAction of sourceActions) { + if ( + sourceAction === ProjectPermissionSecretActions.DescribeSecret || + sourceAction === ProjectPermissionSecretActions.ReadValue + ) { + throwIfMissingSecretReadValueOrDescribePermission(permission, sourceAction, { + environment: sourceEnvironment, + secretPath: sourceSecretPath, + secretName: secret.key, + secretTags: secret.tags.map((el) => el.slug) + }); + } else { + ForbiddenError.from(permission).throwUnlessCan( + sourceAction, + subject(ProjectPermissionSub.Secrets, { + environment: sourceEnvironment, + secretPath: sourceSecretPath, + secretName: secret.key, + secretTags: secret.tags.map((el) => el.slug) + }) + ); + } + } }); if (sourceSecrets.length !== secretIds.length) { @@ -1728,27 +2394,17 @@ export const secretV2BridgeServiceFactory = ({ // permission check whether can create or edit the ones in the destination folder locallyCreatedSecrets.forEach((secret) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { - environment: destinationEnvironment, - secretPath: destinationEnvironment, - secretName: secret.key, - secretTags: secret.tags.map((el) => el.slug) - }) - ); - }); - - locallyUpdatedSecrets.forEach((secret) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { - environment: destinationEnvironment, - secretPath: destinationEnvironment, - secretName: secret.key, - secretTags: secret.tags.map((el) => el.slug) - }) - ); + for (const destinationAction of destinationActions) { + ForbiddenError.from(permission).throwUnlessCan( + destinationAction, + subject(ProjectPermissionSub.Secrets, { + environment: destinationEnvironment, + secretPath: destinationFolder.path, + secretName: secret.key, + secretTags: secret.tags.map((el) => el.slug) + }) + ); + } }); const destinationFolderPolicy = await secretApprovalPolicyService.getSecretApprovalPolicy( @@ -1802,11 +2458,17 @@ export const secretV2BridgeServiceFactory = ({ if (locallyCreatedSecrets.length) { await fnSecretBulkInsert({ folderId: destinationFolder.id, + orgId: actorOrgId, secretVersionDAL, secretDAL, tx, secretTagDAL, + resourceMetadataDAL, secretVersionTagDAL, + actor: { + type: actor, + actorId + }, inputSecrets: locallyCreatedSecrets.map((doc) => { return { type: doc.type, @@ -1817,6 +2479,7 @@ export const secretV2BridgeServiceFactory = ({ skipMultilineEncoding: doc.skipMultilineEncoding, reminderNote: doc.reminderNote, reminderRepeatDays: doc.reminderRepeatDays, + secretMetadata: doc.secretMetadata, references: doc.value ? getAllSecretReferences(doc.value).nestedReferences : [] }; }) @@ -1825,11 +2488,17 @@ export const secretV2BridgeServiceFactory = ({ if (locallyUpdatedSecrets.length) { await fnSecretBulkUpdate({ folderId: destinationFolder.id, + orgId: actorOrgId, + resourceMetadataDAL, secretVersionDAL, secretDAL, tx, secretTagDAL, secretVersionTagDAL, + actor: { + type: actor, + actorId + }, inputSecrets: locallyUpdatedSecrets.map((doc) => { return { filter: { @@ -1842,6 +2511,7 @@ export const secretV2BridgeServiceFactory = ({ encryptedComment: doc.encryptedComment, skipMultilineEncoding: doc.skipMultilineEncoding, reminderNote: doc.reminderNote, + secretMetadata: doc.secretMetadata, reminderRepeatDays: doc.reminderRepeatDays, ...(doc.encryptedValue ? { @@ -1921,10 +2591,14 @@ export const secretV2BridgeServiceFactory = ({ } }); + if (isDestinationUpdated || isSourceUpdated) { + await secretDAL.invalidateSecretCacheByProjectId(projectId); + } if (isDestinationUpdated) { await snapshotService.performSnapshot(destinationFolder.id); await secretQueueService.syncSecrets({ projectId, + orgId: actorOrgId, secretPath: destinationFolder.path, environmentSlug: destinationFolder.environment.slug, actorId, @@ -1936,6 +2610,7 @@ export const secretV2BridgeServiceFactory = ({ await snapshotService.performSnapshot(sourceFolder.id); await secretQueueService.syncSecrets({ projectId, + orgId: actorOrgId, secretPath: sourceFolder.path, environmentSlug: sourceFolder.environment.slug, actorId, @@ -1960,18 +2635,19 @@ export const secretV2BridgeServiceFactory = ({ secretName, actorAuthMethod }: TGetSecretReferencesTreeDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath + }); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -1992,17 +2668,16 @@ export const secretV2BridgeServiceFactory = ({ type: SecretType.Shared }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath, - secretName, - secretTags: (secret?.tags || []).map((el) => el.slug) - }) - ); + if (!secret) throw new NotFoundError({ message: `Secret with name '${secretName}' not found` }); - const secretValue = secret.encryptedValue + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath, + secretName, + secretTags: (secret?.tags || []).map((el) => el.slug) + }); + + const decryptedSecretValue = secret?.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() : ""; @@ -2012,26 +2687,154 @@ export const secretV2BridgeServiceFactory = ({ secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), canExpandValue: (expandEnvironment, expandSecretPath, expandSecretName, expandSecretTags) => - permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: expandEnvironment, - secretPath: expandSecretPath, - secretName: expandSecretName, - secretTags: expandSecretTags - }) - ) + hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretName, + secretTags: expandSecretTags + }) }); + if ( + !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath, + secretName, + secretTags: (secret?.tags || []).map((el) => el.slug) + }) + ) { + throw new ForbiddenRequestError({ + message: `Unable to get secret reference tree for secret with key '${secretName}', because you don't have permission to view secret value.` + }); + } + const { expandedValue, stackTrace } = await getExpandedSecretStackTrace({ environment, secretPath, - value: secretValue + value: decryptedSecretValue }); return { tree: stackTrace, value: expandedValue }; }; + const getAccessibleSecrets = async ({ + projectId, + secretPath, + environment, + filterByAction, + actorId, + actor, + actorAuthMethod, + actorOrgId, + recursive + }: TGetAccessibleSecretsDTO) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath + }); + + const folders = []; + const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); + if (!folder) return { secrets: [] }; + folders.push({ ...folder, parentId: null }); + + const env = await projectEnvDAL.findOne({ + projectId, + slug: environment + }); + + if (!env) { + throw new NotFoundError({ + message: `Environment with slug '${environment}' in project with ID ${projectId} not found` + }); + } + + if (recursive) { + const subFolders = await folderDAL.find({ + envId: env.id, + isReserved: false + }); + folders.push(...subFolders); + } + + if (folders.length === 0) return { secrets: [] }; + + const folderMap = buildHierarchy(folders); + const paths = Object.fromEntries( + generatePaths(folderMap).map(({ folderId, path }) => [folderId, path === "/" ? path : path.substring(1)]) + ); + + const secrets = await secretDAL.findByFolderIds({ folderIds: folders.map((f) => f.id) }); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + + const decryptedSecrets = secrets + .filter((el) => { + if ( + !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment, + secretPath: paths[el.folderId], + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }) + ) { + return false; + } + + if (filterByAction === ProjectPermissionSecretActions.ReadValue) { + return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: paths[el.folderId], + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }); + } + + return true; + }) + .map((secret) => { + const secretValueHidden = + filterByAction === ProjectPermissionSecretActions.DescribeSecret && + !hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: paths[secret.folderId], + secretName: secret.key, + secretTags: secret.tags.map((i) => i.slug) + }); + + return reshapeBridgeSecret( + projectId, + environment, + paths[secret.folderId], + { + ...secret, + value: secret.encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() + : "", + comment: secret.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() + : "" + }, + secretValueHidden + ); + }); + + return { + secrets: decryptedSecrets + }; + }; + return { createSecret, deleteSecret, @@ -2048,6 +2851,8 @@ export const secretV2BridgeServiceFactory = ({ getSecretsCountMultiEnv, getSecretsMultiEnv, getSecretReferenceTree, - getSecretsByFolderMappings + getSecretsByFolderMappings, + getSecretById, + getAccessibleSecrets }; }; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts index e621f8edb..11149c605 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts @@ -1,12 +1,15 @@ import { Knex } from "knex"; import { SecretType, TSecretsV2, TSecretsV2Insert, TSecretsV2Update } from "@app/db/schemas"; +import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; import { OrderByDirection, TProjectPermission } from "@app/lib/types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; +import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; +import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal"; import { TSecretVersionV2DALFactory } from "./secret-version-dal"; import { TSecretVersionV2TagDALFactory } from "./secret-version-tag-dal"; @@ -21,6 +24,12 @@ export type TSecretReferenceDTO = { secretKey: string; }; +export enum SecretUpdateMode { + Ignore = "ignore", + Upsert = "upsert", + FailOnNotFound = "failOnNotFound" +} + export type TGetSecretsDTO = { expandSecretReferences?: boolean; path: string; @@ -28,13 +37,25 @@ export type TGetSecretsDTO = { includeImports?: boolean; recursive?: boolean; tagSlugs?: string[]; + viewSecretValue: boolean; + throwOnMissingReadValuePermission?: boolean; + metadataFilter?: { + key?: string; + value?: string; + }[]; orderBy?: SecretsOrderBy; orderDirection?: OrderByDirection; offset?: number; limit?: number; search?: string; + keys?: string[]; } & TProjectPermission; +export type TGetSecretsMissingReadValuePermissionDTO = Omit< + TGetSecretsDTO, + "viewSecretValue" | "recursive" | "expandSecretReferences" +>; + export type TGetASecretDTO = { secretName: string; path: string; @@ -44,6 +65,7 @@ export type TGetASecretDTO = { includeImports?: boolean; version?: number; projectId: string; + viewSecretValue: boolean; } & Omit; export type TCreateSecretDTO = TProjectPermission & { @@ -57,6 +79,7 @@ export type TCreateSecretDTO = TProjectPermission & { skipMultilineEncoding?: boolean; secretReminderRepeatDays?: number | null; secretReminderNote?: string | null; + secretMetadata?: ResourceMetadataDTO; }; export type TUpdateSecretDTO = TProjectPermission & { @@ -74,6 +97,7 @@ export type TUpdateSecretDTO = TProjectPermission & { metadata?: { source?: string; }; + secretMetadata?: ResourceMetadataDTO; }; export type TDeleteSecretDTO = TProjectPermission & { @@ -93,6 +117,7 @@ export type TCreateManySecretDTO = Omit & { secretComment?: string; skipMultilineEncoding?: boolean; tagIds?: string[]; + secretMetadata?: ResourceMetadataDTO; metadata?: { source?: string; }; @@ -103,15 +128,18 @@ export type TUpdateManySecretDTO = Omit & { secretPath: string; projectId: string; environment: string; + mode: SecretUpdateMode; secrets: { secretKey: string; newSecretName?: string; - secretValue: string; + secretValue?: string; secretComment?: string; skipMultilineEncoding?: boolean; tagIds?: string[]; secretReminderRepeatDays?: number | null; secretReminderNote?: string | null; + secretMetadata?: ResourceMetadataDTO; + secretPath?: string; }[]; }; @@ -135,12 +163,24 @@ export type TSecretReference = { environment: string; secretPath: string; secret export type TFnSecretBulkInsert = { folderId: string; + orgId: string; tx?: Knex; - inputSecrets: Array & { tagIds?: string[]; references: TSecretReference[] }>; - secretDAL: Pick; + inputSecrets: Array< + Omit & { + tagIds?: string[]; + references: TSecretReference[]; + secretMetadata?: ResourceMetadataDTO; + } + >; + resourceMetadataDAL: Pick; + secretDAL: Pick; secretVersionDAL: Pick; - secretTagDAL: Pick; + secretTagDAL: Pick; secretVersionTagDAL: Pick; + actor?: { + type: string; + actorId: string; + }; }; type TRequireReferenceIfValue = @@ -155,14 +195,20 @@ type TRequireReferenceIfValue = export type TFnSecretBulkUpdate = { folderId: string; + orgId: string; inputSecrets: { filter: Partial; - data: TRequireReferenceIfValue & { tags?: string[] }; + data: TRequireReferenceIfValue & { tags?: string[]; secretMetadata?: ResourceMetadataDTO }; }[]; - secretDAL: Pick; + resourceMetadataDAL: Pick; + secretDAL: Pick; secretVersionDAL: Pick; - secretTagDAL: Pick; + secretTagDAL: Pick; secretVersionTagDAL: Pick; + actor?: { + type: string; + actorId: string; + }; tx?: Knex; }; @@ -233,6 +279,13 @@ export type TUpdateManySecretsFnFactory = { folderDAL: TSecretFolderDALFactory; }; +export type TFindByFolderIdDALDTO = { + folderId: string; + userId?: string; + tx?: Knex; + projectId: string; +}; + export type TUpdateManySecretsFn = { projectId: string; environment: string; @@ -293,7 +346,9 @@ export type TFindSecretsByFolderIdsFilter = { orderDirection?: OrderByDirection; search?: string; tagSlugs?: string[]; + metadataFilter?: { key?: string; value?: string }[]; includeTagsInSearch?: boolean; + keys?: string[]; }; export type TGetSecretsRawByFolderMappingsDTO = { @@ -301,4 +356,13 @@ export type TGetSecretsRawByFolderMappingsDTO = { folderMappings: { folderId: string; path: string; environment: string }[]; userId: string; filters: TFindSecretsByFolderIdsFilter; + filterByAction?: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue; }; + +export type TGetAccessibleSecretsDTO = { + environment: string; + projectId: string; + secretPath: string; + recursive?: boolean; + filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue; +} & TProjectPermission; diff --git a/backend/src/services/secret-v2-bridge/secret-version-dal.ts b/backend/src/services/secret-v2-bridge/secret-version-dal.ts index a0bce5371..b54b073a6 100644 --- a/backend/src/services/secret-v2-bridge/secret-version-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-version-dal.ts @@ -1,9 +1,10 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas"; +import { SecretVersionsV2Schema, TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas"; import { BadRequestError, DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex"; import { logger } from "@app/lib/logger"; import { QueueName } from "@app/queue"; @@ -12,6 +13,58 @@ export type TSecretVersionV2DALFactory = ReturnType { const secretVersionV2Orm = ormify(db, TableName.SecretVersionV2); + const findBySecretId = async (secretId: string, { offset, limit, sort, tx }: TFindOpt = {}) => { + try { + const query = (tx || db.replicaNode())(TableName.SecretVersionV2) + .where(`${TableName.SecretVersionV2}.secretId`, secretId) + .leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`) + .leftJoin( + TableName.SecretV2JnTag, + `${TableName.SecretV2}.id`, + `${TableName.SecretV2JnTag}.${TableName.SecretV2}Id` + ) + .leftJoin( + TableName.SecretTag, + `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, + `${TableName.SecretTag}.id` + ) + .select(selectAllTableCols(TableName.SecretVersionV2)) + .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) + .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); + + if (limit) void query.limit(limit); + if (offset) void query.offset(offset); + if (sort) { + void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); + } + + const docs = await query; + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => ({ _id: el.id, ...SecretVersionsV2Schema.parse(el) }), + childrenMapper: [ + { + key: "tagId", + label: "tags" as const, + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ + id, + color, + slug, + name: slug + }) + } + ] + }); + + return data; + } catch (error) { + throw new DatabaseError({ error, name: `${TableName.SecretVersionV2}: FindBySecretId` }); + } + }; + // This will fetch all latest secret versions from a folder const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => { try { @@ -20,7 +73,8 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { .join(TableName.SecretV2, `${TableName.SecretV2}.id`, `${TableName.SecretVersionV2}.secretId`) .join( (tx || db)(TableName.SecretVersionV2) - .groupBy("folderId", "secretId") + .where(`${TableName.SecretVersionV2}.folderId`, folderId) + .groupBy("secretId") .max("version") .select("secretId") .as("latestVersion"), @@ -118,11 +172,101 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v2 completed`); }; + const findVersionsBySecretIdWithActors = async ( + secretId: string, + projectId: string, + { offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt = {}, + tx?: Knex + ) => { + try { + const query = (tx || db)(TableName.SecretVersionV2) + .leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SecretVersionV2}.userActorId`) + .leftJoin( + TableName.ProjectMembership, + `${TableName.ProjectMembership}.userId`, + `${TableName.SecretVersionV2}.userActorId` + ) + .leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`) + .leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`) + .leftJoin( + TableName.SecretV2JnTag, + `${TableName.SecretV2}.id`, + `${TableName.SecretV2JnTag}.${TableName.SecretV2}Id` + ) + .leftJoin( + TableName.SecretTag, + `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, + `${TableName.SecretTag}.id` + ) + .where((qb) => { + void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId); + void qb.where(`${TableName.ProjectMembership}.projectId`, projectId); + }) + .orWhere((qb) => { + void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId); + void qb.whereNull(`${TableName.ProjectMembership}.projectId`); + }) + .select( + selectAllTableCols(TableName.SecretVersionV2), + db.ref("username").withSchema(TableName.Users).as("userActorName"), + db.ref("name").withSchema(TableName.Identity).as("identityActorName"), + db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"), + db.ref("id").withSchema(TableName.SecretTag).as("tagId"), + db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), + db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug") + ); + + if (limit) void query.limit(limit); + if (offset) void query.offset(offset); + if (sort) { + void query.orderBy( + sort.map(([column, order, nulls]) => ({ + column: `${TableName.SecretVersionV2}.${column as string}`, + order, + nulls + })) + ); + } + + const docs = await query; + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => ({ + _id: el.id, + ...SecretVersionsV2Schema.parse(el), + userActorName: el.userActorName, + identityActorName: el.identityActorName, + membershipId: el.membershipId + }), + childrenMapper: [ + { + key: "tagId", + label: "tags" as const, + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ + id, + color, + slug, + name: slug + }) + } + ] + }); + + return data; + } catch (error) { + throw new DatabaseError({ error, name: "FindVersionsBySecretIdWithActors" }); + } + }; + return { ...secretVersionV2Orm, pruneExcessVersions, findLatestVersionMany, bulkUpdate, - findLatestVersionByFolderId + findLatestVersionByFolderId, + findVersionsBySecretIdWithActors, + findBySecretId }; }; diff --git a/backend/src/services/secret/secret-dal.ts b/backend/src/services/secret/secret-dal.ts index 0d4ae0cda..dc41d129f 100644 --- a/backend/src/services/secret/secret-dal.ts +++ b/backend/src/services/secret/secret-dal.ts @@ -5,6 +5,8 @@ import { TDbClient } from "@app/db"; import { SecretsSchema, SecretType, TableName, TSecrets, TSecretsUpdate } from "@app/db/schemas"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; +import { logger } from "@app/lib/logger"; +import { QueueName, TQueueServiceFactory } from "@app/queue"; export type TSecretDALFactory = ReturnType; @@ -167,6 +169,48 @@ export const secretDALFactory = (db: TDbClient) => { } }; + const findManySecretsWithTags = async ( + filter: { + secretIds: string[]; + type: SecretType; + }, + tx?: Knex + ) => { + try { + const secrets = await (tx || db.replicaNode())(TableName.Secret) + .whereIn(`${TableName.Secret}.id` as "id", filter.secretIds) + .where("type", filter.type) + .leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`) + .leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`) + .select(selectAllTableCols(TableName.Secret)) + .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) + .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); + + const data = sqlNestRelationships({ + data: secrets, + key: "id", + parentMapper: (el) => ({ _id: el.id, ...SecretsSchema.parse(el) }), + childrenMapper: [ + { + key: "tagId", + label: "tags" as const, + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ + id, + color, + slug, + name: slug + }) + } + ] + }); + + return data; + } catch (error) { + throw new DatabaseError({ error, name: "get many secrets with tags" }); + } + }; + const findByFolderIds = async (folderIds: string[], userId?: string, tx?: Knex) => { try { // check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo) @@ -339,6 +383,94 @@ export const secretDALFactory = (db: TDbClient) => { } }; + const pruneSecretReminders = async (queueService: TQueueServiceFactory) => { + const REMINDER_PRUNE_BATCH_SIZE = 5_000; + const MAX_RETRY_ON_FAILURE = 3; + let numberOfRetryOnFailure = 0; + let deletedReminderCount = 0; + + logger.info(`${QueueName.DailyResourceCleanUp}: secret reminders started`); + + try { + const repeatableJobs = await queueService.getRepeatableJobs(QueueName.SecretReminder); + const reminderJobs = repeatableJobs + .map((job) => ({ secretId: job.id?.replace("reminder-", "") as string, jobKey: job.key })) + .filter(Boolean); + + if (reminderJobs.length === 0) { + logger.info(`${QueueName.DailyResourceCleanUp}: no reminder jobs found`); + return; + } + + for (let offset = 0; offset < reminderJobs.length; offset += REMINDER_PRUNE_BATCH_SIZE) { + try { + const batchIds = reminderJobs.slice(offset, offset + REMINDER_PRUNE_BATCH_SIZE).map((r) => r.secretId); + + const payload = { + $in: { + id: batchIds + } + }; + + const opts = { + limit: REMINDER_PRUNE_BATCH_SIZE + }; + + // Find existing secrets with pagination + // eslint-disable-next-line no-await-in-loop + const [secrets, secretsV2] = await Promise.all([ + ormify(db, TableName.Secret).find(payload, opts), + ormify(db, TableName.SecretV2).find(payload, opts) + ]); + + const foundSecretIds = new Set([ + ...secrets.map((secret) => secret.id), + ...secretsV2.map((secret) => secret.id) + ]); + + // Find IDs that don't exist in either table + const secretIdsNotFound = batchIds.filter((secretId) => !foundSecretIds.has(secretId)); + + // Delete reminders for non-existent secrets + for (const secretId of secretIdsNotFound) { + const jobKey = reminderJobs.find((r) => r.secretId === secretId)?.jobKey; + + if (jobKey) { + // eslint-disable-next-line no-await-in-loop + await queueService.stopRepeatableJobByKey(QueueName.SecretReminder, jobKey); + deletedReminderCount += 1; + } + } + + numberOfRetryOnFailure = 0; + } catch (error) { + numberOfRetryOnFailure += 1; + logger.error(error, `Failed to process batch at offset ${offset}`); + + if (numberOfRetryOnFailure >= MAX_RETRY_ON_FAILURE) { + break; + } + + // Retry the current batch + offset -= REMINDER_PRUNE_BATCH_SIZE; + + // eslint-disable-next-line no-promise-executor-return, @typescript-eslint/no-loop-func, no-await-in-loop + await new Promise((resolve) => setTimeout(resolve, 500 * numberOfRetryOnFailure)); + } + + // Small delay between batches + // eslint-disable-next-line no-promise-executor-return, @typescript-eslint/no-loop-func, no-await-in-loop + await new Promise((resolve) => setTimeout(resolve, 10)); + } + } catch (error) { + logger.error(error, "Failed to complete secret reminder pruning"); + } finally { + logger.info( + `${QueueName.DailyResourceCleanUp}: secret reminders completed. Deleted ${deletedReminderCount} reminders` + ); + } + }; + return { ...secretOrm, update, @@ -352,6 +484,8 @@ export const secretDALFactory = (db: TDbClient) => { findByBlindIndexes, upsertSecretReferences, findReferencedSecretReferences, - findAllProjectSecretValues + findAllProjectSecretValues, + pruneSecretReminders, + findManySecretsWithTags }; }; diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index 65691fcbb..f08a5a04c 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -1,8 +1,9 @@ /* eslint-disable no-await-in-loop */ -import { subject } from "@casl/ability"; import path from "path"; +import RE2 from "re2"; import { + ActionProjectType, SecretEncryptionAlgo, SecretKeyEncoding, SecretType, @@ -11,17 +12,20 @@ import { TSecretFolders, TSecrets } from "@app/db/schemas"; +import { hasSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { buildSecretBlindIndexFromName, decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; +import { daysToMillisecond, secondsToMillis } from "@app/lib/dates"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { groupBy, unique } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; +import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { fnSecretBulkInsert as fnSecretV2BridgeBulkInsert, fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate, @@ -31,8 +35,10 @@ import { import { ActorAuthMethod, ActorType } from "../auth/auth-type"; import { KmsDataKey } from "../kms/kms-types"; import { getBotKeyFnFactory } from "../project-bot/project-bot-fns"; +import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; +import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; import { TSecretDALFactory } from "./secret-dal"; import { TCreateManySecretsRawFn, @@ -46,6 +52,8 @@ import { TUpdateManySecretsRawFnFactory } from "./secret-types"; +export const INFISICAL_SECRET_VALUE_HIDDEN_MASK = ""; + export const generateSecretBlindIndexBySalt = async (secretName: string, secretBlindIndexDoc: TSecretBlindIndexes) => { const appCfg = getConfig(); const secretBlindIndex = await buildSecretBlindIndexFromName({ @@ -172,24 +180,22 @@ export const recursivelyGetSecretPaths = ({ folderId: p.folderId })); - const { permission } = await permissionService.getProjectPermission( - auth.actor, - auth.actorId, + const { permission } = await permissionService.getProjectPermission({ + actor: auth.actor, + actorId: auth.actorId, projectId, - auth.actorAuthMethod, - auth.actorOrgId - ); + actorAuthMethod: auth.actorAuthMethod, + actorOrgId: auth.actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); // Filter out paths that the user does not have permission to access, and paths that are not in the current path const allowedPaths = paths.filter( (folder) => - permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment, - secretPath: folder.path - }) - ) && folder.path.startsWith(currentPath === "/" ? "" : currentPath) + hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: folder.path + }) && folder.path.startsWith(currentPath === "/" ? "" : currentPath) ); return allowedPaths; @@ -202,7 +208,7 @@ export const recursivelyGetSecretPaths = ({ const formatMultiValueEnv = (val?: string) => { if (!val) return ""; if (!val.match("\n")) return val; - return `"${val.replace(/\n/g, "\\n")}"`; + return `"${val.replaceAll("\n", "\\n")}"`; }; type TInterpolateSecretArg = { @@ -213,7 +219,9 @@ type TInterpolateSecretArg = { }; const MAX_SECRET_REFERENCE_DEPTH = 5; -const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/g; +const INTERPOLATION_PATTERN_STRING = String.raw`\${([a-zA-Z0-9-_.]+)}`; +const INTERPOLATION_TEST_REGEX = new RE2(INTERPOLATION_PATTERN_STRING); + export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderDAL }: TInterpolateSecretArg) => { const secretCache: Record> = {}; const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`; @@ -268,9 +276,17 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD if (!value) return ""; if (depth > MAX_SECRET_REFERENCE_DEPTH) return ""; - const refs = value.match(INTERPOLATION_SYNTAX_REG); + const refs = []; + let match; + const execRegex = new RE2(INTERPOLATION_PATTERN_STRING, "g"); + + // eslint-disable-next-line no-cond-assign + while ((match = execRegex.exec(value)) !== null) { + refs.push(match[0]); + } + let expandedValue = value; - if (refs) { + if (refs.length > 0) { for (const interpolationSyntax of refs) { const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1); const entities = interpolationKey.trim().split("."); @@ -279,7 +295,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD const [secretKey] = entities; // eslint-disable-next-line let referenceValue = await fetchSecret(environment, secretPath, secretKey); - if (INTERPOLATION_SYNTAX_REG.test(referenceValue)) { + if (INTERPOLATION_TEST_REGEX.test(referenceValue)) { // eslint-disable-next-line referenceValue = await recursivelyExpandSecret({ environment, @@ -300,7 +316,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD // eslint-disable-next-line let referenceValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey); - if (INTERPOLATION_SYNTAX_REG.test(referenceValue)) { + if (INTERPOLATION_TEST_REGEX.test(referenceValue)) { // eslint-disable-next-line referenceValue = await recursivelyExpandSecret({ environment: secretReferenceEnvironment, @@ -327,7 +343,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD }) => { if (!inputSecret.value) return inputSecret.value; - const shouldExpand = Boolean(inputSecret.value?.match(INTERPOLATION_SYNTAX_REG)); + const shouldExpand = INTERPOLATION_TEST_REGEX.test(inputSecret.value); if (!shouldExpand) return inputSecret.value; const expandedSecretValue = await recursivelyExpandSecret(inputSecret); @@ -338,6 +354,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD export const decryptSecretRaw = ( secret: TSecrets & { + secretValueHidden: boolean; workspace: string; environment: string; secretPath: string; @@ -356,12 +373,14 @@ export const decryptSecretRaw = ( key }); - const secretValue = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secret.secretValueCiphertext, - iv: secret.secretValueIV, - tag: secret.secretValueTag, - key - }); + const secretValue = !secret.secretValueHidden + ? decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key + }) + : INFISICAL_SECRET_VALUE_HIDDEN_MASK; let secretComment = ""; @@ -379,6 +398,7 @@ export const decryptSecretRaw = ( secretPath: secret.secretPath, workspace: secret.workspace, environment: secret.environment, + secretValueHidden: secret.secretValueHidden, secretValue, secretComment, version: secret.version, @@ -442,7 +462,18 @@ export const fnSecretBlindIndexCheckV2 = async ({ * // ] */ export const getAllNestedSecretReferences = (maybeSecretReference: string) => { - const references = Array.from(maybeSecretReference.matchAll(INTERPOLATION_SYNTAX_REG), (m) => m[1]); + const matches = []; + let match; + + const execRegex = new RE2(INTERPOLATION_PATTERN_STRING, "g"); + + // eslint-disable-next-line no-cond-assign + while ((match = execRegex.exec(maybeSecretReference)) !== null) { + matches.push(match); + } + + const references = matches.map((m) => m[1]); + return references .filter((el) => el.includes(".")) .map((el) => { @@ -573,6 +604,7 @@ export const fnSecretBulkInsert = async ({ [`${TableName.Secret}Id` as const]: newSecretGroupByBlindIndex[secretBlindIndex as string][0].id })) ); + const secretVersions = await secretVersionDAL.insertMany( sanitizedInputSecrets.map((el) => ({ ...el, @@ -745,7 +777,8 @@ export const createManySecretsRawFnFactory = ({ secretVersionV2BridgeDAL, secretV2BridgeDAL, secretVersionTagV2BridgeDAL, - kmsService + kmsService, + resourceMetadataDAL }: TCreateManySecretsRawFnFactory) => { const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL); const createManySecretsRawFn = async ({ @@ -756,7 +789,7 @@ export const createManySecretsRawFnFactory = ({ userId }: TCreateManySecretsRawFn) => { const { botKey, shouldUseSecretV2Bridge } = await getBotKeyFn(projectId); - + const project = await projectDAL.findById(projectId); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) throw new NotFoundError({ @@ -810,7 +843,9 @@ export const createManySecretsRawFnFactory = ({ tagIds: el.tags })), folderId, + orgId: project.orgId, secretDAL: secretV2BridgeDAL, + resourceMetadataDAL, secretVersionDAL: secretVersionV2BridgeDAL, secretTagDAL, secretVersionTagDAL: secretVersionTagV2BridgeDAL, @@ -905,6 +940,7 @@ export const updateManySecretsRawFnFactory = ({ secretVersionTagV2BridgeDAL, secretVersionV2BridgeDAL, secretV2BridgeDAL, + resourceMetadataDAL, kmsService }: TUpdateManySecretsRawFnFactory) => { const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL); @@ -916,6 +952,7 @@ export const updateManySecretsRawFnFactory = ({ userId }: TUpdateManySecretsRawFn): Promise> => { const { botKey, shouldUseSecretV2Bridge } = await getBotKeyFn(projectId); + const project = await projectDAL.findById(projectId); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -984,11 +1021,13 @@ export const updateManySecretsRawFnFactory = ({ const updatedSecrets = await secretDAL.transaction(async (tx) => fnSecretV2BridgeBulkUpdate({ folderId, + orgId: project.orgId, tx, inputSecrets: inputSecrets.map((el) => ({ filter: { id: secretsToUpdateInDBGroupedByKey[el.key][0].id, type: SecretType.Shared }, data: el })), + resourceMetadataDAL, secretDAL: secretV2BridgeDAL, secretVersionDAL: secretVersionV2BridgeDAL, secretTagDAL, @@ -1138,3 +1177,69 @@ export const decryptSecretWithBot = ( secretComment }; }; + +type TFnDeleteProjectSecretReminders = { + secretDAL: Pick; + secretV2BridgeDAL: Pick; + queueService: Pick; + projectBotService: Pick; + folderDAL: Pick; +}; + +export const fnDeleteProjectSecretReminders = async ( + projectId: string, + { secretDAL, secretV2BridgeDAL, queueService, projectBotService, folderDAL }: TFnDeleteProjectSecretReminders +) => { + const projectFolders = await folderDAL.findByProjectId(projectId); + const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId, false); + + const projectSecrets = shouldUseSecretV2Bridge + ? await secretV2BridgeDAL.find({ + $in: { folderId: projectFolders.map((folder) => folder.id) }, + $notNull: ["reminderRepeatDays"] + }) + : await secretDAL.find({ + $in: { folderId: projectFolders.map((folder) => folder.id) }, + $notNull: ["secretReminderRepeatDays"] + }); + + const appCfg = getConfig(); + for await (const secret of projectSecrets) { + const repeatDays = shouldUseSecretV2Bridge + ? (secret as { reminderRepeatDays: number }).reminderRepeatDays + : (secret as { secretReminderRepeatDays: number }).secretReminderRepeatDays; + + // We're using the queue service directly to get around conflicting imports. + if (repeatDays) { + await queueService.stopRepeatableJob( + QueueName.SecretReminder, + QueueJobs.SecretReminder, + { + // on prod it this will be in days, in development this will be second + every: appCfg.NODE_ENV === "development" ? secondsToMillis(repeatDays) : daysToMillisecond(repeatDays) + }, + `reminder-${secret.id}` + ); + } + } +}; + +export const conditionallyHideSecretValue = ( + shouldHideValue: boolean, + { + secretValueCiphertext, + secretValueIV, + secretValueTag + }: { + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + } +) => { + return { + secretValueCiphertext: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueCiphertext, + secretValueIV: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueIV, + secretValueTag: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueTag, + secretValueHidden: shouldHideValue + }; +}; diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 3d75fa4f8..5791c415d 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -1,4 +1,5 @@ /* eslint-disable no-await-in-loop */ +import opentelemetry from "@opentelemetry/api"; import { AxiosError } from "axios"; import { @@ -28,6 +29,7 @@ import { createManySecretsRawFnFactory, updateManySecretsRawFnFactory } from "@a import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal"; import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal"; import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal"; +import { TSecretSyncQueueFactory } from "@app/services/secret-sync/secret-sync-queue"; import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; import { ActorType } from "../auth/auth-type"; @@ -46,6 +48,8 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; +import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; +import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns"; @@ -57,6 +61,7 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TUserDALFactory } from "../user/user-dal"; import { TWebhookDALFactory } from "../webhook/webhook-dal"; import { fnTriggerWebhook } from "../webhook/webhook-fns"; +import { WebhookEvents } from "../webhook/webhook-types"; import { TSecretDALFactory } from "./secret-dal"; import { interpolateSecrets } from "./secret-fns"; import { @@ -103,6 +108,8 @@ type TSecretQueueFactoryDep = { auditLogService: Pick; orgService: Pick; projectUserMembershipRoleDAL: Pick; + resourceMetadataDAL: Pick; + secretSyncQueue: Pick; }; export type TGetSecrets = { @@ -119,7 +126,12 @@ export const uniqueSecretQueueKey = (environment: string, secretPath: string) => type TIntegrationSecret = Record< string, - { value: string; comment?: string; skipMultilineEncoding?: boolean | null | undefined } + { + value: string; + comment?: string; + skipMultilineEncoding?: boolean | null | undefined; + secretMetadata?: ResourceMetadataDTO; + } >; // TODO(akhilmhdh): split this into multiple queue @@ -156,8 +168,16 @@ export const secretQueueFactory = ({ auditLogService, orgService, projectUserMembershipRoleDAL, - projectKeyDAL + projectKeyDAL, + resourceMetadataDAL, + secretSyncQueue }: TSecretQueueFactoryDep) => { + const integrationMeter = opentelemetry.metrics.getMeter("Integrations"); + const errorHistogram = integrationMeter.createHistogram("integration_secret_sync_errors", { + description: "Integration secret sync errors", + unit: "1" + }); + const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => { const appCfg = getConfig(); await queueService.stopRepeatableJob( @@ -248,7 +268,9 @@ export const secretQueueFactory = ({ ? secondsToMillis(newSecret.secretReminderRepeatDays) : daysToMillisecond(newSecret.secretReminderRepeatDays), immediately: true - } + }, + removeOnComplete: true, + removeOnFail: true } ); } catch (err) { @@ -297,7 +319,8 @@ export const secretQueueFactory = ({ kmsService, secretVersionV2BridgeDAL, secretV2BridgeDAL, - secretVersionTagV2BridgeDAL + secretVersionTagV2BridgeDAL, + resourceMetadataDAL }); const updateManySecretsRawFn = updateManySecretsRawFnFactory({ @@ -312,7 +335,8 @@ export const secretQueueFactory = ({ kmsService, secretVersionV2BridgeDAL, secretV2BridgeDAL, - secretVersionTagV2BridgeDAL + secretVersionTagV2BridgeDAL, + resourceMetadataDAL }); /** @@ -343,7 +367,7 @@ export const secretQueueFactory = ({ canExpandValue: () => true }); // process secrets in current folder - const secrets = await secretV2BridgeDAL.findByFolderId(dto.folderId); + const secrets = await secretV2BridgeDAL.findByFolderId({ folderId: dto.folderId }); await Promise.allSettled( secrets.map(async (secret) => { @@ -363,6 +387,7 @@ export const secretQueueFactory = ({ } content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding); + content[secretKey].secretMetadata = secret.secretMetadata; }) ); @@ -378,7 +403,8 @@ export const secretQueueFactory = ({ expandSecretReferences, secretImportDAL, secretImports, - hasSecretAccess: () => true + hasSecretAccess: () => true, + viewSecretValue: true }); for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { @@ -388,7 +414,8 @@ export const secretQueueFactory = ({ content[importedSecret.key] = { skipMultilineEncoding: importedSecret.skipMultilineEncoding, comment: importedSecret.secretComment, - value: importedSecret.secretValue || "" + value: importedSecret.secretValue || "", + secretMetadata: importedSecret.secretMetadata }; } } @@ -588,6 +615,7 @@ export const secretQueueFactory = ({ _depth: depth, secretPath, projectId, + orgId, environmentSlug: environment, excludeReplication, actorId, @@ -597,7 +625,14 @@ export const secretQueueFactory = ({ await queueService.queue( QueueName.SecretWebhook, QueueJobs.SecWebhook, - { environment, projectId, secretPath }, + { + type: WebhookEvents.SecretModified, + payload: { + environment, + projectId, + secretPath + } + }, { jobId: `secret-webhook-${environment}-${projectId}-${secretPath}`, removeOnFail: { count: 5 }, @@ -610,12 +645,20 @@ export const secretQueueFactory = ({ } } ); + + const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); + if (!folder) return; + await folderDAL.updateById(folder.id, { lastSecretModified: new Date() }); + + await secretSyncQueue.queueSecretSyncsSyncSecretsByPath({ projectId, environmentSlug: environment, secretPath }); + await syncIntegrations({ secretPath, projectId, environment, deDupeQueue, isManual: false }); if (!excludeReplication) { await replicateSecrets({ _deDupeReplicationQueue: deDupeReplicationQueue, _depth: depth, projectId, + orgId, secretPath, actorId, actor, @@ -672,6 +715,7 @@ export const secretQueueFactory = ({ if (!folder) { throw new Error("Secret path not found"); } + const project = await projectDAL.findById(projectId); // find all imports made with the given environment and secret path const linkSourceDto = { @@ -706,6 +750,7 @@ export const secretQueueFactory = ({ .map(({ folderId }) => syncSecrets({ projectId, + orgId: project.orgId, secretPath: foldersGroupedById[folderId][0]?.path as string, environmentSlug: foldersGroupedById[folderId][0]?.environmentSlug as string, _deDupeQueue: deDupeQueue, @@ -758,6 +803,7 @@ export const secretQueueFactory = ({ .map((folderId) => syncSecrets({ projectId, + orgId: project.orgId, secretPath: referencedFoldersGroupedById[folderId][0]?.path as string, environmentSlug: referencedFoldersGroupedById[folderId][0]?.environmentSlug as string, _deDupeQueue: deDupeQueue, @@ -931,9 +977,30 @@ export const secretQueueFactory = ({ `Secret integration sync error [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}]` ); + const appCfg = getConfig(); + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + errorHistogram.record(1, { + version: 1, + integration: integration.integration, + integrationId: integration.id, + type: err instanceof AxiosError ? "AxiosError" : err?.constructor?.name || "UnknownError", + status: err instanceof AxiosError ? err.response?.status : undefined, + name: err instanceof Error ? err.name : undefined, + projectId: integration.projectId + }); + } + + const { secretKey } = (err as { secretKey: string }) || {}; + const message = - (err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) || - "Unknown error occurred."; + // eslint-disable-next-line no-nested-ternary + (err instanceof AxiosError + ? err?.response?.data + ? JSON.stringify(err?.response?.data) + : err?.message + : (err as Error)?.message) || "Unknown error occurred."; + + const errorLog = `${secretKey ? `[Secret Key: ${secretKey}] ` : ""}${message}`; await auditLogService.createAuditLog({ projectId, @@ -945,7 +1012,7 @@ export const secretQueueFactory = ({ isSynced: false, lastSyncJobId: job?.id ?? "", lastUsed: new Date(), - syncMessage: message + syncMessage: errorLog } } }); @@ -957,13 +1024,13 @@ export const secretQueueFactory = ({ await integrationDAL.updateById(integration.id, { lastSyncJobId: job.id, - syncMessage: message, + syncMessage: errorLog, isSynced: false }); integrationsFailedToSync.push({ integrationId: integration.id, - syncMessage: message + syncMessage: errorLog }); } } @@ -1001,6 +1068,8 @@ export const secretQueueFactory = ({ const organization = await orgDAL.findOrgByProjectId(projectId); const project = await projectDAL.findById(projectId); + const secret = await secretV2BridgeDAL.findById(data.secretId); + const [folder] = await folderDAL.findSecretPathByFolderIds(project.id, [secret.folderId]); if (!organization) { logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}] no organization found`); @@ -1029,6 +1098,19 @@ export const secretQueueFactory = ({ organizationName: organization.name } }); + + await queueService.queue(QueueName.SecretWebhook, QueueJobs.SecWebhook, { + type: WebhookEvents.SecretReminderExpired, + payload: { + projectName: project.name, + projectId: project.id, + secretPath: folder?.path, + environment: folder?.environmentSlug || "", + reminderNote: data.note, + secretName: secret?.key, + secretId: data.secretId + } + }); }); const startSecretV2Migration = async (projectId: string) => { @@ -1434,7 +1516,21 @@ export const secretQueueFactory = ({ }); queueService.start(QueueName.SecretWebhook, async (job) => { - await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL }); + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: job.data.payload.projectId + }); + + await fnTriggerWebhook({ + projectId: job.data.payload.projectId, + environment: job.data.payload.environment, + secretPath: job.data.payload.secretPath || "/", + projectEnvDAL, + projectDAL, + webhookDAL, + event: job.data, + secretManagerDecryptor: (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString() + }); }); return { diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index d62d09f7a..a82b04833 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -3,15 +3,26 @@ import { ForbiddenError, subject } from "@casl/ability"; import { + ActionProjectType, ProjectMembershipRole, ProjectUpgradeStatus, + ProjectVersion, SecretEncryptionAlgo, SecretKeyEncoding, SecretsSchema, SecretType } from "@app/db/schemas"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { + hasSecretReadValueOrDescribePermission, + throwIfMissingSecretReadValueOrDescribePermission +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal"; @@ -28,7 +39,10 @@ import { groupBy, pick } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { OrgServiceActor } from "@app/lib/types"; -import { TGetSecretsRawByFolderMappingsDTO } from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; +import { + SecretUpdateMode, + TGetSecretsRawByFolderMappingsDTO +} from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; import { ActorType } from "../auth/auth-type"; import { TProjectDALFactory } from "../project/project-dal"; @@ -43,6 +57,7 @@ import { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bri import { TGetSecretReferencesTreeDTO } from "../secret-v2-bridge/secret-v2-bridge-types"; import { TSecretDALFactory } from "./secret-dal"; import { + conditionallyHideSecretValue, decryptSecretRaw, fnSecretBlindIndexCheck, fnSecretBulkDelete, @@ -66,8 +81,11 @@ import { TDeleteManySecretRawDTO, TDeleteSecretDTO, TDeleteSecretRawDTO, + TGetAccessibleSecretsDTO, + TGetASecretByIdRawDTO, TGetASecretDTO, TGetASecretRawDTO, + TGetSecretAccessListDTO, TGetSecretsDTO, TGetSecretsRawDTO, TGetSecretVersionsDTO, @@ -85,15 +103,15 @@ type TSecretServiceFactoryDep = { secretDAL: TSecretDALFactory; secretTagDAL: TSecretTagDALFactory; secretVersionDAL: TSecretVersionDALFactory; - projectDAL: Pick; + projectDAL: Pick; projectEnvDAL: Pick; folderDAL: Pick< TSecretFolderDALFactory, - "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" + "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findSecretPathByFolderIds" >; secretV2BridgeService: TSecretV2BridgeServiceFactory; secretBlindIndexDAL: TSecretBlindIndexDALFactory; - permissionService: Pick; + permissionService: Pick; snapshotService: Pick; secretQueueService: Pick< TSecretQueueFactory, @@ -112,6 +130,7 @@ type TSecretServiceFactoryDep = { TSecretApprovalRequestSecretDALFactory, "insertMany" | "insertApprovalSecretTags" >; + licenseService: Pick; }; export type TSecretServiceFactory = ReturnType; @@ -133,7 +152,8 @@ export const secretServiceFactory = ({ secretApprovalRequestDAL, secretApprovalRequestSecretDAL, secretV2BridgeService, - secretApprovalRequestService + secretApprovalRequestService, + licenseService }: TSecretServiceFactoryDep) => { const getSecretReference = async (projectId: string) => { // if bot key missing means e2e still exist @@ -186,15 +206,17 @@ export const secretServiceFactory = ({ projectId, ...inputSecret }: TCreateSecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -285,6 +307,7 @@ export const secretServiceFactory = ({ actorId, actor, projectId, + orgId: actorOrgId, environmentSlug: folder.environment.slug }); } @@ -301,15 +324,17 @@ export const secretServiceFactory = ({ projectId, ...inputSecret }: TUpdateSecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -424,13 +449,30 @@ export const secretServiceFactory = ({ await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ secretPath: path, + orgId: actorOrgId, actorId, actor, projectId, environmentSlug: folder.environment.slug }); } - return { ...updatedSecret[0], workspace: projectId, environment, secretPath: path }; + + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath: path + } + ); + + return { + ...updatedSecret[0], + ...conditionallyHideSecretValue(secretValueHidden, updatedSecret[0]), + workspace: projectId, + environment, + secretPath: path + }; }; const deleteSecret = async ({ @@ -443,15 +485,17 @@ export const secretServiceFactory = ({ projectId, ...inputSecret }: TDeleteSecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, + ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -484,8 +528,8 @@ export const secretServiceFactory = ({ secretDAL }); - const deletedSecret = await secretDAL.transaction(async (tx) => - fnSecretBulkDelete({ + const deletedSecret = await secretDAL.transaction(async (tx) => { + const secrets = await fnSecretBulkDelete({ projectId, folderId, actorId, @@ -498,8 +542,19 @@ export const secretServiceFactory = ({ } ], tx - }) - ); + }); + + for await (const secret of secrets) { + if (secret.secretReminderRepeatDays !== null && secret.secretReminderRepeatDays !== undefined) { + await secretQueueService.removeSecretReminder({ + repeatDays: secret.secretReminderRepeatDays, + secretId: secret.id + }); + } + } + + return secrets; + }); if (inputSecret.type === SecretType.Shared) { await snapshotService.performSnapshot(folderId); @@ -508,11 +563,28 @@ export const secretServiceFactory = ({ actorId, actor, projectId, + orgId: actorOrgId, environmentSlug: folder.environment.slug }); } - return { ...deletedSecret[0], _id: deletedSecret[0].id, workspace: projectId, environment, secretPath: path }; + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath: path + } + ); + + return { + ...deletedSecret[0], + ...conditionallyHideSecretValue(secretValueHidden, deletedSecret[0]), + _id: deletedSecret[0].id, + workspace: projectId, + environment, + secretPath: path + }; }; const getSecrets = async ({ @@ -526,13 +598,14 @@ export const secretServiceFactory = ({ includeImports, recursive }: TGetSecretsDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); let paths: { folderId: string; path: string }[] = []; @@ -559,10 +632,10 @@ export const secretServiceFactory = ({ paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p })); } else { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: path + }); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) return { secrets: [], imports: [] }; @@ -584,13 +657,10 @@ export const secretServiceFactory = ({ // if its service token allow full access over imported one actor === ActorType.SERVICE ? true - : permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: importEnv.slug, - secretPath: importPath - }) - ) + : hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment: importEnv.slug, + secretPath: importPath + }) ); const importedSecrets = await fnSecretsFromImports({ allowedImports, @@ -633,17 +703,19 @@ export const secretServiceFactory = ({ version, includeImports }: TGetASecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: path + }); + const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) throw new NotFoundError({ @@ -690,14 +762,12 @@ export const secretServiceFactory = ({ // if its service token allow full access over imported one actor === ActorType.SERVICE ? true - : permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: importEnv.slug, - secretPath: importPath - }) - ) + : hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment: importEnv.slug, + secretPath: importPath + }) ); + const importedSecrets = await fnSecretsFromImports({ allowedImports, secretDAL, @@ -709,6 +779,7 @@ export const secretServiceFactory = ({ if (secretBlindIndex === importedSecrets[i].secrets[j].secretBlindIndex) { return { ...importedSecrets[i].secrets[j], + secretValueHidden: false, workspace: projectId, environment: importedSecrets[i].environment, secretPath: importedSecrets[i].secretPath @@ -719,7 +790,13 @@ export const secretServiceFactory = ({ } if (!secret) throw new NotFoundError({ message: `Secret with name '${secretName}' not found` }); - return { ...secret, workspace: projectId, environment, secretPath: path }; + return { + ...secret, + secretValueHidden: false, // Always false because we check permission at the beginning of the function + workspace: projectId, + environment, + secretPath: path + }; }; const createManySecret = async ({ @@ -732,15 +809,16 @@ export const secretServiceFactory = ({ projectId, secrets: inputSecrets }: TCreateBulkSecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -801,6 +879,7 @@ export const secretServiceFactory = ({ actorId, secretPath: path, projectId, + orgId: actorOrgId, environmentSlug: folder.environment.slug }); @@ -817,15 +896,17 @@ export const secretServiceFactory = ({ projectId, secrets: inputSecrets }: TUpdateBulkSecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -867,8 +948,8 @@ export const secretServiceFactory = ({ if (tagIds.length !== tags.length) throw new NotFoundError({ message: "One or more tags not found" }); const references = await getSecretReference(projectId); - const secrets = await secretDAL.transaction(async (tx) => - fnSecretBulkUpdate({ + const secrets = await secretDAL.transaction(async (tx) => { + const updatedSecrets = await fnSecretBulkUpdate({ folderId, projectId, tx, @@ -898,8 +979,22 @@ export const secretServiceFactory = ({ secretVersionDAL, secretTagDAL, secretVersionTagDAL - }) - ); + }); + + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath: path + } + ); + + return updatedSecrets.map((secret) => ({ + ...secret, + ...conditionallyHideSecretValue(secretValueHidden, secret) + })); + }); await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ @@ -907,6 +1002,7 @@ export const secretServiceFactory = ({ actorId, secretPath: path, projectId, + orgId: actorOrgId, environmentSlug: folder.environment.slug }); @@ -923,15 +1019,16 @@ export const secretServiceFactory = ({ actorAuthMethod, actorOrgId }: TDeleteBulkSecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, + ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -960,8 +1057,8 @@ export const secretServiceFactory = ({ secretDAL }); - const secretsDeleted = await secretDAL.transaction(async (tx) => - fnSecretBulkDelete({ + const secretsDeleted = await secretDAL.transaction(async (tx) => { + const secrets = await fnSecretBulkDelete({ secretDAL, secretQueueService, inputSecrets: inputSecrets.map(({ type, secretName }) => ({ @@ -972,8 +1069,30 @@ export const secretServiceFactory = ({ folderId, actorId, tx - }) - ); + }); + + for await (const secret of secrets) { + if (secret.secretReminderRepeatDays !== null && secret.secretReminderRepeatDays !== undefined) { + await secretQueueService.removeSecretReminder({ + repeatDays: secret.secretReminderRepeatDays, + secretId: secret.id + }); + } + } + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath: path + } + ); + + return secrets.map((secret) => ({ + ...secret, + ...conditionallyHideSecretValue(secretValueHidden, secret) + })); + }); await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ @@ -981,6 +1100,7 @@ export const secretServiceFactory = ({ actorId, secretPath: path, projectId, + orgId: actorOrgId, environmentSlug: folder.environment.slug }); @@ -1114,6 +1234,120 @@ export const secretServiceFactory = ({ return secretV2BridgeService.getSecretReferenceTree(dto); }; + const getSecretAccessList = async (dto: TGetSecretAccessListDTO) => { + const { environment, secretPath, secretName, projectId } = dto; + const plan = await licenseService.getPlan(dto.actorOrgId); + if (!plan.secretAccessInsights) { + throw new BadRequestError({ + message: "Failed to fetch secret access list due to plan restriction. Upgrade your plan." + }); + } + + const secret = await secretV2BridgeService.getSecretByName({ + actor: dto.actor, + actorId: dto.actorId, + actorOrgId: dto.actorOrgId, + actorAuthMethod: dto.actorAuthMethod, + projectId, + secretName, + path: secretPath, + environment, + viewSecretValue: false, + type: "shared" + }); + + const { userPermissions, identityPermissions, groupPermissions } = await permissionService.getProjectPermissions( + dto.projectId + ); + + const attachAllowedActions = ( + entityPermission: + | (typeof userPermissions)[number] + | (typeof identityPermissions)[number] + | (typeof groupPermissions)[number] + ) => { + const allowedActions = [ + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.Delete, + ProjectPermissionSecretActions.Create, + ProjectPermissionSecretActions.Edit + ].filter((action) => { + if ( + action === ProjectPermissionSecretActions.DescribeSecret || + action === ProjectPermissionSecretActions.ReadValue + ) { + return hasSecretReadValueOrDescribePermission(entityPermission.permission, action, { + environment, + secretPath, + secretName, + secretTags: secret?.tags?.map((el) => el.slug) + }); + } + + return entityPermission.permission.can( + action, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName, + secretTags: secret?.tags?.map((el) => el.slug) + }) + ); + }); + + return { + ...entityPermission, + allowedActions + }; + }; + + const usersWithAccess = userPermissions.map(attachAllowedActions).filter((user) => user.allowedActions.length > 0); + const identitiesWithAccess = identityPermissions + .map(attachAllowedActions) + .filter((identity) => identity.allowedActions.length > 0); + const groupsWithAccess = groupPermissions + .map(attachAllowedActions) + .filter((group) => group.allowedActions.length > 0); + + return { users: usersWithAccess, identities: identitiesWithAccess, groups: groupsWithAccess }; + }; + + const getAccessibleSecrets = async ({ + projectId, + secretPath, + actor, + actorId, + actorOrgId, + actorAuthMethod, + environment, + filterByAction, + recursive + }: TGetAccessibleSecretsDTO) => { + const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); + + if (!shouldUseSecretV2Bridge) { + throw new BadRequestError({ + message: "Project version does not support this endpoint.", + name: "ProjectVersionNotSupported" + }); + } + + const secrets = await secretV2BridgeService.getAccessibleSecrets({ + projectId, + secretPath, + environment, + filterByAction, + actor, + actorId, + actorOrgId, + actorAuthMethod, + recursive + }); + + return secrets; + }; + const getSecretsRaw = async ({ projectId, path, @@ -1121,11 +1355,13 @@ export const secretServiceFactory = ({ actorId, actorOrgId, actorAuthMethod, + viewSecretValue, environment, includeImports, expandSecretReferences, recursive, tagSlugs = [], + throwOnMissingReadValuePermission = true, ...paramsV2 }: TGetSecretsRawDTO) => { const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); @@ -1136,6 +1372,8 @@ export const secretServiceFactory = ({ actorId, actor, actorOrgId, + viewSecretValue, + throwOnMissingReadValuePermission, environment, path, recursive, @@ -1144,6 +1382,7 @@ export const secretServiceFactory = ({ tagSlugs, ...paramsV2 }); + return { secrets, imports }; } @@ -1153,6 +1392,13 @@ export const secretServiceFactory = ({ name: "bot_not_found_error" }); + if (paramsV2.metadataFilter) { + throw new BadRequestError({ + message: "Please upgrade your project to filter secrets by metadata", + name: "SecretMetadataNotSupported" + }); + } + const { secrets, imports } = await getSecrets({ actorId, projectId, @@ -1165,14 +1411,20 @@ export const secretServiceFactory = ({ recursive }); - const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, botKey)); + const decryptedSecrets = secrets.map((el) => decryptSecretRaw({ ...el, secretValueHidden: false }, botKey)); const filteredSecrets = tagSlugs.length ? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug)))) : decryptedSecrets; const processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => { const decryptedImportSecrets = importedSecrets.map((sec) => decryptSecretRaw( - { ...sec, environment: el.environment, workspace: projectId, secretPath: el.secretPath }, + { + ...sec, + environment: el.environment, + workspace: projectId, + secretPath: el.secretPath, + secretValueHidden: false + }, botKey ) ); @@ -1183,6 +1435,7 @@ export const secretServiceFactory = ({ const importedEntries = decryptedImportSecrets.reduce( ( accum: { + secretValueHidden: boolean; secretKey: string; secretPath: string; workspace: string; @@ -1226,6 +1479,7 @@ export const secretServiceFactory = ({ Object.keys(secretsGroupByPath).map((groupedPath) => Promise.allSettled( secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => { + if (decryptedSecret.secretValueHidden) return; const expandedSecretValue = await expandSecret({ value: decryptedSecret.secretValue, secretPath: groupedPath, @@ -1242,6 +1496,7 @@ export const secretServiceFactory = ({ processedImports.map((processedImport) => Promise.allSettled( processedImport.secrets.map(async (decryptedSecret, index) => { + if (decryptedSecret.secretValueHidden) return; const expandedSecretValue = await expandSecret({ value: decryptedSecret.secretValue, secretPath: path, @@ -1262,11 +1517,24 @@ export const secretServiceFactory = ({ }; }; + const getSecretByIdRaw = async ({ secretId, actorId, actor, actorOrgId, actorAuthMethod }: TGetASecretByIdRawDTO) => { + const secret = await secretV2BridgeService.getSecretById({ + secretId, + actorId, + actor, + actorOrgId, + actorAuthMethod + }); + + return secret; + }; + const getSecretByNameRaw = async ({ type, path, actor, environment, + viewSecretValue, projectId: workspaceId, expandSecretReferences, projectSlug, @@ -1286,9 +1554,11 @@ export const secretServiceFactory = ({ includeImports, actorAuthMethod, path, + viewSecretValue, actorOrgId, actor, actorId, + version, expandSecretReferences, type, secretName @@ -1316,6 +1586,7 @@ export const secretServiceFactory = ({ message: `Project bot for project with ID '${projectId}' not found. Please upgrade your project.`, name: "bot_not_found_error" }); + const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey); if (expandSecretReferences) { @@ -1334,7 +1605,10 @@ export const secretServiceFactory = ({ decryptedSecret.secretValue = expandedSecretValue || ""; } - return decryptedSecret; + return { + secretMetadata: undefined, + ...decryptedSecret + }; }; const createSecretRaw = async ({ @@ -1352,9 +1626,20 @@ export const secretServiceFactory = ({ skipMultilineEncoding, tagIds, secretReminderNote, - secretReminderRepeatDays + secretReminderRepeatDays, + secretMetadata }: TCreateSecretRawDTO) => { const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); + const project = await projectDAL.findById(projectId); + if (project.enforceCapitalization) { + if (secretName !== secretName.toUpperCase()) { + throw new BadRequestError({ + message: + "Secret name must be in UPPERCASE per project requirements. You can disable this requirement in project settings." + }); + } + } + const policy = actor === ActorType.USER && type === SecretType.Shared ? await secretApprovalPolicyService.getSecretApprovalPolicy(projectId, environment, secretPath) @@ -1379,7 +1664,8 @@ export const secretServiceFactory = ({ secretValue, tagIds, reminderNote: secretReminderNote, - reminderRepeatDays: secretReminderRepeatDays + reminderRepeatDays: secretReminderRepeatDays, + secretMetadata } ] } @@ -1402,7 +1688,8 @@ export const secretServiceFactory = ({ tagIds, secretReminderNote, skipMultilineEncoding, - secretReminderRepeatDays + secretReminderRepeatDays, + secretMetadata }); return { secret, type: SecretProtectionType.Direct as const }; } @@ -1472,7 +1759,16 @@ export const secretServiceFactory = ({ tags: tagIds }); - return { type: SecretProtectionType.Direct as const, secret: decryptSecretRaw(secret, botKey) }; + return { + type: SecretProtectionType.Direct as const, + secret: decryptSecretRaw( + { + ...secret, + secretValueHidden: false + }, + botKey + ) + }; }; const updateSecretRaw = async ({ @@ -1492,9 +1788,20 @@ export const secretServiceFactory = ({ secretReminderRepeatDays, metadata, secretComment, - newSecretName + newSecretName, + secretMetadata }: TUpdateSecretRawDTO) => { const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); + const project = await projectDAL.findById(projectId); + if (project.enforceCapitalization) { + if (newSecretName && newSecretName !== newSecretName.toUpperCase()) { + throw new BadRequestError({ + message: + "Secret name must be in UPPERCASE per project requirements. You can disable this requirement in project settings." + }); + } + } + const policy = actor === ActorType.USER && type === SecretType.Shared ? await secretApprovalPolicyService.getSecretApprovalPolicy(projectId, environment, secretPath) @@ -1520,7 +1827,8 @@ export const secretServiceFactory = ({ secretValue, tagIds, reminderNote: secretReminderNote, - reminderRepeatDays: secretReminderRepeatDays + reminderRepeatDays: secretReminderRepeatDays, + secretMetadata } ] } @@ -1544,7 +1852,8 @@ export const secretServiceFactory = ({ secretName, newSecretName, metadata, - secretValue + secretValue, + secretMetadata }); return { type: SecretProtectionType.Direct as const, secret }; } @@ -1742,7 +2051,23 @@ export const secretServiceFactory = ({ actor === ActorType.USER ? await secretApprovalPolicyService.getSecretApprovalPolicy(projectId, environment, secretPath) : undefined; + if (shouldUseSecretV2Bridge) { + const project = await projectDAL.findById(projectId); + if (project.enforceCapitalization) { + const caseViolatingSecretKeys = inputSecrets + .filter((sec) => sec.secretKey !== sec.secretKey.toUpperCase()) + .map((sec) => sec.secretKey); + + if (caseViolatingSecretKeys.length) { + throw new BadRequestError({ + message: `Secret names must be in UPPERCASE per project requirements: ${caseViolatingSecretKeys.join( + ", " + )}. You can disable this requirement in project settings` + }); + } + } + if (policy) { const approval = await secretApprovalRequestService.generateSecretApprovalRequestV2Bridge({ policy, @@ -1760,7 +2085,8 @@ export const secretServiceFactory = ({ secretComment: el.secretComment, metadata: el.metadata, skipMultilineEncoding: el.skipMultilineEncoding, - secretKey: el.secretKey + secretKey: el.secretKey, + secretMetadata: el.secretMetadata })) } }); @@ -1838,7 +2164,7 @@ export const secretServiceFactory = ({ return { type: SecretProtectionType.Direct as const, secrets: secrets.map((secret) => - decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath }, botKey) + decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath, secretValueHidden: false }, botKey) ) }; }; @@ -1852,6 +2178,7 @@ export const secretServiceFactory = ({ actorOrgId, actorAuthMethod, secretPath, + mode = SecretUpdateMode.FailOnNotFound, secrets: inputSecrets = [] }: TUpdateManySecretRawDTO) => { if (!projectSlug && !optionalProjectId) @@ -1870,6 +2197,21 @@ export const secretServiceFactory = ({ ? await secretApprovalPolicyService.getSecretApprovalPolicy(projectId, environment, secretPath) : undefined; if (shouldUseSecretV2Bridge) { + const project = await projectDAL.findById(projectId); + if (project.enforceCapitalization) { + const caseViolatingSecretKeys = inputSecrets + .filter((sec) => sec.newSecretName && sec.newSecretName !== sec.newSecretName.toUpperCase()) + .map((sec) => sec.newSecretName); + + if (caseViolatingSecretKeys.length) { + throw new BadRequestError({ + message: `Secret names must be in UPPERCASE per project requirements: ${caseViolatingSecretKeys.join( + ", " + )}. You can disable this requirement in project settings` + }); + } + } + if (policy) { const approval = await secretApprovalRequestService.generateSecretApprovalRequestV2Bridge({ policy, @@ -1886,7 +2228,8 @@ export const secretServiceFactory = ({ secretValue: el.secretValue, secretComment: el.secretComment, skipMultilineEncoding: el.skipMultilineEncoding, - secretKey: el.secretKey + secretKey: el.secretKey, + secretMetadata: el.secretMetadata })) } }); @@ -1900,7 +2243,8 @@ export const secretServiceFactory = ({ actorOrgId, actor, actorId, - secrets: inputSecrets + secrets: inputSecrets, + mode }); return { type: SecretProtectionType.Direct as const, secrets }; } @@ -2109,30 +2453,62 @@ export const secretServiceFactory = ({ const folder = await folderDAL.findById(secret.folderId); if (!folder) throw new NotFoundError({ message: `Folder with ID '${secret.folderId}' not found` }); + const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(folder.projectId, [folder.id]); + + if (!folderWithPath) { + throw new NotFoundError({ message: `Folder with ID '${folder.id}' not found` }); + } + const { botKey } = await projectBotService.getBotKey(folder.projectId); if (!botKey) throw new NotFoundError({ message: `Project bot for project with ID '${folder.projectId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - folder.projectId, + projectId: folder.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); - const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] }); - return secretVersions.map((el) => - decryptSecretRaw( + const secretVersions = await secretVersionDAL.findBySecretId(secretId, { + offset, + limit, + sort: [["createdAt", "desc"]] + }); + return secretVersions.map((el) => { + const secretKey = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretKeyCiphertext, + iv: secret.secretKeyIV, + tag: secret.secretKeyTag, + key: botKey + }); + + const secretValueHidden = !hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, { + environment: folder.environment.envSlug, + secretPath: folderWithPath.path, + secretName: secretKey, + ...(el.tags?.length && { + secretTags: el.tags.map((tag) => tag.slug) + }) + } + ); + + return decryptSecretRaw( + { + secretValueHidden, ...el, workspace: folder.projectId, environment: folder.environment.envSlug, - secretPath: "/" + secretPath: folderWithPath.path }, botKey - ) - ); + ); + }); }; const attachTags = async ({ @@ -2148,16 +2524,17 @@ export const secretServiceFactory = ({ actorId }: TAttachSecretTagsDTO) => { const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - project.id, + projectId: project.id, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); @@ -2229,6 +2606,7 @@ export const secretServiceFactory = ({ await secretQueueService.syncSecrets({ secretPath, projectId: project.id, + orgId: project.orgId, environmentSlug: environment, excludeReplication: true }); @@ -2252,16 +2630,17 @@ export const secretServiceFactory = ({ actorId }: TAttachSecretTagsDTO) => { const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - project.id, + projectId: project.id, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionSecretActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); @@ -2337,6 +2716,7 @@ export const secretServiceFactory = ({ await secretQueueService.syncSecrets({ secretPath, projectId: project.id, + orgId: project.orgId, environmentSlug: environment, excludeReplication: true }); @@ -2357,13 +2737,14 @@ export const secretServiceFactory = ({ actorOrgId, actorAuthMethod }: TBackFillSecretReferencesDTO) => { - const { hasRole } = await permissionService.getProjectPermission( + const { hasRole } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); if (!hasRole(ProjectMembershipRole.Admin)) throw new ForbiddenRequestError({ message: "Only admins are allowed to take this action" }); @@ -2425,7 +2806,7 @@ export const secretServiceFactory = ({ message: `Project with slug '${projectSlug}' not found` }); } - if (project.version === 3) { + if (project.version === ProjectVersion.V3) { return secretV2BridgeService.moveSecrets({ sourceEnvironment, sourceSecretPath, @@ -2441,37 +2822,14 @@ export const secretServiceFactory = ({ }); } - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - project.id, + projectId: project.id, actorAuthMethod, - actorOrgId - ); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { - environment: sourceEnvironment, - secretPath: sourceSecretPath - }) - ); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { - environment: destinationEnvironment, - secretPath: destinationSecretPath - }) - ); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { - environment: destinationEnvironment, - secretPath: destinationSecretPath - }) - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); const { botKey } = await projectBotService.getBotKey(project.id); if (!botKey) { @@ -2500,11 +2858,9 @@ export const secretServiceFactory = ({ }); } - const sourceSecrets = await secretDAL.find({ + const sourceSecrets = await secretDAL.findManySecretsWithTags({ type: SecretType.Shared, - $in: { - id: secretIds - } + secretIds }); if (sourceSecrets.length !== secretIds.length) { @@ -2513,21 +2869,62 @@ export const secretServiceFactory = ({ }); } - const decryptedSourceSecrets = sourceSecrets.map((secret) => ({ - ...secret, - secretKey: decryptSymmetric128BitHexKeyUTF8({ + const sourceActions = [ + ProjectPermissionSecretActions.Delete, + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue + ] as const; + const destinationActions = [ProjectPermissionSecretActions.Create, ProjectPermissionSecretActions.Edit] as const; + + const decryptedSourceSecrets = sourceSecrets.map((secret) => { + const secretKey = decryptSymmetric128BitHexKeyUTF8({ ciphertext: secret.secretKeyCiphertext, iv: secret.secretKeyIV, tag: secret.secretKeyTag, key: botKey - }), - secretValue: decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secret.secretValueCiphertext, - iv: secret.secretValueIV, - tag: secret.secretValueTag, - key: botKey - }) - })); + }); + + for (const destinationAction of destinationActions) { + ForbiddenError.from(permission).throwUnlessCan( + destinationAction, + subject(ProjectPermissionSub.Secrets, { + environment: destinationEnvironment, + secretPath: destinationSecretPath + }) + ); + } + + for (const sourceAction of sourceActions) { + if ( + sourceAction === ProjectPermissionSecretActions.ReadValue || + sourceAction === ProjectPermissionSecretActions.DescribeSecret + ) { + throwIfMissingSecretReadValueOrDescribePermission(permission, sourceAction, { + environment: sourceEnvironment, + secretPath: sourceSecretPath + }); + } else { + ForbiddenError.from(permission).throwUnlessCan( + sourceAction, + subject(ProjectPermissionSub.Secrets, { + environment: sourceEnvironment, + secretPath: sourceSecretPath + }) + ); + } + } + + return { + ...secret, + secretKey, + secretValue: decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key: botKey + }) + }; + }); let isSourceUpdated = false; let isDestinationUpdated = false; @@ -2795,6 +3192,7 @@ export const secretServiceFactory = ({ await snapshotService.performSnapshot(destinationFolder.id); await secretQueueService.syncSecrets({ projectId: project.id, + orgId: project.orgId, secretPath: destinationFolder.path, environmentSlug: destinationFolder.environment.slug, actorId, @@ -2806,6 +3204,7 @@ export const secretServiceFactory = ({ await snapshotService.performSnapshot(sourceFolder.id); await secretQueueService.syncSecrets({ projectId: project.id, + orgId: project.orgId, secretPath: sourceFolder.path, environmentSlug: sourceFolder.environment.slug, actorId, @@ -2827,13 +3226,14 @@ export const secretServiceFactory = ({ actorOrgId, actorAuthMethod }: TStartSecretsV2MigrationDTO) => { - const { hasRole } = await permissionService.getProjectPermission( + const { hasRole } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); if (!hasRole(ProjectMembershipRole.Admin)) throw new ForbiddenRequestError({ message: "Only admins are allowed to take this action" }); @@ -2855,13 +3255,14 @@ export const secretServiceFactory = ({ if (!shouldUseSecretV2Bridge) throw new BadRequestError({ message: "Project version not supported" }); - const { permission } = await permissionService.getProjectPermission( - actor.type, - actor.id, - params.projectId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: params.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager + }); const secrets = secretV2BridgeService.getSecretsByFolderMappings({ ...params, userId: actor.id }, permission); @@ -2895,6 +3296,9 @@ export const secretServiceFactory = ({ getSecretsCountMultiEnv, getSecretsRawMultiEnv, getSecretReferenceTree, - getSecretsRawByFolderMappings + getSecretsRawByFolderMappings, + getSecretAccessList, + getSecretByIdRaw, + getAccessibleSecrets }; }; diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 7c09c9349..be036cab8 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -2,6 +2,7 @@ import { Knex } from "knex"; import { z } from "zod"; import { SecretType, TSecretBlindIndexes, TSecrets, TSecretsInsert, TSecretsUpdate } from "@app/db/schemas"; +import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; import { OrderByDirection, TProjectPermission } from "@app/lib/types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; @@ -14,7 +15,10 @@ import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; import { ActorType } from "../auth/auth-type"; import { TKmsServiceFactory } from "../kms/kms-service"; +import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; +import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; +import { SecretUpdateMode } from "../secret-v2-bridge/secret-v2-bridge-types"; import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal"; import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal"; @@ -118,6 +122,10 @@ export type TGetASecretDTO = { version?: number; } & TProjectPermission; +export type TGetASecretByIdDTO = { + secretId: string; +} & Omit; + export type TCreateBulkSecretDTO = { path: string; environment: string; @@ -173,24 +181,45 @@ export enum SecretsOrderBy { Name = "name" // "key" for secrets but using name for use across resources } +export type TGetAccessibleSecretsDTO = { + secretPath: string; + environment: string; + recursive?: boolean; + filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue; +} & TProjectPermission; + export type TGetSecretsRawDTO = { expandSecretReferences?: boolean; path: string; environment: string; + viewSecretValue: boolean; + throwOnMissingReadValuePermission?: boolean; includeImports?: boolean; recursive?: boolean; tagSlugs?: string[]; + metadataFilter?: { + key?: string; + value?: string; + }[]; orderBy?: SecretsOrderBy; orderDirection?: OrderByDirection; offset?: number; limit?: number; search?: string; + keys?: string[]; +} & TProjectPermission; + +export type TGetSecretAccessListDTO = { + environment: string; + secretPath: string; + secretName: string; } & TProjectPermission; export type TGetASecretRawDTO = { secretName: string; path: string; environment: string; + viewSecretValue: boolean; expandSecretReferences?: boolean; type: "shared" | "personal"; includeImports?: boolean; @@ -199,6 +228,10 @@ export type TGetASecretRawDTO = { projectId?: string; } & Omit; +export type TGetASecretByIdRawDTO = { + secretId: string; +} & Omit; + export type TCreateSecretRawDTO = TProjectPermission & { secretName: string; secretPath: string; @@ -210,6 +243,7 @@ export type TCreateSecretRawDTO = TProjectPermission & { skipMultilineEncoding?: boolean; secretReminderRepeatDays?: number | null; secretReminderNote?: string | null; + secretMetadata?: ResourceMetadataDTO; }; export type TUpdateSecretRawDTO = TProjectPermission & { @@ -227,6 +261,7 @@ export type TUpdateSecretRawDTO = TProjectPermission & { metadata?: { source?: string; }; + secretMetadata?: ResourceMetadataDTO; }; export type TDeleteSecretRawDTO = TProjectPermission & { @@ -247,6 +282,7 @@ export type TCreateManySecretRawDTO = Omit & { secretComment?: string; skipMultilineEncoding?: boolean; tagIds?: string[]; + secretMetadata?: ResourceMetadataDTO; metadata?: { source?: string; }; @@ -258,13 +294,15 @@ export type TUpdateManySecretRawDTO = Omit & { projectId?: string; projectSlug?: string; environment: string; + mode: SecretUpdateMode; secrets: { secretKey: string; newSecretName?: string; - secretValue: string; + secretValue?: string; secretComment?: string; skipMultilineEncoding?: boolean; tagIds?: string[]; + secretMetadata?: ResourceMetadataDTO; secretReminderRepeatDays?: number | null; secretReminderNote?: string | null; }[]; @@ -292,7 +330,13 @@ export type TSecretReference = { environment: string; secretPath: string }; export type TFnSecretBulkInsert = { folderId: string; tx?: Knex; - inputSecrets: Array & { tags?: string[]; references?: TSecretReference[] }>; + inputSecrets: Array< + Omit & { + tags?: string[]; + references?: TSecretReference[]; + secretMetadata?: ResourceMetadataDTO; + } + >; secretDAL: Pick; secretVersionDAL: Pick; secretTagDAL: Pick; @@ -384,10 +428,11 @@ export type TCreateManySecretsRawFnFactory = { kmsService: Pick; secretV2BridgeDAL: Pick< TSecretV2BridgeDALFactory, - "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" + "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find" >; secretVersionV2BridgeDAL: Pick; secretVersionTagV2BridgeDAL: Pick; + resourceMetadataDAL: Pick; }; export type TCreateManySecretsRawFn = { @@ -420,10 +465,11 @@ export type TUpdateManySecretsRawFnFactory = { kmsService: Pick; secretV2BridgeDAL: Pick< TSecretV2BridgeDALFactory, - "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" + "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find" >; secretVersionV2BridgeDAL: Pick; secretVersionTagV2BridgeDAL: Pick; + resourceMetadataDAL: Pick; }; export type TUpdateManySecretsRawFn = { @@ -459,6 +505,7 @@ export type TSyncSecretsDTO = { _depth?: number; secretPath: string; projectId: string; + orgId: string; environmentSlug: string; // cases for just doing sync integration and webhook excludeReplication?: T; diff --git a/backend/src/services/secret/secret-version-dal.ts b/backend/src/services/secret/secret-version-dal.ts index 8e77858a5..8e4544c19 100644 --- a/backend/src/services/secret/secret-version-dal.ts +++ b/backend/src/services/secret/secret-version-dal.ts @@ -1,9 +1,9 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas"; +import { SecretVersionsSchema, TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; -import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex"; import { logger } from "@app/lib/logger"; import { QueueName } from "@app/queue"; @@ -12,6 +12,50 @@ export type TSecretVersionDALFactory = ReturnType { const secretVersionOrm = ormify(db, TableName.SecretVersion); + const findBySecretId = async (secretId: string, { offset, limit, sort, tx }: TFindOpt = {}) => { + try { + const query = (tx || db.replicaNode())(TableName.SecretVersion) + .where(`${TableName.SecretVersion}.secretId`, secretId) + .leftJoin(TableName.Secret, `${TableName.SecretVersion}.secretId`, `${TableName.Secret}.id`) + .leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`) + .leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`) + .select(selectAllTableCols(TableName.SecretVersion)) + .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) + .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); + + if (limit) void query.limit(limit); + if (offset) void query.offset(offset); + if (sort) { + void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); + } + + const docs = await query; + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => ({ _id: el.id, ...SecretVersionsSchema.parse(el) }), + childrenMapper: [ + { + key: "tagId", + label: "tags" as const, + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ + id, + color, + slug, + name: slug + }) + } + ] + }); + + return data; + } catch (error) { + throw new DatabaseError({ error, name: `${TableName.SecretVersion}: FindBySecretId` }); + } + }; + // This will fetch all latest secret versions from a folder const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => { try { @@ -149,6 +193,7 @@ export const secretVersionDALFactory = (db: TDbClient) => { findLatestVersionMany, bulkUpdate, findLatestVersionByFolderId, + findBySecretId, bulkUpdateNoVersionIncrement }; }; diff --git a/backend/src/services/service-token/service-token-dal.ts b/backend/src/services/service-token/service-token-dal.ts index ed9c5de7e..adb2f325a 100644 --- a/backend/src/services/service-token/service-token-dal.ts +++ b/backend/src/services/service-token/service-token-dal.ts @@ -28,5 +28,36 @@ export const serviceTokenDALFactory = (db: TDbClient) => { } }; - return { ...stOrm, findById }; + const findExpiringTokens = async (tx?: Knex, batchSize = 500, offset = 0) => { + try { + const batch: { name: string; projectName: string; createdByEmail: string; id: string; projectId: string }[] = + await (tx || db.replicaNode())(TableName.ServiceToken) + .leftJoin( + TableName.Users, + `${TableName.Users}.id`, + db.raw(`${TableName.ServiceToken}."createdBy"::uuid`) + ) + .join(TableName.Project, `${TableName.Project}.id`, `${TableName.ServiceToken}.projectId`) + .whereRaw( + `${TableName.ServiceToken}."expiresAt" < NOW() + INTERVAL '1 day' AND ${TableName.ServiceToken}."expiryNotificationSent" = false` + ) + .whereNotNull(`${TableName.Users}.email`) + .select( + db.ref("id").withSchema(TableName.ServiceToken), + db.ref("name").withSchema(TableName.ServiceToken), + db.ref("projectId").withSchema(TableName.ServiceToken), + db.ref("createdBy").withSchema(TableName.ServiceToken), + db.ref("email").withSchema(TableName.Users).as("createdByEmail"), + db.ref("name").withSchema(TableName.Project).as("projectName") + ) + .limit(batchSize) + .offset(offset); + + return batch; + } catch (err) { + throw new DatabaseError({ error: err, name: "FindExpiredTokens" }); + } + }; + + return { ...stOrm, findById, findExpiringTokens }; }; diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts index fe2c1c0d2..bbd306bb5 100644 --- a/backend/src/services/service-token/service-token-service.ts +++ b/backend/src/services/service-token/service-token-service.ts @@ -3,15 +3,22 @@ import crypto from "node:crypto"; import { ForbiddenError, subject } from "@casl/ability"; import bcrypt from "bcrypt"; +import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionSecretActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-token-queue"; import { ActorType } from "../auth/auth-type"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; +import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TUserDALFactory } from "../user/user-dal"; import { TServiceTokenDALFactory } from "./service-token-dal"; import { @@ -28,6 +35,7 @@ type TServiceTokenServiceFactoryDep = { projectEnvDAL: Pick; projectDAL: Pick; accessTokenQueue: Pick; + smtpService: Pick; }; export type TServiceTokenServiceFactory = ReturnType; @@ -38,7 +46,8 @@ export const serviceTokenServiceFactory = ({ permissionService, projectEnvDAL, projectDAL, - accessTokenQueue + accessTokenQueue, + smtpService }: TServiceTokenServiceFactoryDep) => { const createServiceToken = async ({ iv, @@ -54,18 +63,19 @@ export const serviceTokenServiceFactory = ({ permissions, encryptedKey }: TCreateServiceTokenDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens); scopes.forEach(({ environment, secretPath }) => { ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionSecretActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); }); @@ -109,13 +119,14 @@ export const serviceTokenServiceFactory = ({ const serviceToken = await serviceTokenDAL.findById(id); if (!serviceToken) throw new NotFoundError({ message: `Service token with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - serviceToken.projectId, + projectId: serviceToken.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens); const deletedServiceToken = await serviceTokenDAL.deleteById(id); @@ -143,13 +154,14 @@ export const serviceTokenServiceFactory = ({ actorAuthMethod, projectId }: TProjectServiceTokensDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); const tokens = await serviceTokenDAL.find({ projectId }, { sort: [["createdAt", "desc"]] }); @@ -177,11 +189,56 @@ export const serviceTokenServiceFactory = ({ return { ...serviceToken, lastUsed: new Date(), orgId: project.orgId }; }; + const notifyExpiringTokens = async () => { + const appCfg = getConfig(); + let processedCount = 0; + let hasMoreRecords = true; + let offset = 0; + const batchSize = 500; + + while (hasMoreRecords) { + // eslint-disable-next-line no-await-in-loop + const expiringTokens = await serviceTokenDAL.findExpiringTokens(undefined, batchSize, offset); + + if (expiringTokens.length === 0) { + hasMoreRecords = false; + break; + } + + // eslint-disable-next-line no-await-in-loop + await Promise.all( + expiringTokens.map(async (token) => { + try { + await smtpService.sendMail({ + recipients: [token.createdByEmail], + subjectLine: "Service Token Expiry Notice", + template: SmtpTemplates.ServiceTokenExpired, + substitutions: { + tokenName: token.name, + projectName: token.projectName, + url: `${appCfg.SITE_URL}/secret-manager/${token.projectId}/access-management?selectedTab=service-tokens` + } + }); + await serviceTokenDAL.update({ id: token.id }, { expiryNotificationSent: true }); + } catch (error) { + logger.error(error, `Failed to send expiration notification for token ${token.id}:`); + } + }) + ); + + processedCount += expiringTokens.length; + offset += batchSize; + } + + return processedCount; + }; + return { createServiceToken, deleteServiceToken, getServiceToken, getProjectServiceTokens, - fnValidateServiceToken + fnValidateServiceToken, + notifyExpiringTokens }; }; diff --git a/backend/src/services/slack/slack-fns.ts b/backend/src/services/slack/slack-fns.ts index 919f9de05..6c84c0e76 100644 --- a/backend/src/services/slack/slack-fns.ts +++ b/backend/src/services/slack/slack-fns.ts @@ -50,8 +50,9 @@ const buildSlackPayload = (notification: TSlackNotification) => { const messageBody = `A secret approval request has been opened by ${payload.userEmail}. *Environment*: ${payload.environment} *Secret path*: ${payload.secretPath || "/"} +*Secret Key${payload.secretKeys.length > 1 ? "s" : ""}*: ${payload.secretKeys.join(", ")} -View the complete details <${appCfg.SITE_URL}/project/${payload.projectId}/approval?requestId=${ +View the complete details <${appCfg.SITE_URL}/secret-manager/${payload.projectId}/approval?requestId=${ payload.requestId }|here>.`; @@ -86,7 +87,12 @@ View the complete details <${appCfg.SITE_URL}/project/${payload.projectId}/appro The following permissions are requested: ${payload.permissions.join(", ")} -View the request and approve or deny it <${payload.approvalUrl}|here>.`; +View the request and approve or deny it <${payload.approvalUrl}|here>.${ + payload.note + ? ` +User Note: ${payload.note}` + : "" + }`; const payloadBlocks = [ { diff --git a/backend/src/services/slack/slack-types.ts b/backend/src/services/slack/slack-types.ts index a1914eee2..3e8354adf 100644 --- a/backend/src/services/slack/slack-types.ts +++ b/backend/src/services/slack/slack-types.ts @@ -62,6 +62,7 @@ export type TSlackNotification = secretPath: string; requestId: string; projectId: string; + secretKeys: string[]; }; } | { @@ -75,5 +76,6 @@ export type TSlackNotification = projectName: string; permissions: string[]; approvalUrl: string; + note?: string; }; }; diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index 1f38babb3..550e1bb07 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -30,14 +30,22 @@ export enum SmtpTemplates { NewDeviceJoin = "newDevice.handlebars", OrgInvite = "organizationInvitation.handlebars", ResetPassword = "passwordReset.handlebars", + SetupPassword = "passwordSetup.handlebars", SecretLeakIncident = "secretLeakIncident.handlebars", WorkspaceInvite = "workspaceInvitation.handlebars", ScimUserProvisioned = "scimUserProvisioned.handlebars", PkiExpirationAlert = "pkiExpirationAlert.handlebars", IntegrationSyncFailed = "integrationSyncFailed.handlebars", + SecretSyncFailed = "secretSyncFailed.handlebars", ExternalImportSuccessful = "externalImportSuccessful.handlebars", ExternalImportFailed = "externalImportFailed.handlebars", - ExternalImportStarted = "externalImportStarted.handlebars" + ExternalImportStarted = "externalImportStarted.handlebars", + SecretRequestCompleted = "secretRequestCompleted.handlebars", + SecretRotationFailed = "secretRotationFailed.handlebars", + ProjectAccessRequest = "projectAccess.handlebars", + OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars", + OrgAdminBreakglassAccess = "orgAdminBreakglassAccess.handlebars", + ServiceTokenExpired = "serviceTokenExpired.handlebars" } export enum SmtpHost { @@ -53,6 +61,13 @@ export const smtpServiceFactory = (cfg: TSmtpConfig) => { const smtp = createTransport(cfg); const isSmtpOn = Boolean(cfg.host); + handlebars.registerHelper("emailFooter", () => { + const { SITE_URL } = getConfig(); + return new handlebars.SafeString( + `

Email sent via Infisical at ${SITE_URL}

` + ); + }); + const sendMail = async ({ substitutions, recipients, template, subjectLine }: TSmtpSendMail) => { const appCfg = getConfig(); const html = await fs.readFile(path.resolve(__dirname, "./templates/", template), "utf8"); @@ -77,5 +92,21 @@ export const smtpServiceFactory = (cfg: TSmtpConfig) => { } }; - return { sendMail }; + const verify = async () => { + const isConnected = smtp + .verify() + .then(async () => { + logger.info("SMTP connected"); + return true; + }) + .catch((err: Error) => { + logger.error("SMTP error"); + logger.error(err); + return false; + }); + + return isConnected; + }; + + return { sendMail, verify }; }; diff --git a/backend/src/services/smtp/templates/accessApprovalRequest.handlebars b/backend/src/services/smtp/templates/accessApprovalRequest.handlebars index 82c66ce5f..6813c1200 100644 --- a/backend/src/services/smtp/templates/accessApprovalRequest.handlebars +++ b/backend/src/services/smtp/templates/accessApprovalRequest.handlebars @@ -40,11 +40,16 @@ {{/each}}

+ {{#if note}} +

User Note: "{{note}}"

+ {{/if}}

View the request and approve or deny it here.

+ + {{emailFooter}} - \ No newline at end of file + diff --git a/backend/src/services/smtp/templates/accessSecretRequestBypassed.handlebars b/backend/src/services/smtp/templates/accessSecretRequestBypassed.handlebars index 3313d352f..8c82df289 100644 --- a/backend/src/services/smtp/templates/accessSecretRequestBypassed.handlebars +++ b/backend/src/services/smtp/templates/accessSecretRequestBypassed.handlebars @@ -11,8 +11,11 @@

A secret approval request has been bypassed in the project "{{projectName}}".

- {{requesterFullName}} ({{requesterEmail}}) has merged - a secret to environment {{environment}} at secret path {{secretPath}} + {{requesterFullName}} + ({{requesterEmail}}) has merged a secret to environment + {{environment}} + at secret path + {{secretPath}} without obtaining the required approvals.

@@ -24,5 +27,7 @@ To review this action, please visit the request panel here.

+ + {{emailFooter}} \ No newline at end of file diff --git a/backend/src/services/smtp/templates/emailMfa.handlebars b/backend/src/services/smtp/templates/emailMfa.handlebars index 936195c34..4c948b08c 100644 --- a/backend/src/services/smtp/templates/emailMfa.handlebars +++ b/backend/src/services/smtp/templates/emailMfa.handlebars @@ -1,4 +1,3 @@ - @@ -14,6 +13,8 @@

{{code}}

The MFA code will be valid for 2 minutes.

Not you? Contact {{#if isCloud}}Infisical{{else}}your administrator{{/if}} immediately.

+ + {{emailFooter}} \ No newline at end of file diff --git a/backend/src/services/smtp/templates/emailVerification.handlebars b/backend/src/services/smtp/templates/emailVerification.handlebars index ad9694d5c..4a989626e 100644 --- a/backend/src/services/smtp/templates/emailVerification.handlebars +++ b/backend/src/services/smtp/templates/emailVerification.handlebars @@ -10,6 +10,8 @@

Confirm your email address

Your confirmation code is below — enter it in the browser window where you've started confirming your email.

{{code}}

+ + {{emailFooter}} \ No newline at end of file diff --git a/backend/src/services/smtp/templates/externalImportFailed.handlebars b/backend/src/services/smtp/templates/externalImportFailed.handlebars index c7869af27..1755052c1 100644 --- a/backend/src/services/smtp/templates/externalImportFailed.handlebars +++ b/backend/src/services/smtp/templates/externalImportFailed.handlebars @@ -16,6 +16,7 @@

Error: {{error}}

+ {{emailFooter}} \ No newline at end of file diff --git a/backend/src/services/smtp/templates/externalImportStarted.handlebars b/backend/src/services/smtp/templates/externalImportStarted.handlebars index 551f972cc..90026f762 100644 --- a/backend/src/services/smtp/templates/externalImportStarted.handlebars +++ b/backend/src/services/smtp/templates/externalImportStarted.handlebars @@ -12,6 +12,8 @@ {{provider}} to Infisical is in progress. The import process may take up to 30 minutes, and you will receive once the import has finished or if it fails.

+ + {{emailFooter}} \ No newline at end of file diff --git a/backend/src/services/smtp/templates/externalImportSuccessful.handlebars b/backend/src/services/smtp/templates/externalImportSuccessful.handlebars index 51a1c465e..a918e9ec7 100644 --- a/backend/src/services/smtp/templates/externalImportSuccessful.handlebars +++ b/backend/src/services/smtp/templates/externalImportSuccessful.handlebars @@ -9,6 +9,8 @@

An import from {{provider}} to Infisical was successful

An import from {{provider}} was successful. Your data is now available in Infisical.

+ + {{emailFooter}} \ No newline at end of file diff --git a/backend/src/services/smtp/templates/historicalSecretLeakIncident.handlebars b/backend/src/services/smtp/templates/historicalSecretLeakIncident.handlebars index 0798538fb..4a918ee0d 100644 --- a/backend/src/services/smtp/templates/historicalSecretLeakIncident.handlebars +++ b/backend/src/services/smtp/templates/historicalSecretLeakIncident.handlebars @@ -1,21 +1,21 @@ - - - - - Incident alert: secrets potentially leaked - + + + + Incident alert: secrets potentially leaked + - -

Infisical has uncovered {{numberOfSecrets}} secret(s) from historical commits to your repo

-

View leaked secrets

+ +

Infisical has uncovered {{numberOfSecrets}} secret(s) from historical commits to your repo

+

View leaked secrets

-

If these are production secrets, please rotate them immediately.

+

If these are production secrets, please rotate them immediately.

-

Once you have taken action, be sure to update the status of the risk in your Infisical - dashboard.

- +

Once you have taken action, be sure to update the status of the risk in your + Infisical dashboard.

+ + {{emailFooter}} + \ No newline at end of file diff --git a/backend/src/services/smtp/templates/integrationSyncFailed.handlebars b/backend/src/services/smtp/templates/integrationSyncFailed.handlebars index 5c5d76693..2aff820fa 100644 --- a/backend/src/services/smtp/templates/integrationSyncFailed.handlebars +++ b/backend/src/services/smtp/templates/integrationSyncFailed.handlebars @@ -26,6 +26,8 @@ {{#if syncMessage}}

Reason: {{syncMessage}}

{{/if}} + + {{emailFooter}} \ No newline at end of file diff --git a/backend/src/services/smtp/templates/newDevice.handlebars b/backend/src/services/smtp/templates/newDevice.handlebars index 6c7f2e9f6..197e0b7a7 100644 --- a/backend/src/services/smtp/templates/newDevice.handlebars +++ b/backend/src/services/smtp/templates/newDevice.handlebars @@ -1,4 +1,3 @@ - @@ -13,7 +12,11 @@

Timestamp: {{timestamp}}

IP address: {{ip}}

User agent: {{userAgent}}

-

If you believe that this login is suspicious, please contact {{#if isCloud}}Infisical{{else}}your administrator{{/if}} or reset your password immediately.

+

If you believe that this login is suspicious, please contact + {{#if isCloud}}Infisical{{else}}your administrator{{/if}} + or reset your password immediately.

+ + {{emailFooter}} \ No newline at end of file diff --git a/backend/src/services/smtp/templates/orgAdminBreakglassAccess.handlebars b/backend/src/services/smtp/templates/orgAdminBreakglassAccess.handlebars new file mode 100644 index 000000000..cc97ff201 --- /dev/null +++ b/backend/src/services/smtp/templates/orgAdminBreakglassAccess.handlebars @@ -0,0 +1,20 @@ + + + + + + Organization admin has bypassed SSO + + + +

Infisical

+

The organization admin {{email}} has bypassed enforced SSO login.

+

Timestamp: {{timestamp}}

+

IP address: {{ip}}

+

User agent: {{userAgent}}

+

If you'd like to disable Admin SSO Bypass, please visit Organization Settings > Security.

+ + {{emailFooter}} + + + diff --git a/backend/src/services/smtp/templates/orgAdminProjectGrantAccess.handlebars b/backend/src/services/smtp/templates/orgAdminProjectGrantAccess.handlebars new file mode 100644 index 000000000..ef8c6e6b4 --- /dev/null +++ b/backend/src/services/smtp/templates/orgAdminProjectGrantAccess.handlebars @@ -0,0 +1,16 @@ + + + + + + Organization admin issued direct access to project + + + +

Infisical

+

The organization admin {{email}} has granted direct access to the project "{{projectName}}".

+ + {{emailFooter}} + + + diff --git a/backend/src/services/smtp/templates/organizationInvitation.handlebars b/backend/src/services/smtp/templates/organizationInvitation.handlebars index 3ee16ee37..da429477b 100644 --- a/backend/src/services/smtp/templates/organizationInvitation.handlebars +++ b/backend/src/services/smtp/templates/organizationInvitation.handlebars @@ -8,9 +8,11 @@

Join your organization on Infisical

-

{{inviterFirstName}} ({{inviterUsername}}) has invited you to their Infisical organization — {{organizationName}}

- Join now +

{{inviterFirstName}} ({{inviterUsername}}) has invited you to their Infisical organization named {{organizationName}}

+ Click to join

What is Infisical?

Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.

+ + {{emailFooter}} - \ No newline at end of file + diff --git a/backend/src/services/smtp/templates/passwordReset.handlebars b/backend/src/services/smtp/templates/passwordReset.handlebars index 6499a629c..1cb2ae8ce 100644 --- a/backend/src/services/smtp/templates/passwordReset.handlebars +++ b/backend/src/services/smtp/templates/passwordReset.handlebars @@ -1,14 +1,16 @@ - - - - + + + Account Recovery - - + +

Reset your password

Someone requested a password reset.

Reset password -

If you didn't initiate this request, please contact {{#if isCloud}}us immediately at team@infisical.com.{{else}}your administrator immediately.{{/if}}

- +

If you didn't initiate this request, please contact + {{#if isCloud}}us immediately at team@infisical.com.{{else}}your administrator immediately.{{/if}}

+ + {{emailFooter}} + \ No newline at end of file diff --git a/backend/src/services/smtp/templates/passwordSetup.handlebars b/backend/src/services/smtp/templates/passwordSetup.handlebars new file mode 100644 index 000000000..1059a7446 --- /dev/null +++ b/backend/src/services/smtp/templates/passwordSetup.handlebars @@ -0,0 +1,17 @@ + + + + + Password Setup + + +

Setup your password

+

Someone requested to set up a password for your account.

+

Make sure you are already logged in to Infisical in the current browser before clicking the link below.

+ Setup password +

If you didn't initiate this request, please contact + {{#if isCloud}}us immediately at team@infisical.com.{{else}}your administrator immediately.{{/if}}

+ + {{emailFooter}} + + \ No newline at end of file diff --git a/backend/src/services/smtp/templates/pkiExpirationAlert.handlebars b/backend/src/services/smtp/templates/pkiExpirationAlert.handlebars index 77d2543ae..f9013e24d 100644 --- a/backend/src/services/smtp/templates/pkiExpirationAlert.handlebars +++ b/backend/src/services/smtp/templates/pkiExpirationAlert.handlebars @@ -27,5 +27,7 @@

Please take necessary actions to renew these items before they expire.

For more details, please log in to your Infisical account and check your PKI management section.

+ + {{emailFooter}} \ No newline at end of file diff --git a/backend/src/services/smtp/templates/projectAccess.handlebars b/backend/src/services/smtp/templates/projectAccess.handlebars new file mode 100644 index 000000000..5ff1ca7ec --- /dev/null +++ b/backend/src/services/smtp/templates/projectAccess.handlebars @@ -0,0 +1,26 @@ + + + + + + Project Access Request + + + +

Infisical

+

You have a new project access request!

+
    +
  • Requester Name: "{{requesterName}}"
  • +
  • Requester Email: "{{requesterEmail}}"
  • +
  • Project Name: "{{projectName}}"
  • +
  • Organization Name: "{{orgName}}"
  • +
  • User Note: "{{note}}"
  • +
+

+ Please click on the link below to grant access +

+ Grant Access + {{emailFooter}} + + + diff --git a/backend/src/services/smtp/templates/scimUserProvisioned.handlebars b/backend/src/services/smtp/templates/scimUserProvisioned.handlebars index b1482aa17..ba04d7201 100644 --- a/backend/src/services/smtp/templates/scimUserProvisioned.handlebars +++ b/backend/src/services/smtp/templates/scimUserProvisioned.handlebars @@ -1,16 +1,18 @@ - - - - + + + Organization Invitation - - + +

Join your organization on Infisical

You've been invited to join the Infisical organization — {{organizationName}}

Join now

What is Infisical?

-

Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.

- +

Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets + and configs.

+ + {{emailFooter}} + \ No newline at end of file diff --git a/backend/src/services/smtp/templates/secretApprovalRequestNeedsReview.handlebars b/backend/src/services/smtp/templates/secretApprovalRequestNeedsReview.handlebars index 9dd6fe747..c12c08460 100644 --- a/backend/src/services/smtp/templates/secretApprovalRequestNeedsReview.handlebars +++ b/backend/src/services/smtp/templates/secretApprovalRequestNeedsReview.handlebars @@ -17,6 +17,8 @@ View the request and approve or deny it here.

+ + {{emailFooter}} \ No newline at end of file diff --git a/backend/src/services/smtp/templates/secretLeakIncident.handlebars b/backend/src/services/smtp/templates/secretLeakIncident.handlebars index c3c5f353a..d0d9a617c 100644 --- a/backend/src/services/smtp/templates/secretLeakIncident.handlebars +++ b/backend/src/services/smtp/templates/secretLeakIncident.handlebars @@ -1,25 +1,27 @@ - - - - - Incident alert: secret leaked - + + + + Incident alert: secret leaked + - -

Infisical has uncovered {{numberOfSecrets}} secret(s) from your recent push

-

View leaked secrets

-

You are receiving this notification because one or more secret leaks have been detected in a recent commit pushed - by {{pusher_name}} ({{pusher_email}}). If - these are test secrets, please add `infisical-scan:ignore` at the end of the line containing the secret as comment - in the given programming. This will prevent future notifications from being sent out for those secret(s).

+ +

Infisical has uncovered {{numberOfSecrets}} secret(s) from your recent push

+

View leaked secrets

+

You are receiving this notification because one or more secret leaks have been detected in a recent commit pushed + by + {{pusher_name}} + ({{pusher_email}}). If these are test secrets, please add `infisical-scan:ignore` at the end of the line + containing the secret as comment in the given programming. This will prevent future notifications from being sent + out for those secret(s).

-

If these are production secrets, please rotate them immediately.

+

If these are production secrets, please rotate them immediately.

-

Once you have taken action, be sure to update the status of the risk in your Infisical - dashboard.

- +

Once you have taken action, be sure to update the status of the risk in your + Infisical dashboard.

+ + {{emailFooter}} + \ No newline at end of file diff --git a/backend/src/services/smtp/templates/secretReminder.handlebars b/backend/src/services/smtp/templates/secretReminder.handlebars index 2a0efcac8..d64c4bf42 100644 --- a/backend/src/services/smtp/templates/secretReminder.handlebars +++ b/backend/src/services/smtp/templates/secretReminder.handlebars @@ -13,6 +13,8 @@ {{#if reminderNote}}

Here's the note included with the reminder: {{reminderNote}}

{{/if}} + + {{emailFooter}} \ No newline at end of file diff --git a/backend/src/services/smtp/templates/secretRequestCompleted.handlebars b/backend/src/services/smtp/templates/secretRequestCompleted.handlebars new file mode 100644 index 000000000..d2cefdd54 --- /dev/null +++ b/backend/src/services/smtp/templates/secretRequestCompleted.handlebars @@ -0,0 +1,33 @@ + + + + + + Secret Request Completed + + + +

Infisical

+

A secret has been shared with you

+ + {{#if name}} +

Secret request name: {{name}}

+ {{/if}} + {{#if respondentUsername}} +

Shared by: {{respondentUsername}}

+ {{/if}} + +
+
+ +

+ You can access the secret by clicking the link below. +

+

+ Access Secret +

+ + {{emailFooter}} + + + \ No newline at end of file diff --git a/backend/src/services/smtp/templates/secretRotationFailed.handlebars b/backend/src/services/smtp/templates/secretRotationFailed.handlebars new file mode 100644 index 000000000..728798ce8 --- /dev/null +++ b/backend/src/services/smtp/templates/secretRotationFailed.handlebars @@ -0,0 +1,31 @@ + + + + + + Your {{rotationType}} Rotation "{{rotationName}}" Failed to Rotate + + + +

Infisical

+ + + +
+
+

Name: {{rotationName}}

+

Type: {{rotationType}}

+

Project: {{projectName}}

+

Environment: {{environment}}

+

Secret Path: {{secretPath}}

+
+ + {{emailFooter}} + + + \ No newline at end of file diff --git a/backend/src/services/smtp/templates/secretSyncFailed.handlebars b/backend/src/services/smtp/templates/secretSyncFailed.handlebars new file mode 100644 index 000000000..3e7ad7831 --- /dev/null +++ b/backend/src/services/smtp/templates/secretSyncFailed.handlebars @@ -0,0 +1,39 @@ + + + + + + {{syncDestination}} Sync "{{syncName}}" Failed + + + +

Infisical

+ +
+

{{content}}

+ + View in Infisical. + +
+ +
+
+

Name: {{syncName}}

+

Destination: {{syncDestination}}

+

Project: {{projectName}}

+ {{#if environment}} +

Environment: {{environment}}

+ {{/if}} + {{#if secretPath}} +

Secret Path: {{secretPath}}

+ {{/if}} +
+ + {{#if failureMessage}} +

Reason: {{failureMessage}}

+ {{/if}} + + {{emailFooter}} + + + \ No newline at end of file diff --git a/backend/src/services/smtp/templates/serviceTokenExpired.handlebars b/backend/src/services/smtp/templates/serviceTokenExpired.handlebars new file mode 100644 index 000000000..199150c05 --- /dev/null +++ b/backend/src/services/smtp/templates/serviceTokenExpired.handlebars @@ -0,0 +1,19 @@ + + + + + + Service Token Expiring Soon + + + +

Service Token Expiry Notice

+

Your service token "{{tokenName}}" will expire within 24 hours.

+ +

This token is currently being used on project "{{projectName}}". If this token is still needed for your workflow, please create a new one before it expires.

+ + Create New Token + + {{emailFooter}} + + \ No newline at end of file diff --git a/backend/src/services/smtp/templates/signupEmailVerification.handlebars b/backend/src/services/smtp/templates/signupEmailVerification.handlebars index 3ba18619f..39f47ae48 100644 --- a/backend/src/services/smtp/templates/signupEmailVerification.handlebars +++ b/backend/src/services/smtp/templates/signupEmailVerification.handlebars @@ -1,17 +1,19 @@ - - - - + + + Code - + - +

Confirm your email address

Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.

{{code}}

-

Questions about setting up Infisical? {{#if isCloud}}Email us at support@infisical.com{{else}}Contact your administrator{{/if}}.

- +

Questions about setting up Infisical? + {{#if isCloud}}Email us at support@infisical.com{{else}}Contact your administrator{{/if}}.

+ + {{emailFooter}} + \ No newline at end of file diff --git a/backend/src/services/smtp/templates/unlockAccount.handlebars b/backend/src/services/smtp/templates/unlockAccount.handlebars index 36664be87..b65cb5625 100644 --- a/backend/src/services/smtp/templates/unlockAccount.handlebars +++ b/backend/src/services/smtp/templates/unlockAccount.handlebars @@ -11,6 +11,8 @@

Your account has been temporarily locked due to multiple failed login attempts. To unlock your account, follow the link here

If these attempts were not made by you, reset your password immediately.

+ + {{emailFooter}} \ No newline at end of file diff --git a/backend/src/services/smtp/templates/workspaceInvitation.handlebars b/backend/src/services/smtp/templates/workspaceInvitation.handlebars index 39a9b74ba..fde75a6d6 100644 --- a/backend/src/services/smtp/templates/workspaceInvitation.handlebars +++ b/backend/src/services/smtp/templates/workspaceInvitation.handlebars @@ -6,10 +6,12 @@

Join your team on Infisical

-

You have been invited to a new Infisical project — {{workspaceName}}

- Join now +

You have been invited to a new Infisical project named {{workspaceName}}

+ Click to join

What is Infisical?

Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.

+ + {{emailFooter}} - \ No newline at end of file + diff --git a/backend/src/services/super-admin/super-admin-fns.ts b/backend/src/services/super-admin/super-admin-fns.ts new file mode 100644 index 000000000..12ac0e7d7 --- /dev/null +++ b/backend/src/services/super-admin/super-admin-fns.ts @@ -0,0 +1,30 @@ +import { ForbiddenRequestError } from "@app/lib/errors"; +import { TAuthMode } from "@app/server/plugins/auth/inject-identity"; + +import { ActorType } from "../auth/auth-type"; +import { getServerCfg } from "./super-admin-service"; + +export const isSuperAdmin = (auth: TAuthMode) => { + if (auth.actor === ActorType.USER && auth.user.superAdmin) { + return true; + } + + if (auth.actor === ActorType.IDENTITY && auth.isInstanceAdmin) { + return true; + } + + return false; +}; + +export const validateIdentityUpdateForSuperAdminPrivileges = async ( + identityId: string, + isActorSuperAdmin?: boolean +) => { + const serverCfg = await getServerCfg(); + if (serverCfg.adminIdentityIds?.includes(identityId) && !isActorSuperAdmin) { + throw new ForbiddenRequestError({ + message: + "You are attempting to modify an instance admin identity. This requires elevated instance admin privileges" + }); + } +}; diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 12c25de91..317348cac 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -1,26 +1,49 @@ import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; -import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; +import { IdentityAuthMethod, OrgMembershipRole, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { TKeyStoreFactory } from "@app/keystore/keystore"; +import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; -import { getUserPrivateKey } from "@app/lib/crypto/srp"; +import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { TIdentityDALFactory } from "@app/services/identity/identity-dal"; import { TAuthLoginFactory } from "../auth/auth-login-service"; -import { AuthMethod } from "../auth/auth-type"; +import { AuthMethod, AuthTokenType } from "../auth/auth-type"; +import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; +import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; +import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { TIdentityTokenAuthDALFactory } from "../identity-token-auth/identity-token-auth-dal"; +import { KMS_ROOT_CONFIG_UUID } from "../kms/kms-fns"; +import { TKmsRootConfigDALFactory } from "../kms/kms-root-config-dal"; import { TKmsServiceFactory } from "../kms/kms-service"; +import { RootKeyEncryptionStrategy } from "../kms/kms-types"; import { TOrgServiceFactory } from "../org/org-service"; import { TUserDALFactory } from "../user/user-dal"; +import { TUserAliasDALFactory } from "../user-alias/user-alias-dal"; +import { UserAliasType } from "../user-alias/user-alias-types"; import { TSuperAdminDALFactory } from "./super-admin-dal"; -import { LoginMethod, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types"; +import { + LoginMethod, + TAdminBootstrapInstanceDTO, + TAdminGetIdentitiesDTO, + TAdminGetUsersDTO, + TAdminSignUpDTO +} from "./super-admin-types"; type TSuperAdminServiceFactoryDep = { + identityDAL: TIdentityDALFactory; + identityTokenAuthDAL: TIdentityTokenAuthDALFactory; + identityAccessTokenDAL: TIdentityAccessTokenDALFactory; + identityOrgMembershipDAL: TIdentityOrgDALFactory; serverCfgDAL: TSuperAdminDALFactory; userDAL: TUserDALFactory; + userAliasDAL: Pick; authService: Pick; - kmsService: Pick; + kmsService: Pick; + kmsRootConfigDAL: TKmsRootConfigDALFactory; orgService: Pick; keyStore: Pick; licenseService: Pick; @@ -44,11 +67,17 @@ const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000"; export const superAdminServiceFactory = ({ serverCfgDAL, userDAL, + identityDAL, + userAliasDAL, authService, orgService, keyStore, + kmsRootConfigDAL, kmsService, - licenseService + licenseService, + identityAccessTokenDAL, + identityTokenAuthDAL, + identityOrgMembershipDAL }: TSuperAdminServiceFactoryDep) => { const initServerCfg = async () => { // TODO(akhilmhdh): bad pattern time less change this later to me itself @@ -78,17 +107,21 @@ export const superAdminServiceFactory = ({ // reset on initialized await keyStore.deleteItem(ADMIN_CONFIG_KEY); - const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); - if (serverCfg) return; + const serverCfg = await serverCfgDAL.transaction(async (tx) => { + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.SuperAdminInit]); + const serverCfgInDB = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); + if (serverCfgInDB) return serverCfgInDB; - const newCfg = await serverCfgDAL.create({ - // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition - id: ADMIN_CONFIG_DB_UUID, - initialized: false, - allowSignUp: true, - defaultAuthOrgId: null + const newCfg = await serverCfgDAL.create({ + // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition + id: ADMIN_CONFIG_DB_UUID, + initialized: false, + allowSignUp: true, + defaultAuthOrgId: null + }); + return newCfg; }); - return newCfg; + return serverCfg; }; const updateServerCfg = async ( @@ -99,29 +132,45 @@ export const superAdminServiceFactory = ({ if (data.enabledLoginMethods) { const superAdminUser = await userDAL.findById(userId); + const isSamlConfiguredForUser = Boolean( + await userAliasDAL.findOne({ + userId, + aliasType: UserAliasType.SAML + }) + ); + + // We do not store SAML and OIDC auth values in the user authMethods field + // and so we infer its usage from the user's aliases + const isUserSamlAccessEnabled = isSamlConfiguredForUser && data.enabledLoginMethods.includes(LoginMethod.SAML); + const isOidcConfiguredForUser = Boolean( + await userAliasDAL.findOne({ + userId, + aliasType: UserAliasType.OIDC + }) + ); + + const isUserOidcAccessEnabled = isOidcConfiguredForUser && data.enabledLoginMethods.includes(LoginMethod.OIDC); + const loginMethodToAuthMethod = { [LoginMethod.EMAIL]: [AuthMethod.EMAIL], [LoginMethod.GOOGLE]: [AuthMethod.GOOGLE], [LoginMethod.GITLAB]: [AuthMethod.GITLAB], [LoginMethod.GITHUB]: [AuthMethod.GITHUB], [LoginMethod.LDAP]: [AuthMethod.LDAP], - [LoginMethod.OIDC]: [AuthMethod.OIDC], - [LoginMethod.SAML]: [ - AuthMethod.AZURE_SAML, - AuthMethod.GOOGLE_SAML, - AuthMethod.JUMPCLOUD_SAML, - AuthMethod.KEYCLOAK_SAML, - AuthMethod.OKTA_SAML - ] + [LoginMethod.SAML]: [], + [LoginMethod.OIDC]: [] }; - if ( - !data.enabledLoginMethods.some((loginMethod) => + const canServerAdminAccessAfterApply = + data.enabledLoginMethods.some((loginMethod) => loginMethodToAuthMethod[loginMethod as LoginMethod].some( (authMethod) => superAdminUser.authMethods?.includes(authMethod) ) - ) - ) { + ) || + isUserSamlAccessEnabled || + isUserOidcAccessEnabled; + + if (!canServerAdminAccessAfterApply) { throw new BadRequestError({ message: "You must configure at least one auth method to prevent account lockout" }); @@ -242,26 +291,203 @@ export const superAdminServiceFactory = ({ return { token, user: userInfo, organization }; }; - const getUsers = ({ offset, limit, searchTerm }: TAdminGetUsersDTO) => { + const bootstrapInstance = async ({ email, password, organizationName }: TAdminBootstrapInstanceDTO) => { + const appCfg = getConfig(); + const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); + if (serverCfg?.initialized) { + throw new BadRequestError({ message: "Instance has already been set up" }); + } + + const existingUser = await userDAL.findOne({ email }); + if (existingUser) throw new BadRequestError({ name: "Instance initialization", message: "User already exists" }); + + const userInfo = await userDAL.transaction(async (tx) => { + const newUser = await userDAL.create( + { + firstName: "Admin", + lastName: "User", + username: email, + email, + superAdmin: true, + isGhost: false, + isAccepted: true, + authMethods: [AuthMethod.EMAIL], + isEmailVerified: true + }, + tx + ); + const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(password); + const encKeys = await generateUserSrpKeys(email, password); + + const userEnc = await userDAL.createUserEncryption( + { + userId: newUser.id, + encryptionVersion: 2, + protectedKey: encKeys.protectedKey, + protectedKeyIV: encKeys.protectedKeyIV, + protectedKeyTag: encKeys.protectedKeyTag, + publicKey: encKeys.publicKey, + encryptedPrivateKey: encKeys.encryptedPrivateKey, + iv: encKeys.encryptedPrivateKeyIV, + tag: encKeys.encryptedPrivateKeyTag, + salt: encKeys.salt, + verifier: encKeys.verifier, + serverEncryptedPrivateKeyEncoding: encoding, + serverEncryptedPrivateKeyTag: tag, + serverEncryptedPrivateKeyIV: iv, + serverEncryptedPrivateKey: ciphertext + }, + tx + ); + + return { user: newUser, enc: userEnc }; + }); + + const initialOrganizationName = organizationName ?? "Admin Org"; + + const organization = await orgService.createOrganization({ + userId: userInfo.user.id, + userEmail: userInfo.user.email, + orgName: initialOrganizationName + }); + + const { identity, credentials } = await identityDAL.transaction(async (tx) => { + const newIdentity = await identityDAL.create({ name: "Instance Admin Identity" }, tx); + await identityOrgMembershipDAL.create( + { + identityId: newIdentity.id, + orgId: organization.id, + role: OrgMembershipRole.Admin + }, + tx + ); + + const tokenAuth = await identityTokenAuthDAL.create( + { + identityId: newIdentity.id, + accessTokenMaxTTL: 0, + accessTokenTTL: 0, + accessTokenNumUsesLimit: 0, + accessTokenTrustedIps: JSON.stringify([ + { + type: "ipv4", + prefix: 0, + ipAddress: "0.0.0.0" + }, + { + type: "ipv6", + prefix: 0, + ipAddress: "::" + } + ]) + }, + tx + ); + + const newToken = await identityAccessTokenDAL.create( + { + identityId: newIdentity.id, + isAccessTokenRevoked: false, + accessTokenTTL: tokenAuth.accessTokenTTL, + accessTokenMaxTTL: tokenAuth.accessTokenMaxTTL, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: tokenAuth.accessTokenNumUsesLimit, + name: "Instance Admin Token", + authMethod: IdentityAuthMethod.TOKEN_AUTH + }, + tx + ); + + const generatedAccessToken = jwt.sign( + { + identityId: newIdentity.id, + identityAccessTokenId: newToken.id, + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + } as TIdentityAccessTokenJwtPayload, + appCfg.AUTH_SECRET + ); + + return { identity: newIdentity, auth: tokenAuth, credentials: { token: generatedAccessToken } }; + }); + + await updateServerCfg({ initialized: true, adminIdentityIds: [identity.id] }, userInfo.user.id); + + return { + user: userInfo, + organization, + machineIdentity: { + ...identity, + credentials + } + }; + }; + + const getUsers = ({ offset, limit, searchTerm, adminsOnly }: TAdminGetUsersDTO) => { return userDAL.getUsersByFilter({ limit, offset, searchTerm, - sortBy: "username" + sortBy: "username", + adminsOnly }); }; const deleteUser = async (userId: string) => { - if (!licenseService.onPremFeatures?.instanceUserManagement) { - throw new BadRequestError({ - message: "Failed to delete user due to plan restriction. Upgrade to Infisical's Pro plan." - }); - } - const user = await userDAL.deleteById(userId); return user; }; + const deleteIdentitySuperAdminAccess = async (identityId: string, actorId: string) => { + const identity = await identityDAL.findById(identityId); + if (!identity) { + throw new NotFoundError({ name: "Identity", message: "Identity not found" }); + } + + const currentAdminIdentityIds = (await getServerCfg()).adminIdentityIds ?? []; + if (!currentAdminIdentityIds?.includes(identityId)) { + throw new BadRequestError({ name: "Identity", message: "Identity does not have super admin access" }); + } + + await updateServerCfg({ adminIdentityIds: currentAdminIdentityIds.filter((id) => id !== identityId) }, actorId); + + return identity; + }; + + const deleteUserSuperAdminAccess = async (userId: string) => { + const user = await userDAL.findById(userId); + if (!user) { + throw new NotFoundError({ name: "User", message: "User not found" }); + } + + const updatedUser = userDAL.updateById(userId, { superAdmin: false }); + + return updatedUser; + }; + + const getIdentities = async ({ offset, limit, searchTerm }: TAdminGetIdentitiesDTO) => { + const identities = await identityDAL.getIdentitiesByFilter({ + limit, + offset, + searchTerm, + sortBy: "name" + }); + const serverCfg = await getServerCfg(); + + return identities.map((identity) => ({ + ...identity, + isInstanceAdmin: Boolean(serverCfg?.adminIdentityIds?.includes(identity.id)) + })); + }; + + const grantServerAdminAccessToUser = async (userId: string) => { + if (!licenseService.onPremFeatures?.instanceUserManagement) { + throw new BadRequestError({ + message: "Failed to grant server admin access to user due to plan restriction. Upgrade to Infisical's Pro plan." + }); + } + await userDAL.updateById(userId, { superAdmin: true }); + }; + const getAdminSlackConfig = async () => { const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); @@ -288,12 +514,75 @@ export const superAdminServiceFactory = ({ }; }; + const getConfiguredEncryptionStrategies = async () => { + const appCfg = getConfig(); + + const kmsRootCfg = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID); + + if (!kmsRootCfg) { + throw new NotFoundError({ name: "KmsRootConfig", message: "KMS root configuration not found" }); + } + + const selectedStrategy = kmsRootCfg.encryptionStrategy; + const enabledStrategies: { enabled: boolean; strategy: RootKeyEncryptionStrategy }[] = []; + + if (appCfg.ROOT_ENCRYPTION_KEY || appCfg.ENCRYPTION_KEY) { + const basicStrategy = RootKeyEncryptionStrategy.Software; + + enabledStrategies.push({ + enabled: selectedStrategy === basicStrategy, + strategy: basicStrategy + }); + } + if (appCfg.isHsmConfigured) { + const hsmStrategy = RootKeyEncryptionStrategy.HSM; + + enabledStrategies.push({ + enabled: selectedStrategy === hsmStrategy, + strategy: hsmStrategy + }); + } + + return { + strategies: enabledStrategies + }; + }; + + const updateRootEncryptionStrategy = async (strategy: RootKeyEncryptionStrategy) => { + if (!licenseService.onPremFeatures.hsm) { + throw new BadRequestError({ + message: "Failed to update encryption strategy due to plan restriction. Upgrade to Infisical's Enterprise plan." + }); + } + + const configuredStrategies = await getConfiguredEncryptionStrategies(); + + const foundStrategy = configuredStrategies.strategies.find((s) => s.strategy === strategy); + + if (!foundStrategy) { + throw new BadRequestError({ message: "Invalid encryption strategy" }); + } + + if (foundStrategy.enabled) { + throw new BadRequestError({ message: "The selected encryption strategy is already enabled" }); + } + + await kmsService.updateEncryptionStrategy(strategy); + }; + return { initServerCfg, updateServerCfg, adminSignUp, + bootstrapInstance, getUsers, deleteUser, - getAdminSlackConfig + getIdentities, + getAdminSlackConfig, + updateRootEncryptionStrategy, + getConfiguredEncryptionStrategies, + grantServerAdminAccessToUser, + deleteIdentitySuperAdminAccess, + deleteUserSuperAdminAccess }; }; diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index 2d10941b4..64ec92632 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -16,10 +16,23 @@ export type TAdminSignUpDTO = { userAgent: string; }; +export type TAdminBootstrapInstanceDTO = { + email: string; + password: string; + organizationName: string; +}; + export type TAdminGetUsersDTO = { offset: number; limit: number; searchTerm: string; + adminsOnly: boolean; +}; + +export type TAdminGetIdentitiesDTO = { + offset: number; + limit: number; + searchTerm: string; }; export enum LoginMethod { diff --git a/backend/src/services/telemetry/telemetry-types.ts b/backend/src/services/telemetry/telemetry-types.ts index ddeb24211..ab90a71d4 100644 --- a/backend/src/services/telemetry/telemetry-types.ts +++ b/backend/src/services/telemetry/telemetry-types.ts @@ -13,7 +13,15 @@ export enum PostHogEventTypes { IntegrationCreated = "Integration Created", MachineIdentityCreated = "Machine Identity Created", UserOrgInvitation = "User Org Invitation", - TelemetryInstanceStats = "Self Hosted Instance Stats" + TelemetryInstanceStats = "Self Hosted Instance Stats", + SecretRequestCreated = "Secret Request Created", + SecretRequestDeleted = "Secret Request Deleted", + SignSshKey = "Sign SSH Key", + IssueSshCreds = "Issue SSH Credentials", + IssueSshHostUserCert = "Issue SSH Host User Certificate", + IssueSshHostHostCert = "Issue SSH Host Host Certificate", + SignCert = "Sign PKI Certificate", + IssueCert = "Issue PKI Certificate" } export type TSecretModifiedEvent = { @@ -120,6 +128,81 @@ export type TTelemetryInstanceStatsEvent = { }; }; +export type TSecretRequestCreatedEvent = { + event: PostHogEventTypes.SecretRequestCreated; + properties: { + secretRequestId: string; + organizationId: string; + secretRequestName?: string; + }; +}; + +export type TSecretRequestDeletedEvent = { + event: PostHogEventTypes.SecretRequestDeleted; + properties: { + secretRequestId: string; + organizationId: string; + }; +}; + +export type TSignSshKeyEvent = { + event: PostHogEventTypes.SignSshKey; + properties: { + certificateTemplateId: string; + principals: string[]; + userAgent?: string; + }; +}; + +export type TIssueSshCredsEvent = { + event: PostHogEventTypes.IssueSshCreds; + properties: { + certificateTemplateId: string; + principals: string[]; + userAgent?: string; + }; +}; + +export type TIssueSshHostUserCertEvent = { + event: PostHogEventTypes.IssueSshHostUserCert; + properties: { + sshHostId: string; + hostname: string; + principals: string[]; + userAgent?: string; + }; +}; + +export type TIssueSshHostHostCertEvent = { + event: PostHogEventTypes.IssueSshHostHostCert; + properties: { + sshHostId: string; + hostname: string; + principals: string[]; + userAgent?: string; + }; +}; + +export type TSignCertificateEvent = { + event: PostHogEventTypes.SignCert; + properties: { + caId?: string; + certificateTemplateId?: string; + commonName: string; + userAgent?: string; + }; +}; + +export type TIssueCertificateEvent = { + event: PostHogEventTypes.IssueCert; + properties: { + caId?: string; + certificateTemplateId?: string; + commonName: string; + userAgent?: string; + }; +}; + export type TPostHogEvent = { distinctId: string } & ( | TSecretModifiedEvent | TAdminInitEvent @@ -130,4 +213,12 @@ export type TPostHogEvent = { distinctId: string } & ( | TIntegrationCreatedEvent | TProjectCreateEvent | TTelemetryInstanceStatsEvent + | TSecretRequestCreatedEvent + | TSecretRequestDeletedEvent + | TSignSshKeyEvent + | TIssueSshCredsEvent + | TIssueSshHostUserCertEvent + | TIssueSshHostHostCertEvent + | TSignCertificateEvent + | TIssueCertificateEvent ); diff --git a/backend/src/services/totp/totp-config-dal.ts b/backend/src/services/totp/totp-config-dal.ts new file mode 100644 index 000000000..15abb729a --- /dev/null +++ b/backend/src/services/totp/totp-config-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TTotpConfigDALFactory = ReturnType; + +export const totpConfigDALFactory = (db: TDbClient) => { + const totpConfigDal = ormify(db, TableName.TotpConfig); + + return totpConfigDal; +}; diff --git a/backend/src/services/totp/totp-fns.ts b/backend/src/services/totp/totp-fns.ts new file mode 100644 index 000000000..9e9aae52c --- /dev/null +++ b/backend/src/services/totp/totp-fns.ts @@ -0,0 +1,3 @@ +import crypto from "node:crypto"; + +export const generateRecoveryCode = () => String(crypto.randomInt(10 ** 7, 10 ** 8 - 1)); diff --git a/backend/src/services/totp/totp-service.ts b/backend/src/services/totp/totp-service.ts new file mode 100644 index 000000000..591a66ed6 --- /dev/null +++ b/backend/src/services/totp/totp-service.ts @@ -0,0 +1,270 @@ +import { authenticator } from "otplib"; + +import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; + +import { TKmsServiceFactory } from "../kms/kms-service"; +import { TUserDALFactory } from "../user/user-dal"; +import { TTotpConfigDALFactory } from "./totp-config-dal"; +import { generateRecoveryCode } from "./totp-fns"; +import { + TCreateUserTotpRecoveryCodesDTO, + TDeleteUserTotpConfigDTO, + TGetUserTotpConfigDTO, + TRegisterUserTotpDTO, + TVerifyUserTotpConfigDTO, + TVerifyUserTotpDTO, + TVerifyWithUserRecoveryCodeDTO +} from "./totp-types"; + +type TTotpServiceFactoryDep = { + userDAL: TUserDALFactory; + totpConfigDAL: TTotpConfigDALFactory; + kmsService: TKmsServiceFactory; +}; + +export type TTotpServiceFactory = ReturnType; + +const MAX_RECOVERY_CODE_LIMIT = 10; + +export const totpServiceFactory = ({ totpConfigDAL, kmsService, userDAL }: TTotpServiceFactoryDep) => { + const getUserTotpConfig = async ({ userId }: TGetUserTotpConfigDTO) => { + const totpConfig = await totpConfigDAL.findOne({ + userId + }); + + if (!totpConfig) { + throw new NotFoundError({ + message: "TOTP configuration not found" + }); + } + + if (!totpConfig.isVerified) { + throw new BadRequestError({ + message: "TOTP configuration has not been verified" + }); + } + + const decryptWithRoot = kmsService.decryptWithRootKey(); + const recoveryCodes = decryptWithRoot(totpConfig.encryptedRecoveryCodes).toString().split(","); + + return { + isVerified: totpConfig.isVerified, + recoveryCodes + }; + }; + + const registerUserTotp = async ({ userId }: TRegisterUserTotpDTO) => { + const totpConfig = await totpConfigDAL.transaction(async (tx) => { + const verifiedTotpConfig = await totpConfigDAL.findOne( + { + userId, + isVerified: true + }, + tx + ); + + if (verifiedTotpConfig) { + throw new BadRequestError({ + message: "TOTP configuration for user already exists" + }); + } + + const unverifiedTotpConfig = await totpConfigDAL.findOne({ + userId, + isVerified: false + }); + + if (unverifiedTotpConfig) { + return unverifiedTotpConfig; + } + + const encryptWithRoot = kmsService.encryptWithRootKey(); + + // create new TOTP configuration + const secret = authenticator.generateSecret(); + const encryptedSecret = encryptWithRoot(Buffer.from(secret)); + const recoveryCodes = Array.from({ length: MAX_RECOVERY_CODE_LIMIT }).map(generateRecoveryCode); + const encryptedRecoveryCodes = encryptWithRoot(Buffer.from(recoveryCodes.join(","))); + const newTotpConfig = await totpConfigDAL.create({ + userId, + encryptedRecoveryCodes, + encryptedSecret + }); + + return newTotpConfig; + }); + + const user = await userDAL.findById(userId); + const decryptWithRoot = kmsService.decryptWithRootKey(); + + const secret = decryptWithRoot(totpConfig.encryptedSecret).toString(); + const recoveryCodes = decryptWithRoot(totpConfig.encryptedRecoveryCodes).toString().split(","); + const otpUrl = authenticator.keyuri(user.username, "Infisical", secret); + + return { + otpUrl, + recoveryCodes + }; + }; + + const verifyUserTotpConfig = async ({ userId, totp }: TVerifyUserTotpConfigDTO) => { + const totpConfig = await totpConfigDAL.findOne({ + userId + }); + + if (!totpConfig) { + throw new NotFoundError({ + message: "TOTP configuration not found" + }); + } + + if (totpConfig.isVerified) { + throw new BadRequestError({ + message: "TOTP configuration has already been verified" + }); + } + + const decryptWithRoot = kmsService.decryptWithRootKey(); + const secret = decryptWithRoot(totpConfig.encryptedSecret).toString(); + const isValid = authenticator.verify({ + token: totp, + secret + }); + + if (isValid) { + await totpConfigDAL.updateById(totpConfig.id, { + isVerified: true + }); + } else { + throw new BadRequestError({ + message: "Invalid TOTP token" + }); + } + }; + + const verifyUserTotp = async ({ userId, totp }: TVerifyUserTotpDTO) => { + const totpConfig = await totpConfigDAL.findOne({ + userId + }); + + if (!totpConfig) { + throw new NotFoundError({ + message: "TOTP configuration not found" + }); + } + + if (!totpConfig.isVerified) { + throw new BadRequestError({ + message: "TOTP configuration has not been verified" + }); + } + + const decryptWithRoot = kmsService.decryptWithRootKey(); + const secret = decryptWithRoot(totpConfig.encryptedSecret).toString(); + const isValid = authenticator.verify({ + token: totp, + secret + }); + + if (!isValid) { + throw new ForbiddenRequestError({ + message: "Invalid TOTP" + }); + } + }; + + const verifyWithUserRecoveryCode = async ({ userId, recoveryCode }: TVerifyWithUserRecoveryCodeDTO) => { + const totpConfig = await totpConfigDAL.findOne({ + userId + }); + + if (!totpConfig) { + throw new NotFoundError({ + message: "TOTP configuration not found" + }); + } + + if (!totpConfig.isVerified) { + throw new BadRequestError({ + message: "TOTP configuration has not been verified" + }); + } + + const decryptWithRoot = kmsService.decryptWithRootKey(); + const encryptWithRoot = kmsService.encryptWithRootKey(); + + const recoveryCodes = decryptWithRoot(totpConfig.encryptedRecoveryCodes).toString().split(","); + const matchingCode = recoveryCodes.find((code) => recoveryCode === code); + if (!matchingCode) { + throw new ForbiddenRequestError({ + message: "Invalid TOTP recovery code" + }); + } + + const updatedRecoveryCodes = recoveryCodes.filter((code) => code !== matchingCode); + const encryptedRecoveryCodes = encryptWithRoot(Buffer.from(updatedRecoveryCodes.join(","))); + await totpConfigDAL.updateById(totpConfig.id, { + encryptedRecoveryCodes + }); + }; + + const deleteUserTotpConfig = async ({ userId }: TDeleteUserTotpConfigDTO) => { + const totpConfig = await totpConfigDAL.findOne({ + userId + }); + + if (!totpConfig) { + throw new NotFoundError({ + message: "TOTP configuration not found" + }); + } + + await totpConfigDAL.deleteById(totpConfig.id); + }; + + const createUserTotpRecoveryCodes = async ({ userId }: TCreateUserTotpRecoveryCodesDTO) => { + const decryptWithRoot = kmsService.decryptWithRootKey(); + const encryptWithRoot = kmsService.encryptWithRootKey(); + + return totpConfigDAL.transaction(async (tx) => { + const totpConfig = await totpConfigDAL.findOne( + { + userId, + isVerified: true + }, + tx + ); + + if (!totpConfig) { + throw new NotFoundError({ + message: "Valid TOTP configuration not found" + }); + } + + const recoveryCodes = decryptWithRoot(totpConfig.encryptedRecoveryCodes).toString().split(","); + if (recoveryCodes.length >= MAX_RECOVERY_CODE_LIMIT) { + throw new BadRequestError({ + message: `Cannot have more than ${MAX_RECOVERY_CODE_LIMIT} recovery codes at a time` + }); + } + + const toGenerateCount = MAX_RECOVERY_CODE_LIMIT - recoveryCodes.length; + const newRecoveryCodes = Array.from({ length: toGenerateCount }).map(generateRecoveryCode); + const encryptedRecoveryCodes = encryptWithRoot(Buffer.from([...recoveryCodes, ...newRecoveryCodes].join(","))); + + await totpConfigDAL.updateById(totpConfig.id, { + encryptedRecoveryCodes + }); + }); + }; + + return { + registerUserTotp, + verifyUserTotpConfig, + getUserTotpConfig, + verifyUserTotp, + verifyWithUserRecoveryCode, + deleteUserTotpConfig, + createUserTotpRecoveryCodes + }; +}; diff --git a/backend/src/services/totp/totp-types.ts b/backend/src/services/totp/totp-types.ts new file mode 100644 index 000000000..15c015619 --- /dev/null +++ b/backend/src/services/totp/totp-types.ts @@ -0,0 +1,30 @@ +export type TRegisterUserTotpDTO = { + userId: string; +}; + +export type TVerifyUserTotpConfigDTO = { + userId: string; + totp: string; +}; + +export type TGetUserTotpConfigDTO = { + userId: string; +}; + +export type TVerifyUserTotpDTO = { + userId: string; + totp: string; +}; + +export type TVerifyWithUserRecoveryCodeDTO = { + userId: string; + recoveryCode: string; +}; + +export type TDeleteUserTotpConfigDTO = { + userId: string; +}; + +export type TCreateUserTotpRecoveryCodesDTO = { + userId: string; +}; diff --git a/backend/src/services/user-engagement/user-engagement-service.ts b/backend/src/services/user-engagement/user-engagement-service.ts index 5d7b54929..b14672903 100644 --- a/backend/src/services/user-engagement/user-engagement-service.ts +++ b/backend/src/services/user-engagement/user-engagement-service.ts @@ -1,87 +1,44 @@ -import { PlainClient } from "@team-plain/typescript-sdk"; +import axios from "axios"; import { getConfig } from "@app/lib/config/env"; import { InternalServerError } from "@app/lib/errors"; +import { TOrgDALFactory } from "../org/org-dal"; import { TUserDALFactory } from "../user/user-dal"; type TUserEngagementServiceFactoryDep = { userDAL: Pick; + orgDAL: Pick; }; export type TUserEngagementServiceFactory = ReturnType; -export const userEngagementServiceFactory = ({ userDAL }: TUserEngagementServiceFactoryDep) => { - const createUserWish = async (userId: string, text: string) => { +export const userEngagementServiceFactory = ({ userDAL, orgDAL }: TUserEngagementServiceFactoryDep) => { + const createUserWish = async (userId: string, orgId: string, text: string) => { const user = await userDAL.findById(userId); + const org = await orgDAL.findById(orgId); const appCfg = getConfig(); - if (!appCfg.PLAIN_API_KEY) { + if (!appCfg.PYLON_API_KEY) { throw new InternalServerError({ - message: "Plain is not configured." + message: "Pylon is not configured." }); } - const client = new PlainClient({ - apiKey: appCfg.PLAIN_API_KEY - }); - - const customerUpsertRes = await client.upsertCustomer({ - identifier: { - emailAddress: user.email - }, - onCreate: { - fullName: `${user.firstName} ${user.lastName}`, - shortName: user.firstName, - email: { - email: user.email as string, - isVerified: user.isEmailVerified as boolean - }, - - externalId: user.id - }, - - onUpdate: { - fullName: { - value: `${user.firstName} ${user.lastName}` - }, - shortName: { - value: user.firstName - }, - email: { - email: user.email as string, - isVerified: user.isEmailVerified as boolean - }, - externalId: { - value: user.id - } + const request = axios.create({ + baseURL: "https://api.usepylon.com", + headers: { + Authorization: `Bearer ${appCfg.PYLON_API_KEY}` } }); - if (customerUpsertRes.error) { - throw new InternalServerError({ message: customerUpsertRes.error.message }); - } - - const createThreadRes = await client.createThread({ - title: "Wish", - customerIdentifier: { - externalId: customerUpsertRes.data.customer.externalId - }, - components: [ - { - componentText: { - text - } - } - ], - labelTypeIds: appCfg.PLAIN_WISH_LABEL_IDS?.split(",") + await request.post("/issues", { + title: `New Wish From: ${user.firstName} ${user.lastName} (${org.name})`, + body_html: text, + requester_email: user.email, + requester_name: `${user.firstName} ${user.lastName} (${org.name})`, + tags: ["wish"] }); - - if (createThreadRes.error) { - throw new InternalServerError({ - message: createThreadRes.error.message - }); - } }; return { createUserWish diff --git a/backend/src/services/user/user-dal.ts b/backend/src/services/user/user-dal.ts index 99f403e84..eba497f0f 100644 --- a/backend/src/services/user/user-dal.ts +++ b/backend/src/services/user/user-dal.ts @@ -23,15 +23,18 @@ export const userDALFactory = (db: TDbClient) => { limit, offset, searchTerm, - sortBy + sortBy, + adminsOnly }: { limit: number; offset: number; searchTerm: string; sortBy?: keyof TUsers; + adminsOnly: boolean; }) => { try { let query = db.replicaNode()(TableName.Users).where("isGhost", "=", false); + if (searchTerm) { query = query.where((qb) => { void qb @@ -42,6 +45,10 @@ export const userDALFactory = (db: TDbClient) => { }); } + if (adminsOnly) { + query = query.where("superAdmin", true); + } + if (sortBy) { query = query.orderBy(sortBy); } diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts index b8cf3c7a8..5da5d493c 100644 --- a/backend/src/services/user/user-service.ts +++ b/backend/src/services/user/user-service.ts @@ -15,7 +15,7 @@ import { AuthMethod } from "../auth/auth-type"; import { TGroupProjectDALFactory } from "../group-project/group-project-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TUserDALFactory } from "./user-dal"; -import { TListUserGroupsDTO } from "./user-types"; +import { TListUserGroupsDTO, TUpdateUserMfaDTO } from "./user-types"; type TUserServiceFactoryDep = { userDAL: Pick< @@ -171,15 +171,24 @@ export const userServiceFactory = ({ }); }; - const toggleUserMfa = async (userId: string, isMfaEnabled: boolean) => { + const updateUserMfa = async ({ userId, isMfaEnabled, selectedMfaMethod }: TUpdateUserMfaDTO) => { const user = await userDAL.findById(userId); if (!user || !user.email) throw new BadRequestError({ name: "Failed to toggle MFA" }); + let mfaMethods; + if (isMfaEnabled === undefined) { + mfaMethods = undefined; + } else { + mfaMethods = isMfaEnabled ? ["email"] : []; + } + const updatedUser = await userDAL.updateById(userId, { isMfaEnabled, - mfaMethods: isMfaEnabled ? ["email"] : [] + mfaMethods, + selectedMfaMethod }); + return updatedUser; }; @@ -327,7 +336,7 @@ export const userServiceFactory = ({ return { sendEmailVerificationCode, verifyEmailVerificationCode, - toggleUserMfa, + updateUserMfa, updateUserName, updateAuthMethods, deleteUser, diff --git a/backend/src/services/user/user-types.ts b/backend/src/services/user/user-types.ts index 9b482de98..cef13f27a 100644 --- a/backend/src/services/user/user-types.ts +++ b/backend/src/services/user/user-types.ts @@ -1,5 +1,7 @@ import { TOrgPermission } from "@app/lib/types"; +import { MfaMethod } from "../auth/auth-type"; + export type TListUserGroupsDTO = { username: string; } & Omit; @@ -8,3 +10,9 @@ export enum UserEncryption { V1 = 1, V2 = 2 } + +export type TUpdateUserMfaDTO = { + userId: string; + isMfaEnabled?: boolean; + selectedMfaMethod?: MfaMethod; +}; diff --git a/backend/src/services/webhook/webhook-fns.ts b/backend/src/services/webhook/webhook-fns.ts index ffa4b4a04..a16158e14 100644 --- a/backend/src/services/webhook/webhook-fns.ts +++ b/backend/src/services/webhook/webhook-fns.ts @@ -3,41 +3,26 @@ import crypto from "node:crypto"; import { AxiosError } from "axios"; import picomatch from "picomatch"; -import { SecretKeyEncoding, TWebhooks } from "@app/db/schemas"; +import { TWebhooks } from "@app/db/schemas"; import { request } from "@app/lib/config/request"; -import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TWebhookDALFactory } from "./webhook-dal"; -import { WebhookType } from "./webhook-types"; +import { TWebhookPayloads, WebhookEvents, WebhookType } from "./webhook-types"; const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000; -export const decryptWebhookDetails = (webhook: TWebhooks) => { - const { keyEncoding, iv, encryptedSecretKey, tag, urlCipherText, urlIV, urlTag, url } = webhook; +export const decryptWebhookDetails = (webhook: TWebhooks, decryptor: (value: Buffer) => string) => { + const { encryptedPassKey, encryptedUrl } = webhook; + + const decryptedUrl = decryptor(encryptedUrl); let decryptedSecretKey = ""; - let decryptedUrl = url; - - if (encryptedSecretKey) { - decryptedSecretKey = infisicalSymmetricDecrypt({ - keyEncoding: keyEncoding as SecretKeyEncoding, - ciphertext: encryptedSecretKey, - iv: iv as string, - tag: tag as string - }); - } - - if (urlCipherText) { - decryptedUrl = infisicalSymmetricDecrypt({ - keyEncoding: keyEncoding as SecretKeyEncoding, - ciphertext: urlCipherText, - iv: urlIV as string, - tag: urlTag as string - }); + if (encryptedPassKey) { + decryptedSecretKey = decryptor(encryptedPassKey); } return { @@ -46,10 +31,14 @@ export const decryptWebhookDetails = (webhook: TWebhooks) => { }; }; -export const triggerWebhookRequest = async (webhook: TWebhooks, data: Record) => { +export const triggerWebhookRequest = async ( + webhook: TWebhooks, + decryptor: (value: Buffer) => string, + data: Record +) => { const headers: Record = {}; const payload = { ...data, timestamp: Date.now() }; - const { secretKey, url } = decryptWebhookDetails(webhook); + const { secretKey, url } = decryptWebhookDetails(webhook, decryptor); if (secretKey) { const webhookSign = crypto.createHmac("sha256", secretKey).update(JSON.stringify(payload)).digest("hex"); @@ -65,29 +54,64 @@ export const triggerWebhookRequest = async (webhook: TWebhooks, data: Record { + if (event.type === WebhookEvents.SecretModified) { + const { projectName, projectId, environment, secretPath, type } = event.payload; + + switch (type) { + case WebhookType.SLACK: + return { + text: "A secret value has been added or modified.", + attachments: [ + { + color: "#E7F256", + fields: [ + { + title: "Project", + value: projectName, + short: false + }, + { + title: "Environment", + value: environment, + short: false + }, + { + title: "Secret Path", + value: secretPath, + short: false + } + ] + } + ] + }; + case WebhookType.GENERAL: + default: + return { + event: event.type, + project: { + workspaceId: projectId, + projectName, + environment, + secretPath + } + }; + } } -) => { - const { workspaceName, workspaceId, environment, secretPath, type } = details; + + const { projectName, projectId, environment, secretPath, type, reminderNote, secretName } = event.payload; switch (type) { case WebhookType.SLACK: return { - text: "A secret value has been added or modified.", + text: "You have a secret reminder", attachments: [ { color: "#E7F256", fields: [ { title: "Project", - value: workspaceName, + value: projectName, short: false }, { @@ -99,6 +123,16 @@ export const getWebhookPayload = ( title: "Secret Path", value: secretPath, short: false + }, + { + title: "Secret Name", + value: secretName, + short: false + }, + { + title: "Reminder Note", + value: reminderNote, + short: false } ] } @@ -107,11 +141,14 @@ export const getWebhookPayload = ( case WebhookType.GENERAL: default: return { - event: eventName, + event: event.type, project: { - workspaceId, + workspaceId: projectId, + projectName, environment, - secretPath + secretPath, + secretName, + reminderNote } }; } @@ -121,9 +158,11 @@ export type TFnTriggerWebhookDTO = { projectId: string; secretPath: string; environment: string; + event: TWebhookPayloads; webhookDAL: Pick; projectEnvDAL: Pick; projectDAL: Pick; + secretManagerDecryptor: (value: Buffer) => string; }; // this is reusable function @@ -134,6 +173,8 @@ export const fnTriggerWebhook = async ({ projectId, webhookDAL, projectEnvDAL, + event, + secretManagerDecryptor, projectDAL }: TFnTriggerWebhookDTO) => { const webhooks = await webhookDAL.findAllWebhooks(projectId, environment); @@ -142,21 +183,21 @@ export const fnTriggerWebhook = async ({ !isDisabled && picomatch.isMatch(secretPath, hookSecretPath, { strictSlashes: false }) ); if (!toBeTriggeredHooks.length) return; - logger.info("Secret webhook job started", { environment, secretPath, projectId }); - const project = await projectDAL.findById(projectId); + logger.info({ environment, secretPath, projectId }, "Secret webhook job started"); + let { projectName } = event.payload; + if (!projectName) { + const project = await projectDAL.findById(event.payload.projectId); + projectName = project.name; + } + const webhooksTriggered = await Promise.allSettled( - toBeTriggeredHooks.map((hook) => - triggerWebhookRequest( - hook, - getWebhookPayload("secrets.modified", { - workspaceName: project.name, - workspaceId: projectId, - environment, - secretPath, - type: hook.type - }) - ) - ) + toBeTriggeredHooks.map((hook) => { + const formattedEvent = { + type: event.type, + payload: { ...event.payload, type: hook.type, projectName } + } as TWebhookPayloads; + return triggerWebhookRequest(hook, secretManagerDecryptor, getWebhookPayload(formattedEvent)); + }) ); // filter hooks by status @@ -195,5 +236,5 @@ export const fnTriggerWebhook = async ({ ); } }); - logger.info("Secret webhook job ended", { environment, secretPath, projectId }); + logger.info({ environment, secretPath, projectId }, "Secret webhook job ended"); }; diff --git a/backend/src/services/webhook/webhook-service.ts b/backend/src/services/webhook/webhook-service.ts index a959d904c..c555dc8d1 100644 --- a/backend/src/services/webhook/webhook-service.ts +++ b/backend/src/services/webhook/webhook-service.ts @@ -1,11 +1,12 @@ import { ForbiddenError } from "@casl/ability"; -import { TWebhooksInsert } from "@app/db/schemas"; +import { ActionProjectType, TWebhooksInsert } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { NotFoundError } from "@app/lib/errors"; +import { TKmsServiceFactory } from "../kms/kms-service"; +import { KmsDataKey } from "../kms/kms-types"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TWebhookDALFactory } from "./webhook-dal"; @@ -15,7 +16,8 @@ import { TDeleteWebhookDTO, TListWebhookDTO, TTestWebhookDTO, - TUpdateWebhookDTO + TUpdateWebhookDTO, + WebhookEvents } from "./webhook-types"; type TWebhookServiceFactoryDep = { @@ -23,6 +25,7 @@ type TWebhookServiceFactoryDep = { projectEnvDAL: TProjectEnvDALFactory; projectDAL: Pick; permissionService: Pick; + kmsService: Pick; }; export type TWebhookServiceFactory = ReturnType; @@ -31,7 +34,8 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionService, - projectDAL + projectDAL, + kmsService }: TWebhookServiceFactoryDep) => { const createWebhook = async ({ actor, @@ -45,13 +49,14 @@ export const webhookServiceFactory = ({ webhookSecretKey, type }: TCreateWebhookDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks); const env = await projectEnvDAL.findOne({ projectId, slug: environment }); if (!env) @@ -59,30 +64,20 @@ export const webhookServiceFactory = ({ message: `Environment with slug '${environment}' in project with ID '${projectId}' not found` }); + const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); const insertDoc: TWebhooksInsert = { - url: "", // deprecated - we are moving away from plaintext URLs envId: env.id, isDisabled: false, secretPath: secretPath || "/", - type + type, + encryptedUrl: secretManagerEncryptor({ plainText: Buffer.from(webhookUrl) }).cipherTextBlob }; if (webhookSecretKey) { - const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookSecretKey); - insertDoc.encryptedSecretKey = ciphertext; - insertDoc.iv = iv; - insertDoc.tag = tag; - insertDoc.algorithm = algorithm; - insertDoc.keyEncoding = encoding; - } - - if (webhookUrl) { - const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookUrl); - insertDoc.urlCipherText = ciphertext; - insertDoc.urlIV = iv; - insertDoc.urlTag = tag; - insertDoc.algorithm = algorithm; - insertDoc.keyEncoding = encoding; + insertDoc.encryptedPassKey = secretManagerEncryptor({ plainText: Buffer.from(webhookSecretKey) }).cipherTextBlob; } const webhook = await webhookDAL.create(insertDoc); @@ -93,13 +88,14 @@ export const webhookServiceFactory = ({ const webhook = await webhookDAL.findById(id); if (!webhook) throw new NotFoundError({ message: `Webhook with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - webhook.projectId, + projectId: webhook.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks); const updatedWebhook = await webhookDAL.updateById(id, { isDisabled }); @@ -110,13 +106,14 @@ export const webhookServiceFactory = ({ const webhook = await webhookDAL.findById(id); if (!webhook) throw new NotFoundError({ message: `Webhook with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - webhook.projectId, + projectId: webhook.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks); const deletedWebhook = await webhookDAL.deleteById(id); @@ -127,27 +124,36 @@ export const webhookServiceFactory = ({ const webhook = await webhookDAL.findById(id); if (!webhook) throw new NotFoundError({ message: `Webhook with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - webhook.projectId, + projectId: webhook.projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); const project = await projectDAL.findById(webhook.projectId); + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: project.id + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); let webhookError: string | undefined; try { await triggerWebhookRequest( webhook, - getWebhookPayload("test", { - workspaceName: project.name, - workspaceId: webhook.projectId, - environment: webhook.environment.slug, - secretPath: webhook.secretPath, - type: webhook.type + (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString(), + getWebhookPayload({ + type: "test" as WebhookEvents.SecretModified, + payload: { + projectName: project.name, + projectId: webhook.projectId, + environment: webhook.environment.slug, + secretPath: webhook.secretPath, + type: webhook.type + } }) ); } catch (err) { @@ -170,18 +176,24 @@ export const webhookServiceFactory = ({ secretPath, environment }: TListWebhookDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + actionProjectType: ActionProjectType.Any + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); const webhooks = await webhookDAL.findAllWebhooks(projectId, environment, secretPath); + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + return webhooks.map((w) => { - const { url } = decryptWebhookDetails(w); + const { url } = decryptWebhookDetails(w, (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString()); return { ...w, url diff --git a/backend/src/services/webhook/webhook-types.ts b/backend/src/services/webhook/webhook-types.ts index 40dacb42a..8ce2c8d8e 100644 --- a/backend/src/services/webhook/webhook-types.ts +++ b/backend/src/services/webhook/webhook-types.ts @@ -30,3 +30,36 @@ export enum WebhookType { GENERAL = "general", SLACK = "slack" } + +export enum WebhookEvents { + SecretModified = "secrets.modified", + SecretReminderExpired = "secrets.reminder-expired", + TestEvent = "test" +} + +type TWebhookSecretModifiedEventPayload = { + type: WebhookEvents.SecretModified; + payload: { + projectName?: string; + projectId: string; + environment: string; + secretPath?: string; + type?: string | null; + }; +}; + +type TWebhookSecretReminderEventPayload = { + type: WebhookEvents.SecretReminderExpired; + payload: { + projectName?: string; + projectId: string; + environment: string; + secretPath?: string; + type?: string | null; + secretName: string; + secretId: string; + reminderNote?: string | null; + }; +}; + +export type TWebhookPayloads = TWebhookSecretModifiedEventPayload | TWebhookSecretReminderEventPayload; diff --git a/backend/tsconfig.json b/backend/tsconfig.json index fcf508922..90165acbe 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -1,7 +1,8 @@ { "ts-node": { // Do not forget to `npm i -D tsconfig-paths` - "require": ["tsconfig-paths/register"] + "require": ["tsconfig-paths/register"], + "files": true }, "compilerOptions": { "target": "esnext", @@ -19,6 +20,7 @@ "experimentalDecorators": true, "emitDecoratorMetadata": true, "moduleResolution": "Node", + "allowSyntheticDefaultImports": true, "skipLibCheck": true, "baseUrl": ".", "paths": { diff --git a/backend/vitest.unit.config.ts b/backend/vitest.unit.config.ts new file mode 100644 index 000000000..97862d288 --- /dev/null +++ b/backend/vitest.unit.config.ts @@ -0,0 +1,17 @@ +import path from "path"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + env: { + NODE_ENV: "test" + }, + include: ["./src/**/*.test.ts"] + }, + resolve: { + alias: { + "@app": path.resolve(__dirname, "./src") + } + } +}); diff --git a/cli/config/example-infisical-relay.yaml b/cli/config/example-infisical-relay.yaml new file mode 100644 index 000000000..c913ed757 --- /dev/null +++ b/cli/config/example-infisical-relay.yaml @@ -0,0 +1,8 @@ +public_ip: 127.0.0.1 +auth_secret: example-auth-secret +realm: infisical.org +# set port 5349 for tls +# port: 5349 +# tls_private_key_path: /full-path +# tls_ca_path: /full-path +# tls_cert_path: /full-path diff --git a/cli/config/infisical-relay.yaml b/cli/config/infisical-relay.yaml new file mode 100644 index 000000000..89c6b5e45 --- /dev/null +++ b/cli/config/infisical-relay.yaml @@ -0,0 +1,8 @@ +public_ip: 127.0.0.1 +auth_secret: changeThisOnProduction +realm: infisical.org +# set port 5349 for tls +# port: 5349 +# tls_private_key_path: /full-path +# tls_ca_path: /full-path +# tls_cert_path: /full-path diff --git a/cli/go.mod b/cli/go.mod index 36277ac37..c713417e2 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -1,6 +1,8 @@ module github.com/Infisical/infisical-merge -go 1.21 +go 1.23.0 + +toolchain go1.23.5 require ( github.com/bradleyjkemp/cupaloy/v2 v2.8.0 @@ -10,22 +12,29 @@ require ( github.com/fatih/semgroup v1.2.0 github.com/gitleaks/go-gitdiff v0.8.0 github.com/h2non/filetype v1.1.3 - github.com/infisical/go-sdk v0.3.8 + github.com/infisical/go-sdk v0.5.8 + github.com/infisical/infisical-kmip v0.3.5 github.com/mattn/go-isatty v0.0.20 github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a github.com/muesli/mango-cobra v1.2.0 github.com/muesli/reflow v0.3.0 github.com/muesli/roff v0.1.0 github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 + github.com/pion/dtls/v3 v3.0.4 + github.com/pion/logging v0.2.3 + github.com/pion/turn/v4 v4.0.0 github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a + github.com/quic-go/quic-go v0.50.0 github.com/rs/cors v1.11.0 github.com/rs/zerolog v1.26.1 github.com/spf13/cobra v1.6.1 github.com/spf13/viper v1.8.1 - github.com/stretchr/testify v1.9.0 - golang.org/x/crypto v0.25.0 - golang.org/x/term v0.22.0 + github.com/stretchr/testify v1.10.0 + golang.org/x/crypto v0.36.0 + golang.org/x/sys v0.31.0 + golang.org/x/term v0.30.0 gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -55,16 +64,21 @@ require ( github.com/dvsekhvalnov/jose2go v1.6.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/errors v0.20.2 // indirect github.com/go-openapi/strfmt v0.21.3 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect + github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.5 // indirect + github.com/gosimple/slug v1.15.0 // indirect + github.com/gosimple/unidecode v1.0.1 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.5 // indirect @@ -76,13 +90,19 @@ require ( github.com/muesli/mango-pflag v0.1.0 // indirect github.com/muesli/termenv v0.15.2 // indirect github.com/oklog/ulid v1.3.1 // indirect + github.com/onsi/ginkgo/v2 v2.22.2 // indirect github.com/pelletier/go-toml v1.9.3 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/stun/v3 v3.0.0 // indirect + github.com/pion/transport/v3 v3.0.7 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/spf13/afero v1.6.0 // indirect github.com/spf13/cast v1.3.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect + github.com/wlynxg/anet v0.0.5 // indirect github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect go.mongodb.org/mongo-driver v1.10.0 // indirect go.opencensus.io v0.24.0 // indirect @@ -91,24 +111,26 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/net v0.27.0 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/net v0.35.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect - golang.org/x/time v0.5.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/time v0.6.0 // indirect + golang.org/x/tools v0.30.0 // indirect google.golang.org/api v0.188.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b // indirect google.golang.org/grpc v1.64.1 // indirect - google.golang.org/protobuf v1.34.2 // indirect + google.golang.org/protobuf v1.36.1 // indirect gopkg.in/ini.v1 v1.62.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( github.com/fatih/color v1.17.0 - github.com/go-resty/resty/v2 v2.13.1 + github.com/go-resty/resty/v2 v2.16.5 github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/jedib0t/go-pretty v4.3.0+incompatible github.com/manifoldco/promptui v0.9.0 @@ -117,3 +139,5 @@ require ( ) replace github.com/zalando/go-keyring => github.com/Infisical/go-keyring v1.0.2 + +replace github.com/pion/turn/v4 => github.com/Infisical/turn/v4 v4.0.1 diff --git a/cli/go.sum b/cli/go.sum index 733a7b93d..68bce9cd3 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -49,6 +49,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Infisical/go-keyring v1.0.2 h1:dWOkI/pB/7RocfSJgGXbXxLDcVYsdslgjEPmVhb+nl8= github.com/Infisical/go-keyring v1.0.2/go.mod h1:LWOnn/sw9FxDW/0VY+jHFAfOFEe03xmwBVSfJnBowto= +github.com/Infisical/turn/v4 v4.0.1 h1:omdelNsnFfzS5cu86W5OBR68by68a8sva4ogR0lQQnw= +github.com/Infisical/turn/v4 v4.0.1/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs= github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= @@ -144,16 +146,18 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/errors v0.20.2 h1:dxy7PGTqEh94zj2E3h1cUmQQWiM1+aeCROfAr02EmK8= github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o= github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= -github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g= -github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0= +github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= +github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -222,6 +226,8 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 h1:+J3r2e8+RsmN3vKfo75g0YSY61ms37qzPglu4p0sGro= +github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= @@ -237,6 +243,10 @@ github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBY github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo= +github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= +github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= +github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= @@ -255,6 +265,8 @@ github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= @@ -265,8 +277,10 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/infisical/go-sdk v0.3.8 h1:0dGOhF3cwt0q5QzpnUs4lxwBiEza+DQYOyvEn7AfrM0= -github.com/infisical/go-sdk v0.3.8/go.mod h1:HHW7DgUqoolyQIUw/9HdpkZ3bDLwWyZ0HEtYiVaDKQw= +github.com/infisical/go-sdk v0.5.8 h1:bCetYLp7HWt8DnU9KPh1n8n3z5pjmunkGDB4bA3lEFs= +github.com/infisical/go-sdk v0.5.8/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs= +github.com/infisical/infisical-kmip v0.3.5 h1:QM3s0e18B+mYv3a9HQNjNAlbwZJBzXq5BAJM2scIeiE= +github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs= github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -334,12 +348,27 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= +github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 h1:lL+y4Xv20pVlCGyLzNHRC0I0rIHhIL1lTvHizoS/dU8= github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw= +github.com/pion/dtls/v3 v3.0.4 h1:44CZekewMzfrn9pmGrj5BNnTMDCFwr+6sLH+cCuLM7U= +github.com/pion/dtls/v3 v3.0.4/go.mod h1:R373CsjxWqNPf6MEkfdy3aSe9niZvL/JaKlGeFphtMg= +github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI= +github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= +github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -348,6 +377,8 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a h1:Ey0XWvrg6u6hyIn1Kd/jCCmL+bMv9El81tvuGBbxZGg= github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a/go.mod h1:oa2sAs9tGai3VldabTV0eWejt/O4/OOD7azP8GaikqU= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/quic-go/quic-go v0.50.0 h1:3H/ld1pa3CYhkcc20TPIyG1bNsdhn9qZBGN3b9/UyUo= +github.com/quic-go/quic-go v0.50.0/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -394,13 +425,15 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= @@ -413,7 +446,6 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= @@ -439,6 +471,8 @@ go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8p go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -448,13 +482,10 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -465,6 +496,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 h1:aWwlzYV971S4BXRS9AmqwDLAD85ouC6X+pocatKY58c= +golang.org/x/exp v0.0.0-20250228200357-dead58393ab7/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -490,8 +523,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -530,13 +563,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -562,10 +590,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -612,24 +638,13 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -639,17 +654,13 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -702,8 +713,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -818,8 +829,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index 35767cd3f..ec92f2ad2 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -205,6 +205,25 @@ func CallGetAllWorkSpacesUserBelongsTo(httpClient *resty.Client) (GetWorkSpacesR return workSpacesResponse, nil } +func CallGetProjectById(httpClient *resty.Client, id string) (Project, error) { + var projectResponse GetProjectByIdResponse + response, err := httpClient. + R(). + SetResult(&projectResponse). + SetHeader("User-Agent", USER_AGENT). + Get(fmt.Sprintf("%v/v1/workspace/%s", config.INFISICAL_URL, id)) + + if err != nil { + return Project{}, err + } + + if response.IsError() { + return Project{}, fmt.Errorf("CallGetProjectById: Unsuccessful response: [response=%v]", response) + } + + return projectResponse.Project, nil +} + func CallIsAuthenticated(httpClient *resty.Client) bool { var workSpacesResponse GetWorkSpacesResponse response, err := httpClient. @@ -525,3 +544,79 @@ func CallUpdateRawSecretsV3(httpClient *resty.Client, request UpdateRawSecretByN return nil } + +func CallRegisterGatewayIdentityV1(httpClient *resty.Client) (*GetRelayCredentialsResponseV1, error) { + var resBody GetRelayCredentialsResponseV1 + response, err := httpClient. + R(). + SetResult(&resBody). + SetHeader("User-Agent", USER_AGENT). + Post(fmt.Sprintf("%v/v1/gateways/register-identity", config.INFISICAL_URL)) + + if err != nil { + return nil, fmt.Errorf("CallRegisterGatewayIdentityV1: Unable to complete api request [err=%w]", err) + } + + if response.IsError() { + return nil, fmt.Errorf("CallRegisterGatewayIdentityV1: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String()) + } + + return &resBody, nil +} + +func CallExchangeRelayCertV1(httpClient *resty.Client, request ExchangeRelayCertRequestV1) (*ExchangeRelayCertResponseV1, error) { + var resBody ExchangeRelayCertResponseV1 + response, err := httpClient. + R(). + SetResult(&resBody). + SetBody(request). + SetHeader("User-Agent", USER_AGENT). + Post(fmt.Sprintf("%v/v1/gateways/exchange-cert", config.INFISICAL_URL)) + + if err != nil { + return nil, fmt.Errorf("CallExchangeRelayCertV1: Unable to complete api request [err=%w]", err) + } + + if response.IsError() { + return nil, fmt.Errorf("CallExchangeRelayCertV1: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String()) + } + + return &resBody, nil +} + +func CallGatewayHeartBeatV1(httpClient *resty.Client) error { + response, err := httpClient. + R(). + SetHeader("User-Agent", USER_AGENT). + Post(fmt.Sprintf("%v/v1/gateways/heartbeat", config.INFISICAL_URL)) + + if err != nil { + return fmt.Errorf("CallGatewayHeartBeatV1: Unable to complete api request [err=%w]", err) + } + + if response.IsError() { + return fmt.Errorf("CallGatewayHeartBeatV1: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String()) + } + + return nil +} + +func CallBootstrapInstance(httpClient *resty.Client, request BootstrapInstanceRequest) (map[string]interface{}, error) { + var resBody map[string]interface{} + response, err := httpClient. + R(). + SetResult(&resBody). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Post(fmt.Sprintf("%v/v1/admin/bootstrap", request.Domain)) + + if err != nil { + return nil, fmt.Errorf("CallBootstrapInstance: Unable to complete api request [err=%w]", err) + } + + if response.IsError() { + return nil, fmt.Errorf("CallBootstrapInstance: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String()) + } + + return resBody, nil +} diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index f96c93709..a7a797a0b 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -128,6 +128,10 @@ type GetWorkSpacesResponse struct { } `json:"workspaces"` } +type GetProjectByIdResponse struct { + Project Project `json:"workspace"` +} + type GetOrganizationsResponse struct { Organizations []struct { ID string `json:"id"` @@ -136,8 +140,9 @@ type GetOrganizationsResponse struct { } type SelectOrganizationResponse struct { - Token string `json:"token"` - MfaEnabled bool `json:"isMfaEnabled"` + Token string `json:"token"` + MfaEnabled bool `json:"isMfaEnabled"` + MfaMethod string `json:"mfaMethod"` } type SelectOrganizationRequest struct { @@ -162,6 +167,12 @@ type Secret struct { PlainTextKey string `json:"plainTextKey"` } +type Project struct { + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` +} + type RawSecret struct { SecretKey string `json:"secretKey,omitempty"` SecretValue string `json:"secretValue,omitempty"` @@ -260,8 +271,9 @@ type GetLoginTwoV2Response struct { } type VerifyMfaTokenRequest struct { - Email string `json:"email"` - MFAToken string `json:"mfaToken"` + Email string `json:"email"` + MFAToken string `json:"mfaToken"` + MFAMethod string `json:"mfaMethod"` } type VerifyMfaTokenResponse struct { @@ -617,3 +629,29 @@ type GetRawSecretV3ByNameResponse struct { } `json:"secret"` ETag string } + +type GetRelayCredentialsResponseV1 struct { + TurnServerUsername string `json:"turnServerUsername"` + TurnServerPassword string `json:"turnServerPassword"` + TurnServerRealm string `json:"turnServerRealm"` + TurnServerAddress string `json:"turnServerAddress"` + InfisicalStaticIp string `json:"infisicalStaticIp"` +} + +type ExchangeRelayCertRequestV1 struct { + RelayAddress string `json:"relayAddress"` +} + +type ExchangeRelayCertResponseV1 struct { + SerialNumber string `json:"serialNumber"` + PrivateKey string `json:"privateKey"` + Certificate string `json:"certificate"` + CertificateChain string `json:"certificateChain"` +} + +type BootstrapInstanceRequest struct { + Email string `json:"email"` + Password string `json:"password"` + Organization string `json:"organization"` + Domain string `json:"domain"` +} diff --git a/cli/packages/cmd/agent.go b/cli/packages/cmd/agent.go index f4fe94a6e..b8fc6ed7b 100644 --- a/cli/packages/cmd/agent.go +++ b/cli/packages/cmd/agent.go @@ -29,7 +29,6 @@ import ( "github.com/Infisical/infisical-merge/packages/config" "github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/util" - "github.com/go-resty/resty/v2" "github.com/spf13/cobra" ) @@ -514,7 +513,10 @@ type NewAgentMangerOptions struct { } func NewAgentManager(options NewAgentMangerOptions) *AgentManager { - + customHeaders, err := util.GetInfisicalCustomHeadersMap() + if err != nil { + util.HandleError(err, "Unable to get custom headers") + } return &AgentManager{ filePaths: options.FileDeposits, templates: options.Templates, @@ -529,6 +531,7 @@ func NewAgentManager(options NewAgentMangerOptions) *AgentManager { SiteUrl: config.INFISICAL_URL, UserAgent: api.USER_AGENT, // ? Should we perhaps use a different user agent for the Agent for better analytics? AutoTokenRefresh: false, + CustomHeaders: customHeaders, }), } @@ -716,7 +719,11 @@ func (tm *AgentManager) FetchNewAccessToken() error { // Refreshes the existing access token func (tm *AgentManager) RefreshAccessToken() error { - httpClient := resty.New() + httpClient, err := util.GetRestyClientWithCustomHeaders() + if err != nil { + return err + } + httpClient.SetRetryCount(10000). SetRetryMaxWaitTime(20 * time.Second). SetRetryWaitTime(5 * time.Second) diff --git a/cli/packages/cmd/bootstrap.go b/cli/packages/cmd/bootstrap.go new file mode 100644 index 000000000..4582cb001 --- /dev/null +++ b/cli/packages/cmd/bootstrap.go @@ -0,0 +1,107 @@ +/* +Copyright (c) 2023 Infisical Inc. +*/ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/Infisical/infisical-merge/packages/api" + "github.com/Infisical/infisical-merge/packages/util" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +var bootstrapCmd = &cobra.Command{ + Use: "bootstrap", + Short: "Used to bootstrap your Infisical instance", + DisableFlagsInUseLine: true, + Example: "infisical bootstrap", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + email, _ := cmd.Flags().GetString("email") + if email == "" { + if envEmail, ok := os.LookupEnv("INFISICAL_ADMIN_EMAIL"); ok { + email = envEmail + } + } + + if email == "" { + log.Error().Msg("email is required") + return + } + + password, _ := cmd.Flags().GetString("password") + if password == "" { + if envPassword, ok := os.LookupEnv("INFISICAL_ADMIN_PASSWORD"); ok { + password = envPassword + } + } + + if password == "" { + log.Error().Msg("password is required") + return + } + + organization, _ := cmd.Flags().GetString("organization") + if organization == "" { + if envOrganization, ok := os.LookupEnv("INFISICAL_ADMIN_ORGANIZATION"); ok { + organization = envOrganization + } + } + + if organization == "" { + log.Error().Msg("organization is required") + return + } + + domain, _ := cmd.Flags().GetString("domain") + if domain == "" { + if envDomain, ok := os.LookupEnv("INFISICAL_API_URL"); ok { + domain = envDomain + } + } + + if domain == "" { + log.Error().Msg("domain is required") + return + } + + httpClient, err := util.GetRestyClientWithCustomHeaders() + if err != nil { + log.Error().Msgf("Failed to get resty client with custom headers: %v", err) + return + } + httpClient.SetHeader("Accept", "application/json") + + bootstrapResponse, err := api.CallBootstrapInstance(httpClient, api.BootstrapInstanceRequest{ + Domain: util.AppendAPIEndpoint(domain), + Email: email, + Password: password, + Organization: organization, + }) + + if err != nil { + log.Error().Msgf("Failed to bootstrap instance: %v", err) + return + } + + responseJSON, err := json.MarshalIndent(bootstrapResponse, "", " ") + if err != nil { + log.Fatal().Msgf("Failed to convert response to JSON: %v", err) + return + } + fmt.Println(string(responseJSON)) + }, +} + +func init() { + bootstrapCmd.Flags().String("domain", "", "The domain of your self-hosted Infisical instance") + bootstrapCmd.Flags().String("email", "", "The desired email address of the instance admin") + bootstrapCmd.Flags().String("password", "", "The desired password of the instance admin") + bootstrapCmd.Flags().String("organization", "", "The name of the organization to create for the instance") + + rootCmd.AddCommand(bootstrapCmd) +} diff --git a/cli/packages/cmd/dynamic_secrets.go b/cli/packages/cmd/dynamic_secrets.go new file mode 100644 index 000000000..60f356185 --- /dev/null +++ b/cli/packages/cmd/dynamic_secrets.go @@ -0,0 +1,615 @@ +/* +Copyright (c) 2023 Infisical Inc. +*/ +package cmd + +import ( + "context" + "fmt" + + "github.com/Infisical/infisical-merge/packages/api" + "github.com/Infisical/infisical-merge/packages/config" + "github.com/Infisical/infisical-merge/packages/visualize" + + // "github.com/Infisical/infisical-merge/packages/models" + "github.com/Infisical/infisical-merge/packages/util" + // "github.com/Infisical/infisical-merge/packages/visualize" + "github.com/posthog/posthog-go" + "github.com/spf13/cobra" + + infisicalSdk "github.com/infisical/go-sdk" + infisicalSdkModels "github.com/infisical/go-sdk/packages/models" +) + +var dynamicSecretCmd = &cobra.Command{ + Example: `infisical dynamic-secrets`, + Short: "Used to list dynamic secrets", + Use: "dynamic-secrets", + DisableFlagsInUseLine: true, + Args: cobra.NoArgs, + Run: getDynamicSecretList, +} + +func getDynamicSecretList(cmd *cobra.Command, args []string) { + environmentName, _ := cmd.Flags().GetString("env") + if !cmd.Flags().Changed("env") { + environmentFromWorkspace := util.GetEnvFromWorkspaceFile() + if environmentFromWorkspace != "" { + environmentName = environmentFromWorkspace + } + } + + token, err := util.GetInfisicalToken(cmd) + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + projectId, err := cmd.Flags().GetString("projectId") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + secretsPath, err := cmd.Flags().GetString("path") + if err != nil { + util.HandleError(err, "Unable to parse path flag") + } + + var infisicalToken string + httpClient, err := util.GetRestyClientWithCustomHeaders() + if err != nil { + util.HandleError(err, "Unable to get resty client with custom headers") + } + + if projectId == "" { + workspaceFile, err := util.GetWorkSpaceFromFile() + if err != nil { + util.HandleError(err, "Unable to get local project details") + } + projectId = workspaceFile.WorkspaceId + } + + if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { + infisicalToken = token.Token + } else { + util.RequireLogin() + util.RequireLocalWorkspaceFile() + + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) + if err != nil { + util.HandleError(err, "Unable to authenticate") + } + + if loggedInUserDetails.LoginExpired { + util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + } + infisicalToken = loggedInUserDetails.UserCredentials.JTWToken + } + + httpClient.SetAuthToken(infisicalToken) + + customHeaders, err := util.GetInfisicalCustomHeadersMap() + if err != nil { + util.HandleError(err, "Unable to get custom headers") + } + + infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ + SiteUrl: config.INFISICAL_URL, + UserAgent: api.USER_AGENT, + AutoTokenRefresh: false, + CustomHeaders: customHeaders, + }) + infisicalClient.Auth().SetAccessToken(infisicalToken) + + projectDetails, err := api.CallGetProjectById(httpClient, projectId) + if err != nil { + util.HandleError(err, "To fetch project details") + } + + dynamicSecretRootCredentials, err := infisicalClient.DynamicSecrets().List(infisicalSdk.ListDynamicSecretsRootCredentialsOptions{ + ProjectSlug: projectDetails.Slug, + SecretPath: secretsPath, + EnvironmentSlug: environmentName, + }) + + if err != nil { + util.HandleError(err, "To fetch dynamic secret root credentials details") + } + + visualize.PrintAllDynamicRootCredentials(dynamicSecretRootCredentials) + Telemetry.CaptureEvent("cli-command:dynamic-secrets", posthog.NewProperties().Set("count", len(dynamicSecretRootCredentials)).Set("version", util.CLI_VERSION)) +} + +var dynamicSecretLeaseCmd = &cobra.Command{ + Example: `lease`, + Short: "Manage leases for dynamic secrets", + Use: "lease", + DisableFlagsInUseLine: true, +} + +var dynamicSecretLeaseCreateCmd = &cobra.Command{ + Example: `lease create "`, + Short: "Used to lease dynamic secret by name", + Use: "create [dynamic-secret]", + DisableFlagsInUseLine: true, + Args: cobra.ExactArgs(1), + Run: createDynamicSecretLeaseByName, +} + +func createDynamicSecretLeaseByName(cmd *cobra.Command, args []string) { + dynamicSecretRootCredentialName := args[0] + + environmentName, _ := cmd.Flags().GetString("env") + if !cmd.Flags().Changed("env") { + environmentFromWorkspace := util.GetEnvFromWorkspaceFile() + if environmentFromWorkspace != "" { + environmentName = environmentFromWorkspace + } + } + + token, err := util.GetInfisicalToken(cmd) + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + projectId, err := cmd.Flags().GetString("projectId") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + ttl, err := cmd.Flags().GetString("ttl") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + secretsPath, err := cmd.Flags().GetString("path") + if err != nil { + util.HandleError(err, "Unable to parse path flag") + } + + plainOutput, err := cmd.Flags().GetBool("plain") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + var infisicalToken string + httpClient, err := util.GetRestyClientWithCustomHeaders() + if err != nil { + util.HandleError(err, "Unable to get resty client with custom headers") + } + + if projectId == "" { + workspaceFile, err := util.GetWorkSpaceFromFile() + if err != nil { + util.HandleError(err, "Unable to get local project details") + } + projectId = workspaceFile.WorkspaceId + } + + if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { + infisicalToken = token.Token + } else { + util.RequireLogin() + util.RequireLocalWorkspaceFile() + + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) + if err != nil { + util.HandleError(err, "Unable to authenticate") + } + + if loggedInUserDetails.LoginExpired { + util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + } + infisicalToken = loggedInUserDetails.UserCredentials.JTWToken + } + + httpClient.SetAuthToken(infisicalToken) + + customHeaders, err := util.GetInfisicalCustomHeadersMap() + if err != nil { + util.HandleError(err, "Unable to get custom headers") + } + + infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ + SiteUrl: config.INFISICAL_URL, + UserAgent: api.USER_AGENT, + AutoTokenRefresh: false, + CustomHeaders: customHeaders, + }) + infisicalClient.Auth().SetAccessToken(infisicalToken) + + projectDetails, err := api.CallGetProjectById(httpClient, projectId) + if err != nil { + util.HandleError(err, "To fetch project details") + } + + dynamicSecretRootCredential, err := infisicalClient.DynamicSecrets().GetByName(infisicalSdk.GetDynamicSecretRootCredentialByNameOptions{ + DynamicSecretName: dynamicSecretRootCredentialName, + ProjectSlug: projectDetails.Slug, + SecretPath: secretsPath, + EnvironmentSlug: environmentName, + }) + + if err != nil { + util.HandleError(err, "To fetch dynamic secret root credentials details") + } + + leaseCredentials, _, leaseDetails, err := infisicalClient.DynamicSecrets().Leases().Create(infisicalSdk.CreateDynamicSecretLeaseOptions{ + DynamicSecretName: dynamicSecretRootCredential.Name, + ProjectSlug: projectDetails.Slug, + TTL: ttl, + SecretPath: secretsPath, + EnvironmentSlug: environmentName, + }) + if err != nil { + util.HandleError(err, "To lease dynamic secret") + } + + if plainOutput { + for key, value := range leaseCredentials { + if cred, ok := value.(string); ok { + fmt.Printf("%s=%s\n", key, cred) + } + } + } else { + fmt.Println("Dynamic Secret Leasing") + fmt.Printf("Name: %s\n", dynamicSecretRootCredential.Name) + fmt.Printf("Provider: %s\n", dynamicSecretRootCredential.Type) + fmt.Printf("Lease ID: %s\n", leaseDetails.Id) + fmt.Printf("Expire At: %s\n", leaseDetails.ExpireAt.Local().Format("02-Jan-2006 03:04:05 PM")) + visualize.PrintAllDyamicSecretLeaseCredentials(leaseCredentials) + } + + Telemetry.CaptureEvent("cli-command:dynamic-secrets lease", posthog.NewProperties().Set("type", dynamicSecretRootCredential.Type).Set("version", util.CLI_VERSION)) +} + +var dynamicSecretLeaseRenewCmd = &cobra.Command{ + Example: `lease renew "`, + Short: "Used to renew dynamic secret lease by name", + Use: "renew [lease-id]", + DisableFlagsInUseLine: true, + Args: cobra.ExactArgs(1), + Run: renewDynamicSecretLeaseByName, +} + +func renewDynamicSecretLeaseByName(cmd *cobra.Command, args []string) { + dynamicSecretLeaseId := args[0] + + environmentName, _ := cmd.Flags().GetString("env") + if !cmd.Flags().Changed("env") { + environmentFromWorkspace := util.GetEnvFromWorkspaceFile() + if environmentFromWorkspace != "" { + environmentName = environmentFromWorkspace + } + } + + token, err := util.GetInfisicalToken(cmd) + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + projectId, err := cmd.Flags().GetString("projectId") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + ttl, err := cmd.Flags().GetString("ttl") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + secretsPath, err := cmd.Flags().GetString("path") + if err != nil { + util.HandleError(err, "Unable to parse path flag") + } + + var infisicalToken string + httpClient, err := util.GetRestyClientWithCustomHeaders() + if err != nil { + util.HandleError(err, "Unable to get resty client with custom headers") + } + + if projectId == "" { + workspaceFile, err := util.GetWorkSpaceFromFile() + if err != nil { + util.HandleError(err, "Unable to get local project details") + } + projectId = workspaceFile.WorkspaceId + } + + if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { + infisicalToken = token.Token + } else { + util.RequireLogin() + util.RequireLocalWorkspaceFile() + + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) + if err != nil { + util.HandleError(err, "Unable to authenticate") + } + + if loggedInUserDetails.LoginExpired { + util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + } + infisicalToken = loggedInUserDetails.UserCredentials.JTWToken + } + + httpClient.SetAuthToken(infisicalToken) + + customHeaders, err := util.GetInfisicalCustomHeadersMap() + if err != nil { + util.HandleError(err, "Unable to get custom headers") + } + + infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ + SiteUrl: config.INFISICAL_URL, + UserAgent: api.USER_AGENT, + AutoTokenRefresh: false, + CustomHeaders: customHeaders, + }) + infisicalClient.Auth().SetAccessToken(infisicalToken) + + projectDetails, err := api.CallGetProjectById(httpClient, projectId) + if err != nil { + util.HandleError(err, "To fetch project details") + } + + if err != nil { + util.HandleError(err, "To fetch dynamic secret root credentials details") + } + + leaseDetails, err := infisicalClient.DynamicSecrets().Leases().RenewById(infisicalSdk.RenewDynamicSecretLeaseOptions{ + ProjectSlug: projectDetails.Slug, + TTL: ttl, + SecretPath: secretsPath, + EnvironmentSlug: environmentName, + LeaseId: dynamicSecretLeaseId, + }) + if err != nil { + util.HandleError(err, "To renew dynamic secret lease") + } + + fmt.Println("Successfully renewed dynamic secret lease") + visualize.PrintAllDynamicSecretLeases([]infisicalSdkModels.DynamicSecretLease{leaseDetails}) + + Telemetry.CaptureEvent("cli-command:dynamic-secrets lease renew", posthog.NewProperties().Set("version", util.CLI_VERSION)) +} + +var dynamicSecretLeaseRevokeCmd = &cobra.Command{ + Example: `lease delete "`, + Short: "Used to delete dynamic secret lease by name", + Use: "delete [lease-id]", + DisableFlagsInUseLine: true, + Args: cobra.ExactArgs(1), + Run: revokeDynamicSecretLeaseByName, +} + +func revokeDynamicSecretLeaseByName(cmd *cobra.Command, args []string) { + dynamicSecretLeaseId := args[0] + + environmentName, _ := cmd.Flags().GetString("env") + if !cmd.Flags().Changed("env") { + environmentFromWorkspace := util.GetEnvFromWorkspaceFile() + if environmentFromWorkspace != "" { + environmentName = environmentFromWorkspace + } + } + + token, err := util.GetInfisicalToken(cmd) + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + projectId, err := cmd.Flags().GetString("projectId") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + secretsPath, err := cmd.Flags().GetString("path") + if err != nil { + util.HandleError(err, "Unable to parse path flag") + } + + var infisicalToken string + httpClient, err := util.GetRestyClientWithCustomHeaders() + if err != nil { + util.HandleError(err, "Unable to get resty client with custom headers") + } + + if projectId == "" { + workspaceFile, err := util.GetWorkSpaceFromFile() + if err != nil { + util.HandleError(err, "Unable to get local project details") + } + projectId = workspaceFile.WorkspaceId + } + + if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { + infisicalToken = token.Token + } else { + util.RequireLogin() + util.RequireLocalWorkspaceFile() + + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) + if err != nil { + util.HandleError(err, "Unable to authenticate") + } + + if loggedInUserDetails.LoginExpired { + util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + } + infisicalToken = loggedInUserDetails.UserCredentials.JTWToken + } + + httpClient.SetAuthToken(infisicalToken) + + customHeaders, err := util.GetInfisicalCustomHeadersMap() + if err != nil { + util.HandleError(err, "Unable to get custom headers") + } + + infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ + SiteUrl: config.INFISICAL_URL, + UserAgent: api.USER_AGENT, + AutoTokenRefresh: false, + CustomHeaders: customHeaders, + }) + infisicalClient.Auth().SetAccessToken(infisicalToken) + + projectDetails, err := api.CallGetProjectById(httpClient, projectId) + if err != nil { + util.HandleError(err, "To fetch project details") + } + + if err != nil { + util.HandleError(err, "To fetch dynamic secret root credentials details") + } + + leaseDetails, err := infisicalClient.DynamicSecrets().Leases().DeleteById(infisicalSdk.DeleteDynamicSecretLeaseOptions{ + ProjectSlug: projectDetails.Slug, + SecretPath: secretsPath, + EnvironmentSlug: environmentName, + LeaseId: dynamicSecretLeaseId, + }) + if err != nil { + util.HandleError(err, "To revoke dynamic secret lease") + } + + fmt.Println("Successfully revoked dynamic secret lease") + visualize.PrintAllDynamicSecretLeases([]infisicalSdkModels.DynamicSecretLease{leaseDetails}) + + Telemetry.CaptureEvent("cli-command:dynamic-secrets lease revoke", posthog.NewProperties().Set("version", util.CLI_VERSION)) +} + +var dynamicSecretLeaseListCmd = &cobra.Command{ + Example: `lease list "`, + Short: "Used to list leases of a dynamic secret by name", + Use: "list [dynamic-secret]", + DisableFlagsInUseLine: true, + Args: cobra.ExactArgs(1), + Run: listDynamicSecretLeaseByName, +} + +func listDynamicSecretLeaseByName(cmd *cobra.Command, args []string) { + dynamicSecretRootCredentialName := args[0] + + environmentName, _ := cmd.Flags().GetString("env") + if !cmd.Flags().Changed("env") { + environmentFromWorkspace := util.GetEnvFromWorkspaceFile() + if environmentFromWorkspace != "" { + environmentName = environmentFromWorkspace + } + } + + token, err := util.GetInfisicalToken(cmd) + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + projectId, err := cmd.Flags().GetString("projectId") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + secretsPath, err := cmd.Flags().GetString("path") + if err != nil { + util.HandleError(err, "Unable to parse path flag") + } + + var infisicalToken string + httpClient, err := util.GetRestyClientWithCustomHeaders() + if err != nil { + util.HandleError(err, "Unable to get resty client with custom headers") + } + + if projectId == "" { + workspaceFile, err := util.GetWorkSpaceFromFile() + if err != nil { + util.HandleError(err, "Unable to get local project details") + } + projectId = workspaceFile.WorkspaceId + } + + if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { + infisicalToken = token.Token + } else { + util.RequireLogin() + util.RequireLocalWorkspaceFile() + + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) + if err != nil { + util.HandleError(err, "Unable to authenticate") + } + + if loggedInUserDetails.LoginExpired { + util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + } + infisicalToken = loggedInUserDetails.UserCredentials.JTWToken + } + + httpClient.SetAuthToken(infisicalToken) + + customHeaders, err := util.GetInfisicalCustomHeadersMap() + if err != nil { + util.HandleError(err, "Unable to get custom headers") + } + + infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ + SiteUrl: config.INFISICAL_URL, + UserAgent: api.USER_AGENT, + AutoTokenRefresh: false, + CustomHeaders: customHeaders, + }) + infisicalClient.Auth().SetAccessToken(infisicalToken) + + projectDetails, err := api.CallGetProjectById(httpClient, projectId) + if err != nil { + util.HandleError(err, "To fetch project details") + } + + dynamicSecretLeases, err := infisicalClient.DynamicSecrets().Leases().List(infisicalSdk.ListDynamicSecretLeasesOptions{ + DynamicSecretName: dynamicSecretRootCredentialName, + ProjectSlug: projectDetails.Slug, + SecretPath: secretsPath, + EnvironmentSlug: environmentName, + }) + + if err != nil { + util.HandleError(err, "To fetch dynamic secret leases list") + } + + visualize.PrintAllDynamicSecretLeases(dynamicSecretLeases) + Telemetry.CaptureEvent("cli-command:dynamic-secrets lease list", posthog.NewProperties().Set("lease-count", len(dynamicSecretLeases)).Set("version", util.CLI_VERSION)) +} + +func init() { + dynamicSecretLeaseCreateCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from") + dynamicSecretLeaseCreateCmd.Flags().String("token", "", "Create dynamic secret leases using machine identity access token") + dynamicSecretLeaseCreateCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth") + dynamicSecretLeaseCreateCmd.Flags().String("ttl", "", "The lease lifetime TTL. If not provided the default TTL of dynamic secret will be used.") + dynamicSecretLeaseCreateCmd.Flags().Bool("plain", false, "Print leased credentials without formatting, one per line") + dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseCreateCmd) + + dynamicSecretLeaseListCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from") + dynamicSecretLeaseListCmd.Flags().String("token", "", "Fetch dynamic secret leases machine identity access token") + dynamicSecretLeaseListCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth") + dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseListCmd) + + dynamicSecretLeaseRenewCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from") + dynamicSecretLeaseRenewCmd.Flags().String("token", "", "Renew dynamic secrets machine identity access token") + dynamicSecretLeaseRenewCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth") + dynamicSecretLeaseRenewCmd.Flags().String("ttl", "", "The lease lifetime TTL. If not provided the default TTL of dynamic secret will be used.") + dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseRenewCmd) + + dynamicSecretLeaseRevokeCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from") + dynamicSecretLeaseRevokeCmd.Flags().String("token", "", "Delete dynamic secrets using machine identity access token") + dynamicSecretLeaseRevokeCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth") + dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseRevokeCmd) + + dynamicSecretCmd.AddCommand(dynamicSecretLeaseCmd) + + dynamicSecretCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") + dynamicSecretCmd.Flags().String("projectId", "", "Manually set the projectId to fetch dynamic-secret when using machine identity based auth") + dynamicSecretCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on") + dynamicSecretCmd.Flags().String("path", "/", "get dynamic secret within a folder path") + rootCmd.AddCommand(dynamicSecretCmd) +} diff --git a/cli/packages/cmd/export.go b/cli/packages/cmd/export.go index 6f02408fd..b872b0e61 100644 --- a/cli/packages/cmd/export.go +++ b/cli/packages/cmd/export.go @@ -111,7 +111,7 @@ var exportCmd = &cobra.Command{ accessToken = token.Token } else { log.Debug().Msg("GetAllEnvironmentVariables: Trying to fetch secrets using logged in details") - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) if err != nil { util.HandleError(err) } diff --git a/cli/packages/cmd/gateway.go b/cli/packages/cmd/gateway.go new file mode 100644 index 000000000..51565b6fd --- /dev/null +++ b/cli/packages/cmd/gateway.go @@ -0,0 +1,211 @@ +package cmd + +import ( + "context" + "fmt" + "os" + "os/exec" + "os/signal" + "runtime" + "syscall" + "time" + + "github.com/Infisical/infisical-merge/packages/gateway" + "github.com/Infisical/infisical-merge/packages/util" + "github.com/posthog/posthog-go" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +var gatewayCmd = &cobra.Command{ + Use: "gateway", + Short: "Run the Infisical gateway or manage its systemd service", + Long: "Run the Infisical gateway in the foreground or manage its systemd service installation. Use 'gateway install' to set up the systemd service.", + Example: `infisical gateway --token= + sudo infisical gateway install --token= --domain=`, + DisableFlagsInUseLine: true, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + token, err := util.GetInfisicalToken(cmd) + if err != nil { + util.HandleError(err, "Unable to parse token flag") + } + + if token == nil { + util.HandleError(fmt.Errorf("Token not found")) + } + + Telemetry.CaptureEvent("cli-command:gateway", posthog.NewProperties().Set("version", util.CLI_VERSION)) + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + sigStopCh := make(chan bool, 1) + + ctx, cancel := context.WithCancel(cmd.Context()) + defer cancel() + + go func() { + <-sigCh + close(sigStopCh) + cancel() + + // If we get a second signal, force exit + <-sigCh + log.Warn().Msgf("Force exit triggered") + os.Exit(1) + }() + + // Main gateway retry loop with proper context handling + retryTicker := time.NewTicker(5 * time.Second) + defer retryTicker.Stop() + + for { + if ctx.Err() != nil { + log.Info().Msg("Shutting down gateway") + return + } + gatewayInstance, err := gateway.NewGateway(token.Token) + if err != nil { + util.HandleError(err) + } + + if err = gatewayInstance.ConnectWithRelay(); err != nil { + if ctx.Err() != nil { + log.Info().Msg("Shutting down gateway") + return + } + + log.Error().Msgf("Gateway connection error with relay: %s", err) + log.Info().Msg("Retrying connection in 5 seconds...") + select { + case <-retryTicker.C: + continue + case <-ctx.Done(): + log.Info().Msg("Shutting down gateway") + return + } + } + + err = gatewayInstance.Listen(ctx) + if ctx.Err() != nil { + log.Info().Msg("Gateway shutdown complete") + return + } + log.Error().Msgf("Gateway listen error: %s", err) + log.Info().Msg("Retrying connection in 5 seconds...") + select { + case <-retryTicker.C: + continue + case <-ctx.Done(): + log.Info().Msg("Shutting down gateway") + return + } + } + }, +} + +var gatewayInstallCmd = &cobra.Command{ + Use: "install", + Short: "Install and enable systemd service for the gateway (requires sudo)", + Long: "Install and enable systemd service for the gateway. Must be run with sudo on Linux.", + Example: "sudo infisical gateway install --token= --domain=", + DisableFlagsInUseLine: true, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + if runtime.GOOS != "linux" { + util.HandleError(fmt.Errorf("systemd service installation is only supported on Linux")) + } + + if os.Geteuid() != 0 { + util.HandleError(fmt.Errorf("systemd service installation requires root/sudo privileges")) + } + + token, err := util.GetInfisicalToken(cmd) + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + if token == nil { + util.HandleError(fmt.Errorf("Token not found")) + } + + domain, err := cmd.Flags().GetString("domain") + if err != nil { + util.HandleError(err, "Unable to parse domain flag") + } + + if err := gateway.InstallGatewaySystemdService(token.Token, domain); err != nil { + util.HandleError(err, "Failed to install systemd service") + } + + enableCmd := exec.Command("systemctl", "enable", "infisical-gateway") + if err := enableCmd.Run(); err != nil { + util.HandleError(err, "Failed to enable systemd service") + } + + log.Info().Msg("Successfully installed and enabled infisical-gateway service") + log.Info().Msg("To start the service, run: sudo systemctl start infisical-gateway") + }, +} + +var gatewayUninstallCmd = &cobra.Command{ + Use: "uninstall", + Short: "Uninstall and remove systemd service for the gateway (requires sudo)", + Long: "Uninstall and remove systemd service for the gateway. Must be run with sudo on Linux.", + Example: "sudo infisical gateway uninstall", + DisableFlagsInUseLine: true, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + if runtime.GOOS != "linux" { + util.HandleError(fmt.Errorf("systemd service installation is only supported on Linux")) + } + + if os.Geteuid() != 0 { + util.HandleError(fmt.Errorf("systemd service installation requires root/sudo privileges")) + } + + if err := gateway.UninstallGatewaySystemdService(); err != nil { + util.HandleError(err, "Failed to uninstall systemd service") + } + }, +} + +var gatewayRelayCmd = &cobra.Command{ + Example: `infisical gateway relay`, + Short: "Used to run infisical gateway relay", + Use: "relay", + DisableFlagsInUseLine: true, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + relayConfigFilePath, err := cmd.Flags().GetString("config") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + if relayConfigFilePath == "" { + util.HandleError(fmt.Errorf("Missing config file")) + } + + gatewayRelay, err := gateway.NewGatewayRelay(relayConfigFilePath) + if err != nil { + util.HandleError(err, "Failed to initialize gateway") + } + err = gatewayRelay.Run() + if err != nil { + util.HandleError(err, "Failed to start gateway") + } + }, +} + +func init() { + gatewayCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token") + gatewayInstallCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token") + gatewayInstallCmd.Flags().String("domain", "", "Domain of your self-hosted Infisical instance") + + gatewayRelayCmd.Flags().String("config", "", "Relay config yaml file path") + + gatewayCmd.AddCommand(gatewayInstallCmd) + gatewayCmd.AddCommand(gatewayUninstallCmd) + gatewayCmd.AddCommand(gatewayRelayCmd) + rootCmd.AddCommand(gatewayCmd) +} diff --git a/cli/packages/cmd/init.go b/cli/packages/cmd/init.go index f95d90485..e10a11c06 100644 --- a/cli/packages/cmd/init.go +++ b/cli/packages/cmd/init.go @@ -10,7 +10,6 @@ import ( "github.com/Infisical/infisical-merge/packages/api" "github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/util" - "github.com/go-resty/resty/v2" "github.com/manifoldco/promptui" "github.com/posthog/posthog-go" "github.com/rs/zerolog/log" @@ -41,7 +40,7 @@ var initCmd = &cobra.Command{ } } - userCreds, err := util.GetCurrentLoggedInUserDetails() + userCreds, err := util.GetCurrentLoggedInUserDetails(true) if err != nil { util.HandleError(err, "Unable to get your login details") } @@ -50,7 +49,10 @@ var initCmd = &cobra.Command{ util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") } - httpClient := resty.New() + httpClient, err := util.GetRestyClientWithCustomHeaders() + if err != nil { + util.HandleError(err, "Unable to get resty client with custom headers") + } httpClient.SetAuthToken(userCreds.UserCredentials.JTWToken) organizationResponse, err := api.CallGetAllOrganizations(httpClient) @@ -79,13 +81,17 @@ var initCmd = &cobra.Command{ if tokenResponse.MfaEnabled { i := 1 for i < 6 { - mfaVerifyCode := askForMFACode() - - httpClient := resty.New() + mfaVerifyCode := askForMFACode(tokenResponse.MfaMethod) + + httpClient, err := util.GetRestyClientWithCustomHeaders() + if err != nil { + util.HandleError(err, "Unable to get resty client with custom headers") + } httpClient.SetAuthToken(tokenResponse.Token) verifyMFAresponse, mfaErrorResponse, requestError := api.CallVerifyMfaToken(httpClient, api.VerifyMfaTokenRequest{ - Email: userCreds.UserCredentials.Email, - MFAToken: mfaVerifyCode, + Email: userCreds.UserCredentials.Email, + MFAToken: mfaVerifyCode, + MFAMethod: tokenResponse.MfaMethod, }) if requestError != nil { util.HandleError(err) @@ -99,7 +105,7 @@ var initCmd = &cobra.Command{ break } } - + if mfaErrorResponse.Context.Code == "mfa_expired" { util.PrintErrorMessageAndExit("Your 2FA verification code has expired, please try logging in again") break diff --git a/cli/packages/cmd/kmip.go b/cli/packages/cmd/kmip.go new file mode 100644 index 000000000..b0c397895 --- /dev/null +++ b/cli/packages/cmd/kmip.go @@ -0,0 +1,103 @@ +/* +Copyright (c) 2023 Infisical Inc. +*/ +package cmd + +import ( + "fmt" + + "github.com/Infisical/infisical-merge/packages/config" + "github.com/Infisical/infisical-merge/packages/util" + kmip "github.com/infisical/infisical-kmip" + "github.com/spf13/cobra" +) + +var kmipCmd = &cobra.Command{ + Example: `infisical kmip`, + Short: "Used to manage KMIP servers", + Use: "kmip", + DisableFlagsInUseLine: true, + Args: cobra.NoArgs, +} + +var kmipStartCmd = &cobra.Command{ + Example: `infisical kmip start`, + Short: "Used to start a KMIP server", + Use: "start", + DisableFlagsInUseLine: true, + Args: cobra.NoArgs, + Run: startKmipServer, +} + +func startKmipServer(cmd *cobra.Command, args []string) { + listenAddr, err := cmd.Flags().GetString("listen-address") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + identityAuthMethod, err := cmd.Flags().GetString("identity-auth-method") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + authMethodValid, strategy := util.IsAuthMethodValid(identityAuthMethod, false) + if !authMethodValid { + util.PrintErrorMessageAndExit(fmt.Sprintf("Invalid login method: %s", identityAuthMethod)) + } + + var identityClientId string + var identityClientSecret string + + if strategy == util.AuthStrategy.UNIVERSAL_AUTH { + identityClientId, err = util.GetCmdFlagOrEnv(cmd, "identity-client-id", util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME) + + if err != nil { + util.HandleError(err, "Unable to parse identity client ID") + } + + identityClientSecret, err = util.GetCmdFlagOrEnv(cmd, "identity-client-secret", util.INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME) + if err != nil { + util.HandleError(err, "Unable to parse identity client secret") + } + } else { + util.PrintErrorMessageAndExit(fmt.Sprintf("Unsupported login method: %s", identityAuthMethod)) + } + + serverName, err := cmd.Flags().GetString("server-name") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + certificateTTL, err := cmd.Flags().GetString("certificate-ttl") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + hostnamesOrIps, err := cmd.Flags().GetString("hostnames-or-ips") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + kmip.StartServer(kmip.ServerConfig{ + Addr: listenAddr, + InfisicalBaseAPIURL: config.INFISICAL_URL, + IdentityClientId: identityClientId, + IdentityClientSecret: identityClientSecret, + ServerName: serverName, + CertificateTTL: certificateTTL, + HostnamesOrIps: hostnamesOrIps, + }) +} + +func init() { + kmipStartCmd.Flags().String("listen-address", "localhost:5696", "The address for the KMIP server to listen on. Defaults to localhost:5696") + kmipStartCmd.Flags().String("identity-auth-method", string(util.AuthStrategy.UNIVERSAL_AUTH), "The auth method to use for authenticating the machine identity. Defaults to universal-auth.") + kmipStartCmd.Flags().String("identity-client-id", "", "Universal auth client ID of machine identity") + kmipStartCmd.Flags().String("identity-client-secret", "", "Universal auth client secret of machine identity") + kmipStartCmd.Flags().String("server-name", "kmip-server", "The name of the KMIP server") + kmipStartCmd.Flags().String("certificate-ttl", "1y", "The TTL duration for the server certificate") + kmipStartCmd.Flags().String("hostnames-or-ips", "", "Comma-separated list of hostnames or IPs") + + kmipCmd.AddCommand(kmipStartCmd) + rootCmd.AddCommand(kmipCmd) +} diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index 03974ba19..b1c868d8c 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -27,7 +27,6 @@ import ( "github.com/Infisical/infisical-merge/packages/srp" "github.com/Infisical/infisical-merge/packages/util" "github.com/fatih/color" - "github.com/go-resty/resty/v2" "github.com/manifoldco/promptui" "github.com/posthog/posthog-go" "github.com/rs/cors" @@ -154,6 +153,8 @@ var loginCmd = &cobra.Command{ DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) { + presetDomain := config.INFISICAL_URL + clearSelfHostedDomains, err := cmd.Flags().GetBool("clear-domains") if err != nil { util.HandleError(err) @@ -176,10 +177,16 @@ var loginCmd = &cobra.Command{ return } + customHeaders, err := util.GetInfisicalCustomHeadersMap() + if err != nil { + util.HandleError(err, "Unable to get custom headers") + } + infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ SiteUrl: config.INFISICAL_URL, UserAgent: api.USER_AGENT, AutoTokenRefresh: false, + CustomHeaders: customHeaders, }) loginMethod, err := cmd.Flags().GetString("method") @@ -198,7 +205,7 @@ var loginCmd = &cobra.Command{ // standalone user auth if loginMethod == "user" { - currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() + currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) // if the key can't be found or there is an error getting current credentials from key ring, allow them to override if err != nil && (strings.Contains(err.Error(), "we couldn't find your logged in details")) { log.Debug().Err(err) @@ -216,11 +223,19 @@ var loginCmd = &cobra.Command{ return } } + + usePresetDomain, err := usePresetDomain(presetDomain) + + if err != nil { + util.HandleError(err) + } + //override domain domainQuery := true if config.INFISICAL_URL_MANUAL_OVERRIDE != "" && config.INFISICAL_URL_MANUAL_OVERRIDE != fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_EU_URL) && - config.INFISICAL_URL_MANUAL_OVERRIDE != fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_US_URL) { + config.INFISICAL_URL_MANUAL_OVERRIDE != fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_US_URL) && + !usePresetDomain { overrideDomain, err := DomainOverridePrompt() if err != nil { util.HandleError(err) @@ -228,7 +243,7 @@ var loginCmd = &cobra.Command{ //if not override set INFISICAL_URL to exported var //set domainQuery to false - if !overrideDomain { + if !overrideDomain && !usePresetDomain { domainQuery = false config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL_MANUAL_OVERRIDE) config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", strings.TrimSuffix(config.INFISICAL_URL, "/api")) @@ -237,7 +252,7 @@ var loginCmd = &cobra.Command{ } //prompt user to select domain between Infisical cloud and self-hosting - if domainQuery { + if domainQuery && !usePresetDomain { err = askForDomain() if err != nil { util.HandleError(err, "Unable to parse domain url") @@ -305,7 +320,11 @@ var loginCmd = &cobra.Command{ credential, err := authStrategies[strategy](cmd, infisicalClient) if err != nil { - util.HandleError(fmt.Errorf("unable to authenticate with %s [err=%v]", formatAuthMethod(loginMethod), err)) + euErrorMessage := "" + if strings.HasPrefix(config.INFISICAL_URL, util.INFISICAL_DEFAULT_US_URL) { + euErrorMessage = fmt.Sprintf("\nIf you are using the Infisical Cloud Europe Region, please switch to it by using the \"--domain %s\" flag.", util.INFISICAL_DEFAULT_EU_URL) + } + util.HandleError(fmt.Errorf("unable to authenticate with %s [err=%v].%s", formatAuthMethod(loginMethod), err, euErrorMessage)) } if plainOutput { @@ -343,9 +362,12 @@ func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials) { if loginTwoResponse.MfaEnabled { i := 1 for i < 6 { - mfaVerifyCode := askForMFACode() + mfaVerifyCode := askForMFACode("email") - httpClient := resty.New() + httpClient, err := util.GetRestyClientWithCustomHeaders() + if err != nil { + util.HandleError(err, "Unable to get resty client with custom headers") + } httpClient.SetAuthToken(loginTwoResponse.Token) verifyMFAresponse, mfaErrorResponse, requestError := api.CallVerifyMfaToken(httpClient, api.VerifyMfaTokenRequest{ Email: email, @@ -526,13 +548,52 @@ func DomainOverridePrompt() (bool, error) { return selectedOption == OVERRIDE, err } +func usePresetDomain(presetDomain string) (bool, error) { + infisicalConfig, err := util.GetConfigFile() + if err != nil { + return false, fmt.Errorf("askForDomain: unable to get config file because [err=%s]", err) + } + + preconfiguredUrl := strings.TrimSuffix(presetDomain, "/api") + + if preconfiguredUrl != "" && preconfiguredUrl != util.INFISICAL_DEFAULT_US_URL && preconfiguredUrl != util.INFISICAL_DEFAULT_EU_URL { + parsedDomain := strings.TrimSuffix(strings.Trim(preconfiguredUrl, "/"), "/api") + + _, err := url.ParseRequestURI(parsedDomain) + if err != nil { + return false, errors.New(fmt.Sprintf("Invalid domain URL: '%s'", parsedDomain)) + } + + config.INFISICAL_URL = fmt.Sprintf("%s/api", parsedDomain) + config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", parsedDomain) + + if !slices.Contains(infisicalConfig.Domains, parsedDomain) { + infisicalConfig.Domains = append(infisicalConfig.Domains, parsedDomain) + err = util.WriteConfigFile(&infisicalConfig) + + if err != nil { + return false, fmt.Errorf("askForDomain: unable to write domains to config file because [err=%s]", err) + } + } + + whilte := color.New(color.FgGreen) + boldWhite := whilte.Add(color.Bold) + time.Sleep(time.Second * 1) + boldWhite.Printf("[INFO] Using domain '%s' from domain flag or INFISICAL_API_URL environment variable\n", parsedDomain) + + return true, nil + } + + return false, nil +} + func askForDomain() error { // query user to choose between Infisical cloud or self-hosting const ( INFISICAL_CLOUD_US = "Infisical Cloud (US Region)" INFISICAL_CLOUD_EU = "Infisical Cloud (EU Region)" - SELF_HOSTING = "Self-Hosting" + SELF_HOSTING = "Self-Hosting or Dedicated Instance" ADD_NEW_DOMAIN = "Add a new domain" ) @@ -673,7 +734,10 @@ func askForLoginCredentials() (email string, password string, err error) { func getFreshUserCredentials(email string, password string) (*api.GetLoginOneV2Response, *api.GetLoginTwoV2Response, error) { log.Debug().Msg(fmt.Sprint("getFreshUserCredentials: ", "email", email, "password: ", password)) - httpClient := resty.New() + httpClient, err := util.GetRestyClientWithCustomHeaders() + if err != nil { + return nil, nil, err + } httpClient.SetRetryCount(5) params := srp.GetParams(4096) @@ -723,7 +787,10 @@ func getFreshUserCredentials(email string, password string) (*api.GetLoginOneV2R func GetJwtTokenWithOrganizationId(oldJwtToken string, email string) string { log.Debug().Msg(fmt.Sprint("GetJwtTokenWithOrganizationId: ", "oldJwtToken", oldJwtToken)) - httpClient := resty.New() + httpClient, err := util.GetRestyClientWithCustomHeaders() + if err != nil { + util.HandleError(err, "Unable to get resty client with custom headers") + } httpClient.SetAuthToken(oldJwtToken) organizationResponse, err := api.CallGetAllOrganizations(httpClient) @@ -756,13 +823,17 @@ func GetJwtTokenWithOrganizationId(oldJwtToken string, email string) string { if selectedOrgRes.MfaEnabled { i := 1 for i < 6 { - mfaVerifyCode := askForMFACode() + mfaVerifyCode := askForMFACode(selectedOrgRes.MfaMethod) - httpClient := resty.New() + httpClient, err := util.GetRestyClientWithCustomHeaders() + if err != nil { + util.HandleError(err, "Unable to get resty client with custom headers") + } httpClient.SetAuthToken(selectedOrgRes.Token) verifyMFAresponse, mfaErrorResponse, requestError := api.CallVerifyMfaToken(httpClient, api.VerifyMfaTokenRequest{ - Email: email, - MFAToken: mfaVerifyCode, + Email: email, + MFAToken: mfaVerifyCode, + MFAMethod: selectedOrgRes.MfaMethod, }) if requestError != nil { util.HandleError(err) @@ -817,9 +888,15 @@ func generateFromPassword(password string, salt []byte, p *params) (hash []byte, return hash, nil } -func askForMFACode() string { +func askForMFACode(mfaMethod string) string { + var label string + if mfaMethod == "totp" { + label = "Enter the verification code from your mobile authenticator app or use a recovery code" + } else { + label = "Enter the 2FA verification code sent to your email" + } mfaCodePromptUI := promptui.Prompt{ - Label: "Enter the 2FA verification code sent to your email", + Label: label, } mfaVerifyCode, err := mfaCodePromptUI.Run() @@ -853,7 +930,14 @@ func askToPasteJwtToken(success chan models.UserCredentials, failure chan error) } // verify JTW - httpClient := resty.New(). + httpClient, err := util.GetRestyClientWithCustomHeaders() + if err != nil { + failure <- err + fmt.Println("Error getting resty client with custom headers", err) + os.Exit(1) + } + + httpClient. SetAuthToken(userCredentials.JTWToken). SetHeader("Accept", "application/json") diff --git a/cli/packages/cmd/root.go b/cli/packages/cmd/root.go index c533f3415..b9370ad89 100644 --- a/cli/packages/cmd/root.go +++ b/cli/packages/cmd/root.go @@ -50,11 +50,12 @@ func init() { config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL) + // util.DisplayAptInstallationChangeBanner(silent) if !util.IsRunningInDocker() && !silent { util.CheckForUpdate() } - loggedInDetails, err := util.GetCurrentLoggedInUserDetails() + loggedInDetails, err := util.GetCurrentLoggedInUserDetails(false) if !silent && err == nil && loggedInDetails.IsUserLoggedIn && !loggedInDetails.LoginExpired { token, err := util.GetInfisicalToken(cmd) diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go index a232896f1..7f11a3f95 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -419,22 +419,23 @@ func executeCommandWithWatchMode(commandFlag string, args []string, watchModeInt for { <-recheckSecretsChannel - watchMutex.Lock() + func() { + watchMutex.Lock() + defer watchMutex.Unlock() - newEnvironmentVariables, err := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, token) - if err != nil { - log.Error().Err(err).Msg("[HOT RELOAD] Failed to fetch secrets") - continue - } + newEnvironmentVariables, err := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, token) + if err != nil { + log.Error().Err(err).Msg("[HOT RELOAD] Failed to fetch secrets") + return + } - if newEnvironmentVariables.ETag != currentETag { - runCommandWithWatcher(newEnvironmentVariables) - } else { - log.Debug().Msg("[HOT RELOAD] No changes detected in secrets, not reloading process") - } - - watchMutex.Unlock() + if newEnvironmentVariables.ETag != currentETag { + runCommandWithWatcher(newEnvironmentVariables) + } else { + log.Debug().Msg("[HOT RELOAD] No changes detected in secrets, not reloading process") + } + }() } } diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index eff011c5e..fdee3e7c0 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -5,6 +5,7 @@ package cmd import ( "fmt" + "os" "regexp" "sort" "strings" @@ -13,7 +14,6 @@ import ( "github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/util" "github.com/Infisical/infisical-merge/packages/visualize" - "github.com/go-resty/resty/v2" "github.com/posthog/posthog-go" "github.com/spf13/cobra" ) @@ -110,7 +110,7 @@ var secretsCmd = &cobra.Command{ if plainOutput { for _, secret := range secrets { - fmt.Println(secret.Value) + fmt.Println(fmt.Sprintf("%s=%s", secret.Key, secret.Value)) } } else { visualize.PrintAllSecretDetails(secrets) @@ -139,11 +139,19 @@ var secretsGenerateExampleEnvCmd = &cobra.Command{ } var secretsSetCmd = &cobra.Command{ - Example: `secrets set ..."`, + Example: `secrets set ..."`, Short: "Used set secrets", Use: "set [secrets]", DisableFlagsInUseLine: true, - Args: cobra.MinimumNArgs(1), + Args: func(cmd *cobra.Command, args []string) error { + if cmd.Flags().Changed("file") { + if len(args) > 0 { + return fmt.Errorf("secrets cannot be provided as command-line arguments when the --file option is used. Please choose either file-based or argument-based secret input") + } + return nil + } + return cobra.MinimumNArgs(1)(cmd, args) + }, Run: func(cmd *cobra.Command, args []string) { token, err := util.GetInfisicalToken(cmd) if err != nil { @@ -177,13 +185,42 @@ var secretsSetCmd = &cobra.Command{ util.HandleError(err, "Unable to parse secret type") } + processedArgs := []string{} + for _, arg := range args { + splitKeyValue := strings.SplitN(arg, "=", 2) + if len(splitKeyValue) != 2 { + util.HandleError(fmt.Errorf("invalid argument format: %s. Expected format: key=value or key=@filepath", arg), "") + } + + key := splitKeyValue[0] + value := splitKeyValue[1] + + if strings.HasPrefix(value, "\\@") { + value = "@" + value[2:] + } else if strings.HasPrefix(value, "@") { + filePath := strings.TrimPrefix(value, "@") + content, err := os.ReadFile(filePath) + if err != nil { + util.HandleError(err, fmt.Sprintf("Unable to read file %s", filePath)) + } + value = string(content) + } + + processedArgs = append(processedArgs, fmt.Sprintf("%s=%s", key, value)) + } + + file, err := cmd.Flags().GetString("file") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + var secretOperations []models.SecretSetOperation if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { if projectId == "" { util.PrintErrorMessageAndExit("When using service tokens or machine identities, you must set the --projectId flag") } - secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, token) + secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, token, file) } else { if projectId == "" { workspaceFile, err := util.GetWorkSpaceFromFile() @@ -194,7 +231,7 @@ var secretsSetCmd = &cobra.Command{ projectId = workspaceFile.WorkspaceId } - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) if err != nil { util.HandleError(err, "unable to authenticate [err=%v]") } @@ -203,10 +240,10 @@ var secretsSetCmd = &cobra.Command{ util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") } - secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, &models.TokenDetails{ + secretOperations, err = util.SetRawSecrets(processedArgs, secretType, environmentName, secretsPath, projectId, &models.TokenDetails{ Type: "", Token: loggedInUserDetails.UserCredentials.JTWToken, - }) + }, file) } if err != nil { @@ -261,8 +298,12 @@ var secretsDeleteCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - httpClient := resty.New(). - SetHeader("Accept", "application/json") + httpClient, err := util.GetRestyClientWithCustomHeaders() + if err != nil { + util.HandleError(err, "Unable to get resty client with custom headers") + } + + httpClient.SetHeader("Accept", "application/json") if projectId == "" { workspaceFile, err := util.GetWorkSpaceFromFile() @@ -278,7 +319,7 @@ var secretsDeleteCmd = &cobra.Command{ util.RequireLogin() util.RequireLocalWorkspaceFile() - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) if err != nil { util.HandleError(err, "Unable to authenticate") } @@ -691,6 +732,7 @@ func init() { secretsSetCmd.Flags().String("projectId", "", "manually set the project ID to for setting secrets when using machine identity based auth") secretsSetCmd.Flags().String("path", "/", "set secrets within a folder path") secretsSetCmd.Flags().String("type", util.SECRET_TYPE_SHARED, "the type of secret to create: personal or shared") + secretsSetCmd.Flags().String("file", "", "Load secrets from the specified file. File format: .env or YAML (comments: # or //). This option is mutually exclusive with command-line secrets arguments.") secretsDeleteCmd.Flags().String("type", "personal", "the type of secret to delete: personal or shared (default: personal)") secretsDeleteCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") diff --git a/cli/packages/cmd/ssh.go b/cli/packages/cmd/ssh.go new file mode 100644 index 000000000..5b2bb37bb --- /dev/null +++ b/cli/packages/cmd/ssh.go @@ -0,0 +1,1024 @@ +/* +Copyright (c) 2023 Infisical Inc. +*/ +package cmd + +import ( + "context" + "fmt" + "net" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/Infisical/infisical-merge/packages/api" + "github.com/Infisical/infisical-merge/packages/config" + "github.com/Infisical/infisical-merge/packages/util" + infisicalSdk "github.com/infisical/go-sdk" + infisicalSdkUtil "github.com/infisical/go-sdk/packages/util" + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" +) + +var sshCmd = &cobra.Command{ + Example: `infisical ssh`, + Short: "Used to issue SSH credentials", + Use: "ssh", + DisableFlagsInUseLine: true, + Args: cobra.NoArgs, +} + +var sshIssueCredentialsCmd = &cobra.Command{ + Example: `ssh issue-credentials`, + Short: "Used to issue SSH credentials against a certificate template", + Use: "issue-credentials", + DisableFlagsInUseLine: true, + Args: cobra.NoArgs, + Run: issueCredentials, +} + +var sshSignKeyCmd = &cobra.Command{ + Example: `ssh sign-key`, + Short: "Used to sign a SSH public key against a certificate template", + Use: "sign-key", + DisableFlagsInUseLine: true, + Args: cobra.NoArgs, + Run: signKey, +} + +var sshConnectCmd = &cobra.Command{ + Use: "connect", + Short: "Connect to an SSH host using issued credentials", + Run: sshConnect, +} + +var sshAddHostCmd = &cobra.Command{ + Use: "add-host", + Short: "Register a new SSH host with Infisical", + Run: sshAddHost, +} + +var algoToFileName = map[infisicalSdkUtil.CertKeyAlgorithm]string{ + infisicalSdkUtil.RSA2048: "id_rsa_2048", + infisicalSdkUtil.RSA4096: "id_rsa_4096", + infisicalSdkUtil.ECDSAP256: "id_ecdsa_p256", + infisicalSdkUtil.ECDSAP384: "id_ecdsa_p384", +} + +func isValidKeyAlgorithm(algo infisicalSdkUtil.CertKeyAlgorithm) bool { + _, exists := algoToFileName[algo] + return exists +} + +func isValidCertType(certType infisicalSdkUtil.SshCertType) bool { + switch certType { + case infisicalSdkUtil.UserCert, infisicalSdkUtil.HostCert: + return true + default: + return false + } +} + +func writeToFile(filePath string, content string, perm os.FileMode) error { + // Ensure the directory exists + dir := filepath.Dir(filePath) + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("failed to create directory %s: %w", dir, err) + } + + // Write the content to the file + err := os.WriteFile(filePath, []byte(content), perm) + if err != nil { + return fmt.Errorf("failed to write to file %s: %w", filePath, err) + } + + return nil +} + +func addCredentialsToAgent(privateKeyContent, certContent string) error { + // Parse the private key + privateKey, err := ssh.ParseRawPrivateKey([]byte(privateKeyContent)) + if err != nil { + return fmt.Errorf("failed to parse private key: %w", err) + } + + // Parse the certificate + pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(certContent)) + if err != nil { + return fmt.Errorf("failed to parse certificate: %w", err) + } + + cert, ok := pubKey.(*ssh.Certificate) + if !ok { + return fmt.Errorf("parsed key is not a certificate") + } + // Calculate LifetimeSecs based on certificate's valid-to time + validUntil := time.Unix(int64(cert.ValidBefore), 0) + now := time.Now() + + // Handle ValidBefore as either a timestamp or an enumeration + // SSH certificates use ValidBefore as a timestamp unless set to 0 or ~0 + if cert.ValidBefore == ssh.CertTimeInfinity { + // If certificate never expires, set default lifetime to 1 year (can adjust as needed) + validUntil = now.Add(365 * 24 * time.Hour) + } + + // Calculate the duration until expiration + lifetime := validUntil.Sub(now) + if lifetime <= 0 { + return fmt.Errorf("certificate is already expired") + } + + // Convert duration to seconds + lifetimeSecs := uint32(lifetime.Seconds()) + + // Connect to the SSH agent + socket := os.Getenv("SSH_AUTH_SOCK") + if socket == "" { + return fmt.Errorf("SSH_AUTH_SOCK not set") + } + + conn, err := net.Dial("unix", socket) + if err != nil { + return fmt.Errorf("failed to connect to SSH agent: %w", err) + } + defer conn.Close() + + agentClient := agent.NewClient(conn) + + // Add the key with certificate to the agent + err = agentClient.Add(agent.AddedKey{ + PrivateKey: privateKey, + Certificate: cert, + Comment: "Added via Infisical CLI", + LifetimeSecs: lifetimeSecs, + }) + if err != nil { + return fmt.Errorf("failed to add key to agent: %w", err) + } + + return nil +} + +func issueCredentials(cmd *cobra.Command, args []string) { + + token, err := util.GetInfisicalToken(cmd) + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + var infisicalToken string + + if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { + infisicalToken = token.Token + } else { + util.RequireLogin() + util.RequireLocalWorkspaceFile() + + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) + if err != nil { + util.HandleError(err, "Unable to authenticate") + } + + if loggedInUserDetails.LoginExpired { + util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + } + infisicalToken = loggedInUserDetails.UserCredentials.JTWToken + } + + certificateTemplateId, err := cmd.Flags().GetString("certificateTemplateId") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + if certificateTemplateId == "" { + util.PrintErrorMessageAndExit("You must set the --certificateTemplateId flag") + } + + principalsStr, err := cmd.Flags().GetString("principals") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + // Check if the input string is empty before splitting + if principalsStr == "" { + util.HandleError(fmt.Errorf("no principals provided"), "The 'principals' flag cannot be empty") + } + + // Convert the comma-delimited string into a slice of strings + principals := strings.Split(principalsStr, ",") + for i, principal := range principals { + principals[i] = strings.TrimSpace(principal) + } + + keyAlgorithm, err := cmd.Flags().GetString("keyAlgorithm") + if err != nil { + util.HandleError(err, "Unable to parse keyAlgorithm flag") + } + + if !isValidKeyAlgorithm(infisicalSdkUtil.CertKeyAlgorithm(keyAlgorithm)) { + util.HandleError(fmt.Errorf("invalid keyAlgorithm: %s", keyAlgorithm), + "Valid values: RSA_2048, RSA_4096, EC_prime256v1, EC_secp384r1") + } + + certType, err := cmd.Flags().GetString("certType") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + if !isValidCertType(infisicalSdkUtil.SshCertType(certType)) { + util.HandleError(fmt.Errorf("invalid certType: %s", certType), + "Valid values: user, host") + } + + ttl, err := cmd.Flags().GetString("ttl") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + keyId, err := cmd.Flags().GetString("keyId") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + outFilePath, err := cmd.Flags().GetString("outFilePath") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + addToAgent, err := cmd.Flags().GetBool("addToAgent") + if err != nil { + util.HandleError(err, "Unable to parse addToAgent flag") + } + + if outFilePath == "" && !addToAgent { + util.PrintErrorMessageAndExit("You must provide either --outFilePath or --addToAgent flag to use this command") + } + + var ( + outputDir string + privateKeyPath string + publicKeyPath string + signedKeyPath string + ) + + if outFilePath != "" { + // Expand ~ to home directory if present + if strings.HasPrefix(outFilePath, "~") { + homeDir, err := os.UserHomeDir() + if err != nil { + util.HandleError(err, "Failed to resolve home directory") + } + outFilePath = strings.Replace(outFilePath, "~", homeDir, 1) + } + + // Check if outFilePath ends with "-cert.pub" + if strings.HasSuffix(outFilePath, "-cert.pub") { + // Treat outFilePath as the signed key path + signedKeyPath = outFilePath + + // Derive the base name by removing "-cert.pub" + baseName := strings.TrimSuffix(filepath.Base(outFilePath), "-cert.pub") + + // Set the output directory + outputDir = filepath.Dir(outFilePath) + + // Define private and public key paths + privateKeyPath = filepath.Join(outputDir, baseName) + publicKeyPath = filepath.Join(outputDir, baseName+".pub") + } else { + // Treat outFilePath as a directory + outputDir = outFilePath + + // Check if the directory exists; if not, create it + info, err := os.Stat(outputDir) + if os.IsNotExist(err) { + err = os.MkdirAll(outputDir, 0755) + if err != nil { + util.HandleError(err, "Failed to create output directory") + } + } else if err != nil { + util.HandleError(err, "Failed to access output directory") + } else if !info.IsDir() { + util.PrintErrorMessageAndExit("The provided --outFilePath is not a directory") + } + } + } + + // Define file names based on key algorithm + fileName := algoToFileName[infisicalSdkUtil.CertKeyAlgorithm(keyAlgorithm)] + + // Define file paths + privateKeyPath = filepath.Join(outputDir, fileName) + publicKeyPath = filepath.Join(outputDir, fileName+".pub") + signedKeyPath = filepath.Join(outputDir, fileName+"-cert.pub") + + // If outFilePath ends with "-cert.pub", ensure the signedKeyPath is set + if strings.HasSuffix(outFilePath, "-cert.pub") { + // Ensure the signedKeyPath was set + if signedKeyPath == "" { + util.HandleError(fmt.Errorf("signedKeyPath is not set correctly"), "Internal error") + } + } else { + // Ensure all paths are set + if privateKeyPath == "" || publicKeyPath == "" || signedKeyPath == "" { + util.HandleError(fmt.Errorf("file paths are not set correctly"), "Internal error") + } + } + + customHeaders, err := util.GetInfisicalCustomHeadersMap() + if err != nil { + util.HandleError(err, "Unable to get custom headers") + } + + infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ + SiteUrl: config.INFISICAL_URL, + UserAgent: api.USER_AGENT, + AutoTokenRefresh: false, + CustomHeaders: customHeaders, + }) + infisicalClient.Auth().SetAccessToken(infisicalToken) + + creds, err := infisicalClient.Ssh().IssueCredentials(infisicalSdk.IssueSshCredsOptions{ + CertificateTemplateID: certificateTemplateId, + Principals: principals, + KeyAlgorithm: infisicalSdkUtil.CertKeyAlgorithm(keyAlgorithm), + CertType: infisicalSdkUtil.SshCertType(certType), + TTL: ttl, + KeyID: keyId, + }) + + if err != nil { + util.HandleError(err, "Failed to issue SSH credentials") + } + + if outFilePath != "" { + // If signedKeyPath wasn't set in the directory scenario, set it now + if signedKeyPath == "" { + fileName := algoToFileName[infisicalSdkUtil.CertKeyAlgorithm(keyAlgorithm)] + signedKeyPath = filepath.Join(outputDir, fileName+"-cert.pub") + } + + if privateKeyPath == "" { + privateKeyPath = filepath.Join(outputDir, algoToFileName[infisicalSdkUtil.CertKeyAlgorithm(keyAlgorithm)]) + } + err = writeToFile(privateKeyPath, creds.PrivateKey, 0600) + if err != nil { + util.HandleError(err, "Failed to write Private Key to file") + } + + if publicKeyPath == "" { + publicKeyPath = privateKeyPath + ".pub" + } + err = writeToFile(publicKeyPath, creds.PublicKey, 0644) + if err != nil { + util.HandleError(err, "Failed to write Public Key to file") + } + + err = writeToFile(signedKeyPath, creds.SignedKey, 0644) + if err != nil { + util.HandleError(err, "Failed to write Signed Key to file") + } + + fmt.Println("Successfully wrote SSH certificate to:", signedKeyPath) + } + + // Add SSH credentials to the SSH agent if needed + if addToAgent { + // Call the helper function to handle add-to-agent flow + err := addCredentialsToAgent(creds.PrivateKey, creds.SignedKey) + if err != nil { + util.HandleError(err, "Failed to add keys to SSH agent") + } else { + fmt.Println("The SSH key and certificate have been successfully added to your ssh-agent.") + } + } +} + +func signKey(cmd *cobra.Command, args []string) { + + token, err := util.GetInfisicalToken(cmd) + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + var infisicalToken string + + if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { + infisicalToken = token.Token + } else { + util.RequireLogin() + util.RequireLocalWorkspaceFile() + + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) + if err != nil { + util.HandleError(err, "Unable to authenticate") + } + + if loggedInUserDetails.LoginExpired { + util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + } + infisicalToken = loggedInUserDetails.UserCredentials.JTWToken + } + + certificateTemplateId, err := cmd.Flags().GetString("certificateTemplateId") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + if certificateTemplateId == "" { + util.PrintErrorMessageAndExit("You must set the --certificateTemplateId flag") + } + + publicKey, err := cmd.Flags().GetString("publicKey") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + publicKeyFilePath, err := cmd.Flags().GetString("publicKeyFilePath") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + if publicKey == "" && publicKeyFilePath == "" { + util.HandleError(fmt.Errorf("either --publicKey or --publicKeyFilePath must be provided"), "Invalid input") + } + + if publicKey != "" && publicKeyFilePath != "" { + util.HandleError(fmt.Errorf("only one of --publicKey or --publicKeyFile can be provided"), "Invalid input") + } + + if publicKeyFilePath != "" { + if strings.HasPrefix(publicKeyFilePath, "~") { + // Expand the tilde (~) to the user's home directory + homeDir, err := os.UserHomeDir() + if err != nil { + util.HandleError(err, "Failed to resolve home directory") + } + publicKeyFilePath = strings.Replace(publicKeyFilePath, "~", homeDir, 1) + } + + // Ensure the file has a .pub extension + if !strings.HasSuffix(publicKeyFilePath, ".pub") { + util.HandleError(fmt.Errorf("public key file must have a .pub extension"), "Invalid input") + } + + content, err := os.ReadFile(publicKeyFilePath) + if err != nil { + util.HandleError(err, "Failed to read public key file") + } + + publicKey = strings.TrimSpace(string(content)) + } + + if strings.TrimSpace(publicKey) == "" { + util.HandleError(fmt.Errorf("Public key is empty"), "Invalid input") + } + + principalsStr, err := cmd.Flags().GetString("principals") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + // Check if the input string is empty before splitting + if principalsStr == "" { + util.HandleError(fmt.Errorf("no principals provided"), "The 'principals' flag cannot be empty") + } + + // Convert the comma-delimited string into a slice of strings + principals := strings.Split(principalsStr, ",") + for i, principal := range principals { + principals[i] = strings.TrimSpace(principal) + } + + certType, err := cmd.Flags().GetString("certType") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + if !isValidCertType(infisicalSdkUtil.SshCertType(certType)) { + util.HandleError(fmt.Errorf("invalid certType: %s", certType), + "Valid values: user, host") + } + + ttl, err := cmd.Flags().GetString("ttl") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + keyId, err := cmd.Flags().GetString("keyId") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + outFilePath, err := cmd.Flags().GetString("outFilePath") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + var ( + outputDir string + signedKeyPath string + ) + + if outFilePath == "" { + // Use current working directory + if err != nil { + util.HandleError(err, "Failed to get current working directory") + } + + // check if public key path exists + if publicKeyFilePath == "" { + util.PrintErrorMessageAndExit("--outFilePath must be specified when --publicKeyFilePath is not provided") + } + + outputDir = filepath.Dir(publicKeyFilePath) + // Derive the base name by removing "-cert.pub" + baseName := strings.TrimSuffix(filepath.Base(publicKeyFilePath), ".pub") + signedKeyPath = filepath.Join(outputDir, baseName+"-cert.pub") + } else { + // Expand ~ to home directory if present + if strings.HasPrefix(outFilePath, "~") { + homeDir, err := os.UserHomeDir() + if err != nil { + util.HandleError(err, "Failed to resolve home directory") + } + outFilePath = strings.Replace(outFilePath, "~", homeDir, 1) + } + + // Check if outFilePath ends with "-cert.pub" + if !strings.HasSuffix(outFilePath, "-cert.pub") { + util.PrintErrorMessageAndExit("--outFilePath must end with -cert.pub") + } + + // Extract the directory from outFilePath + outputDir = filepath.Dir(outFilePath) + + // Validate the output directory + info, err := os.Stat(outputDir) + if os.IsNotExist(err) { + // Directory does not exist; attempt to create it + err = os.MkdirAll(outputDir, 0755) + if err != nil { + util.HandleError(err, "Failed to create output directory") + } + } else if err != nil { + // Other errors accessing the directory + util.HandleError(err, "Failed to access output directory") + } else if !info.IsDir() { + // Path exists but is not a directory + util.PrintErrorMessageAndExit("The provided --outFilePath's directory is not valid") + } + + signedKeyPath = outFilePath + } + + customHeaders, err := util.GetInfisicalCustomHeadersMap() + if err != nil { + util.HandleError(err, "Unable to get custom headers") + } + + infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ + SiteUrl: config.INFISICAL_URL, + UserAgent: api.USER_AGENT, + AutoTokenRefresh: false, + CustomHeaders: customHeaders, + }) + infisicalClient.Auth().SetAccessToken(infisicalToken) + + creds, err := infisicalClient.Ssh().SignKey(infisicalSdk.SignSshPublicKeyOptions{ + CertificateTemplateID: certificateTemplateId, + PublicKey: publicKey, + Principals: principals, + CertType: infisicalSdkUtil.SshCertType(certType), + TTL: ttl, + KeyID: keyId, + }) + + if err != nil { + util.HandleError(err, "Failed to sign SSH public key") + } + + err = writeToFile(signedKeyPath, creds.SignedKey, 0644) + if err != nil { + util.HandleError(err, "Failed to write Signed Key to file") + } + + fmt.Println("Successfully wrote SSH certificate to:", signedKeyPath) +} + +func sshConnect(cmd *cobra.Command, args []string) { + util.RequireLogin() + util.RequireLocalWorkspaceFile() + + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) + if err != nil { + util.HandleError(err, "Unable to authenticate") + } + + if loggedInUserDetails.LoginExpired { + util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + } + + infisicalToken := loggedInUserDetails.UserCredentials.JTWToken + + writeHostCaToFile, err := cmd.Flags().GetBool("writeHostCaToFile") + if err != nil { + util.HandleError(err, "Unable to parse --writeHostCaToFile flag") + } + + customHeaders, err := util.GetInfisicalCustomHeadersMap() + if err != nil { + util.HandleError(err, "Unable to get custom headers") + } + + infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ + SiteUrl: config.INFISICAL_URL, + UserAgent: api.USER_AGENT, + AutoTokenRefresh: false, + CustomHeaders: customHeaders, + }) + infisicalClient.Auth().SetAccessToken(infisicalToken) + + // Fetch SSH Hosts + hosts, err := infisicalClient.Ssh().GetSshHosts(infisicalSdk.GetSshHostsOptions{}) + if err != nil { + util.HandleError(err, "Failed to fetch SSH hosts") + } + if len(hosts) == 0 { + util.PrintErrorMessageAndExit("You do not have access to any SSH hosts") + } + + // Prompt to select host + hostNames := make([]string, len(hosts)) + for i, h := range hosts { + hostNames[i] = h.Hostname + } + + hostPrompt := promptui.Select{ + Label: "Select an SSH Host", + Items: hostNames, + Size: 10, + } + hostIdx, _, err := hostPrompt.Run() + if err != nil { + util.HandleError(err, "Prompt failed") + } + selectedHost := hosts[hostIdx] + + // Prompt to select login user + if len(selectedHost.LoginMappings) == 0 { + util.PrintErrorMessageAndExit("No login users available for selected host") + } + + loginUsers := make([]string, len(selectedHost.LoginMappings)) + for i, m := range selectedHost.LoginMappings { + loginUsers[i] = m.LoginUser + } + + loginPrompt := promptui.Select{ + Label: "Select Login User", + Items: loginUsers, + Size: 5, + } + loginIdx, _, err := loginPrompt.Run() + if err != nil { + util.HandleError(err, "Prompt failed") + } + selectedLoginUser := selectedHost.LoginMappings[loginIdx].LoginUser + + // Issue SSH creds for host + creds, err := infisicalClient.Ssh().IssueSshHostUserCert(selectedHost.ID, infisicalSdk.IssueSshHostUserCertOptions{ + LoginUser: selectedLoginUser, + }) + if err != nil { + util.HandleError(err, "Failed to issue SSH credentials") + } + + // Write Host CA public key to known_hosts if enabled + if writeHostCaToFile { + hostCaPublicKey, err := infisicalClient.Ssh().GetSshHostHostCaPublicKey(selectedHost.ID) + if err != nil { + util.HandleError(err, "Failed to fetch Host CA public key") + } + + // Build @cert-authority line + caLine := fmt.Sprintf("@cert-authority %s %s\n", selectedHost.Hostname, strings.TrimSpace(hostCaPublicKey)) + + // Determine known_hosts path + sshDir := filepath.Join(os.Getenv("HOME"), ".ssh") + knownHostsPath := filepath.Join(sshDir, "known_hosts") + + // Ensure ~/.ssh exists + if _, err := os.Stat(sshDir); os.IsNotExist(err) { + if err := os.MkdirAll(sshDir, 0700); err != nil { + util.HandleError(err, "Failed to create ~/.ssh directory") + } + } + + // Check if CA line already exists + knownHostsBytes, _ := os.ReadFile(knownHostsPath) + if !strings.Contains(string(knownHostsBytes), caLine) { + f, err := os.OpenFile(knownHostsPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + util.HandleError(err, "Failed to open known_hosts file") + } + defer f.Close() + + if _, err := f.WriteString(caLine); err != nil { + util.HandleError(err, "Failed to write Host CA to known_hosts") + } + + fmt.Printf("📁 Wrote Host CA entry to %s\n", knownHostsPath) + } + } + + // Load credentials into SSH agent + err = addCredentialsToAgent(creds.PrivateKey, creds.SignedKey) + if err != nil { + util.HandleError(err, "Failed to add credentials to SSH agent") + } + fmt.Println("✔ SSH credentials successfully added to agent") + + // Connect to host using system ssh and agent + target := fmt.Sprintf("%s@%s", selectedLoginUser, selectedHost.Hostname) + fmt.Printf("Connecting to %s...\n", target) + + sshCmd := exec.Command("ssh", target) + sshCmd.Stdin = os.Stdin + sshCmd.Stdout = os.Stdout + sshCmd.Stderr = os.Stderr + + err = sshCmd.Run() + if err != nil { + util.HandleError(err, "SSH connection failed") + } +} + +func sshAddHost(cmd *cobra.Command, args []string) { + + token, err := util.GetInfisicalToken(cmd) + if err != nil { + util.HandleError(err, "Unable to parse token") + } + + var infisicalToken string + if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { + infisicalToken = token.Token + } else { + util.RequireLogin() + util.RequireLocalWorkspaceFile() + + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) + if err != nil { + util.HandleError(err, "Unable to authenticate") + } + if loggedInUserDetails.LoginExpired { + util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login]") + } + infisicalToken = loggedInUserDetails.UserCredentials.JTWToken + } + + projectId, err := cmd.Flags().GetString("projectId") + if err != nil { + util.HandleError(err, "Unable to parse --projectId flag") + } + if projectId == "" { + util.PrintErrorMessageAndExit("You must provide --projectId") + } + + hostname, err := cmd.Flags().GetString("hostname") + if err != nil { + util.HandleError(err, "Unable to parse --hostname flag") + } + if hostname == "" { + util.PrintErrorMessageAndExit("You must provide --hostname") + } + + writeUserCaToFile, err := cmd.Flags().GetBool("writeUserCaToFile") + if err != nil { + util.HandleError(err, "Unable to parse --writeUserCaToFile flag") + } + + userCaOutFilePath, err := cmd.Flags().GetString("userCaOutFilePath") + if err != nil { + util.HandleError(err, "Unable to parse --userCaOutFilePath flag") + } + + writeHostCertToFile, err := cmd.Flags().GetBool("writeHostCertToFile") + if err != nil { + util.HandleError(err, "Unable to parse --writeHostCertToFile flag") + } + + configureSshd, err := cmd.Flags().GetBool("configureSshd") + if err != nil { + util.HandleError(err, "Unable to parse --configureSshd flag") + } + + forceOverwrite, err := cmd.Flags().GetBool("force") + if err != nil { + util.HandleError(err, "Unable to parse --force flag") + } + + if configureSshd && (!writeUserCaToFile || !writeHostCertToFile) { + util.PrintErrorMessageAndExit("--configureSshd requires both --writeUserCaToFile and --writeHostCertToFile to also be set") + } + + // Pre-check for file overwrites before proceeding + if writeUserCaToFile { + if strings.HasPrefix(userCaOutFilePath, "~") { + homeDir, err := os.UserHomeDir() + if err != nil { + util.HandleError(err, "Unable to resolve ~ in userCaOutFilePath") + } + userCaOutFilePath = strings.Replace(userCaOutFilePath, "~", homeDir, 1) + } + if _, err := os.Stat(userCaOutFilePath); err == nil && !forceOverwrite { + util.PrintErrorMessageAndExit("File already exists at " + userCaOutFilePath + ". Use --force to overwrite.") + } + } + + keyTypes := []string{"ed25519", "ecdsa", "rsa"} + var hostKeyPath, certOutPath, hostPrivateKeyPath string + if writeHostCertToFile { + for _, keyType := range keyTypes { + pub := fmt.Sprintf("/etc/ssh/ssh_host_%s_key.pub", keyType) + cert := fmt.Sprintf("/etc/ssh/ssh_host_%s_key-cert.pub", keyType) + priv := fmt.Sprintf("/etc/ssh/ssh_host_%s_key", keyType) + + if _, err := os.Stat(pub); err == nil { + hostKeyPath = pub + certOutPath = cert + hostPrivateKeyPath = priv + break + } + } + + if hostKeyPath == "" { + util.PrintErrorMessageAndExit("No supported SSH host public key found at /etc/ssh") + } + + if _, err := os.Stat(certOutPath); err == nil && !forceOverwrite { + util.PrintErrorMessageAndExit("File already exists at " + certOutPath + ". Use --force to overwrite.") + } + } + + if configureSshd { + sshdConfig := "/etc/ssh/sshd_config" + existing, err := os.ReadFile(sshdConfig) + if err != nil { + util.HandleError(err, "Failed to read sshd_config") + } + configLines := []string{ + "TrustedUserCAKeys " + userCaOutFilePath, + "HostKey " + hostPrivateKeyPath, + "HostCertificate " + certOutPath, + } + for _, line := range configLines { + for _, existingLine := range strings.Split(string(existing), "\n") { + trimmed := strings.TrimSpace(existingLine) + if trimmed == line && !strings.HasPrefix(trimmed, "#") && !forceOverwrite { + util.PrintErrorMessageAndExit("sshd_config already contains: " + line + ". Use --force to overwrite.") + } + } + } + } + + customHeaders, err := util.GetInfisicalCustomHeadersMap() + if err != nil { + util.HandleError(err, "Unable to get custom headers") + } + + client := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ + SiteUrl: config.INFISICAL_URL, + UserAgent: api.USER_AGENT, + AutoTokenRefresh: false, + CustomHeaders: customHeaders, + }) + client.Auth().SetAccessToken(infisicalToken) + + host, err := client.Ssh().AddSshHost(infisicalSdk.AddSshHostOptions{ + ProjectID: projectId, + Hostname: hostname, + }) + if err != nil { + util.HandleError(err, "Failed to register SSH host") + } + + fmt.Println("✅ Successfully registered host:", host.Hostname) + + if writeUserCaToFile { + publicKey, err := client.Ssh().GetSshHostUserCaPublicKey(host.ID) + if err != nil { + util.HandleError(err, "Failed to fetch associated User CA public key") + } + + if err := writeToFile(userCaOutFilePath, publicKey, 0644); err != nil { + util.HandleError(err, "Failed to write User CA public key to file") + } + + fmt.Println("📁 Wrote User CA public key to:", userCaOutFilePath) + } + + if writeHostCertToFile { + pubKeyBytes, err := os.ReadFile(hostKeyPath) + if err != nil { + util.HandleError(err, "Failed to read SSH host public key") + } + res, err := client.Ssh().IssueSshHostHostCert(host.ID, infisicalSdk.IssueSshHostHostCertOptions{ + PublicKey: string(pubKeyBytes), + }) + if err != nil { + util.HandleError(err, "Failed to issue SSH host certificate") + } + if err := writeToFile(certOutPath, res.SignedKey, 0644); err != nil { + util.HandleError(err, "Failed to write SSH host certificate to file") + } + fmt.Println("📁 Wrote host certificate to:", certOutPath) + } + + if configureSshd { + sshdConfig := "/etc/ssh/sshd_config" + contentBytes, err := os.ReadFile(sshdConfig) + if err != nil { + util.HandleError(err, "Failed to read sshd_config") + } + lines := strings.Split(string(contentBytes), "\n") + + configMap := map[string]string{ + "TrustedUserCAKeys": userCaOutFilePath, + "HostKey": hostPrivateKeyPath, + "HostCertificate": certOutPath, + } + + seenKeys := map[string]bool{} + for i, line := range lines { + trimmed := strings.TrimSpace(line) + for key, value := range configMap { + if strings.HasPrefix(trimmed, key+" ") { + seenKeys[key] = true + if strings.HasPrefix(trimmed, "#") || forceOverwrite { + lines[i] = fmt.Sprintf("%s %s", key, value) + } else { + util.PrintErrorMessageAndExit("sshd_config already contains: " + trimmed + ". Use --force to overwrite.") + } + } + } + } + + // Append missing lines + for key, value := range configMap { + if !seenKeys[key] { + lines = append(lines, fmt.Sprintf("%s %s", key, value)) + } + } + + // Write back to file + if err := os.WriteFile(sshdConfig, []byte(strings.Join(lines, "\n")), 0644); err != nil { + util.HandleError(err, "Failed to update sshd_config") + } + fmt.Println("📄 Updated sshd_config entries") + } +} + +func init() { + sshSignKeyCmd.Flags().String("token", "", "Issue SSH certificate using machine identity access token") + sshSignKeyCmd.Flags().String("certificateTemplateId", "", "The ID of the SSH certificate template to issue the SSH certificate for") + sshSignKeyCmd.Flags().String("publicKey", "", "The public key to sign") + sshSignKeyCmd.Flags().String("publicKeyFilePath", "", "The file path to the public key file to sign") + sshSignKeyCmd.Flags().String("outFilePath", "", "The path to write the SSH certificate to such as ~/.ssh/id_rsa-cert.pub. If not provided, the credentials will be saved to the directory of the specified public key file path or the current working directory") + sshSignKeyCmd.Flags().String("principals", "", "The principals that the certificate should be signed for") + sshSignKeyCmd.Flags().String("certType", string(infisicalSdkUtil.UserCert), "The cert type for the created certificate") + sshSignKeyCmd.Flags().String("ttl", "", "The ttl for the created certificate") + sshSignKeyCmd.Flags().String("keyId", "", "The keyId that the created certificate should have") + sshCmd.AddCommand(sshSignKeyCmd) + + sshIssueCredentialsCmd.Flags().String("token", "", "Issue SSH credentials using machine identity access token") + sshIssueCredentialsCmd.Flags().String("certificateTemplateId", "", "The ID of the SSH certificate template to issue SSH credentials for") + sshIssueCredentialsCmd.Flags().String("principals", "", "The principals to issue SSH credentials for") + sshIssueCredentialsCmd.Flags().String("keyAlgorithm", string(infisicalSdkUtil.RSA2048), "The key algorithm to issue SSH credentials for") + sshIssueCredentialsCmd.Flags().String("certType", string(infisicalSdkUtil.UserCert), "The cert type to issue SSH credentials for") + sshIssueCredentialsCmd.Flags().String("ttl", "", "The ttl to issue SSH credentials for") + sshIssueCredentialsCmd.Flags().String("keyId", "", "The keyId to issue SSH credentials for") + sshIssueCredentialsCmd.Flags().String("outFilePath", "", "The path to write the SSH credentials to such as ~/.ssh, ./some_folder, ./some_folder/id_rsa-cert.pub. If not provided, the credentials will be saved to the current working directory") + sshIssueCredentialsCmd.Flags().Bool("addToAgent", false, "Whether to add issued SSH credentials to the SSH agent") + sshCmd.AddCommand(sshIssueCredentialsCmd) + + sshConnectCmd.Flags().Bool("writeHostCaToFile", true, "Write Host CA public key to ~/.ssh/known_hosts as a separate entry if doesn't already exist") + sshCmd.AddCommand(sshConnectCmd) + + sshAddHostCmd.Flags().String("token", "", "Use a machine identity access token") + sshAddHostCmd.Flags().String("projectId", "", "Project ID the host belongs to (required)") + sshAddHostCmd.Flags().String("hostname", "", "Hostname of the SSH host (required)") + sshAddHostCmd.Flags().Bool("writeUserCaToFile", false, "Write User CA public key to /etc/ssh/infisical_user_ca.pub") + sshAddHostCmd.Flags().String("userCaOutFilePath", "/etc/ssh/infisical_user_ca.pub", "Custom file path to write the User CA public key") + sshAddHostCmd.Flags().Bool("writeHostCertToFile", false, "Write SSH host certificate to /etc/ssh/ssh_host__key-cert.pub") + sshAddHostCmd.Flags().Bool("configureSshd", false, "Update TrustedUserCAKeys, HostKey, and HostCertificate in the sshd_config file") + sshAddHostCmd.Flags().Bool("force", false, "Force overwrite of existing certificate files as part of writeUserCaToFile and writeHostCertToFile") + + sshCmd.AddCommand(sshAddHostCmd) + + rootCmd.AddCommand(sshCmd) +} diff --git a/cli/packages/cmd/tokens.go b/cli/packages/cmd/tokens.go index e2851f88f..386a7eda5 100644 --- a/cli/packages/cmd/tokens.go +++ b/cli/packages/cmd/tokens.go @@ -13,7 +13,6 @@ import ( "github.com/Infisical/infisical-merge/packages/api" "github.com/Infisical/infisical-merge/packages/crypto" "github.com/Infisical/infisical-merge/packages/util" - "github.com/go-resty/resty/v2" "github.com/spf13/cobra" ) @@ -41,7 +40,7 @@ var tokensCreateCmd = &cobra.Command{ }, Run: func(cmd *cobra.Command, args []string) { // get plain text workspace key - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) if err != nil { util.HandleError(err, "Unable to retrieve your logged in your details. Please login in then try again") @@ -136,7 +135,11 @@ var tokensCreateCmd = &cobra.Command{ } // make a call to the api to save the encrypted symmetric key details - httpClient := resty.New() + httpClient, err := util.GetRestyClientWithCustomHeaders() + if err != nil { + util.HandleError(err, "Unable to get resty client with custom headers") + } + httpClient.SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). SetHeader("Accept", "application/json") diff --git a/cli/packages/gateway/connection.go b/cli/packages/gateway/connection.go new file mode 100644 index 000000000..58a0503ff --- /dev/null +++ b/cli/packages/gateway/connection.go @@ -0,0 +1,142 @@ +package gateway + +import ( + "bufio" + "bytes" + "context" + "errors" + "io" + "net" + "strings" + "sync" + + "github.com/quic-go/quic-go" + "github.com/rs/zerolog/log" +) + +func handleConnection(ctx context.Context, quicConn quic.Connection) { + log.Info().Msgf("New connection from: %s", quicConn.RemoteAddr().String()) + // Use WaitGroup to track all streams + var wg sync.WaitGroup + for { + // Accept the first stream, which we'll use for commands + stream, err := quicConn.AcceptStream(ctx) + if err != nil { + log.Printf("Failed to accept QUIC stream: %v", err) + break + } + wg.Add(1) + go func(stream quic.Stream) { + defer wg.Done() + defer stream.Close() + + handleStream(stream, quicConn) + }(stream) + } + + wg.Wait() + log.Printf("All streams closed for connection: %s", quicConn.RemoteAddr().String()) +} + +func handleStream(stream quic.Stream, quicConn quic.Connection) { + streamID := stream.StreamID() + log.Printf("New stream %d from: %s", streamID, quicConn.RemoteAddr().String()) + + // Use buffered reader for better handling of fragmented data + reader := bufio.NewReader(stream) + defer stream.Close() + + for { + msg, err := reader.ReadBytes('\n') + if err != nil { + if errors.Is(err, io.EOF) { + return + } + log.Error().Msgf("Error reading command: %s", err) + return + } + + cmd := bytes.ToUpper(bytes.TrimSpace(bytes.Split(msg, []byte(" "))[0])) + args := bytes.TrimSpace(bytes.TrimPrefix(msg, cmd)) + + switch string(cmd) { + case "FORWARD-TCP": + proxyAddress := string(bytes.Split(args, []byte(" "))[0]) + destTarget, err := net.Dial("tcp", proxyAddress) + if err != nil { + log.Error().Msgf("Failed to connect to target: %v", err) + return + } + defer destTarget.Close() + log.Info().Msgf("Starting secure transmission between %s->%s", quicConn.LocalAddr().String(), destTarget.LocalAddr().String()) + + // Handle buffered data + buffered := reader.Buffered() + if buffered > 0 { + bufferedData := make([]byte, buffered) + _, err := reader.Read(bufferedData) + if err != nil { + log.Error().Msgf("Error reading buffered data: %v", err) + return + } + + if _, err = destTarget.Write(bufferedData); err != nil { + log.Error().Msgf("Error writing buffered data: %v", err) + return + } + } + + CopyDataFromQuicToTcp(stream, destTarget) + log.Info().Msgf("Ending secure transmission between %s->%s", quicConn.LocalAddr().String(), destTarget.LocalAddr().String()) + return + case "PING": + if _, err := stream.Write([]byte("PONG\n")); err != nil { + log.Error().Msgf("Error writing PONG response: %v", err) + } + return + default: + log.Error().Msgf("Unknown command: %s", string(cmd)) + return + } + } +} + +type CloseWrite interface { + CloseWrite() error +} + +func CopyDataFromQuicToTcp(quicStream quic.Stream, tcpConn net.Conn) { + // Create a WaitGroup to wait for both copy operations + var wg sync.WaitGroup + wg.Add(2) + + // Start copying from QUIC stream to TCP + go func() { + defer wg.Done() + if _, err := io.Copy(tcpConn, quicStream); err != nil { + log.Error().Msgf("Error copying quic->postgres: %v", err) + } + + if e, ok := tcpConn.(CloseWrite); ok { + log.Debug().Msg("Closing TCP write end") + e.CloseWrite() + } else { + log.Debug().Msg("TCP connection does not support CloseWrite") + } + }() + + // Start copying from TCP to QUIC stream + go func() { + defer wg.Done() + if _, err := io.Copy(quicStream, tcpConn); err != nil { + log.Debug().Msgf("Error copying postgres->quic: %v", err) + } + // Close the write side of the QUIC stream + if err := quicStream.Close(); err != nil && !strings.Contains(err.Error(), "close called for canceled stream") { + log.Error().Msgf("Error closing QUIC stream write: %v", err) + } + }() + + // Wait for both copies to complete + wg.Wait() +} diff --git a/cli/packages/gateway/gateway.go b/cli/packages/gateway/gateway.go new file mode 100644 index 000000000..d0a25ca9c --- /dev/null +++ b/cli/packages/gateway/gateway.go @@ -0,0 +1,367 @@ +package gateway + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "net" + "os" + "strings" + "sync" + "time" + + "github.com/Infisical/infisical-merge/packages/api" + "github.com/Infisical/infisical-merge/packages/systemd" + "github.com/Infisical/infisical-merge/packages/util" + "github.com/go-resty/resty/v2" + "github.com/pion/dtls/v3" + "github.com/pion/logging" + "github.com/pion/turn/v4" + "github.com/rs/zerolog/log" + + "github.com/quic-go/quic-go" +) + +type GatewayConfig struct { + TurnServerUsername string + TurnServerPassword string + TurnServerAddress string + InfisicalStaticIp string + SerialNumber string + PrivateKey string + Certificate string + CertificateChain string +} + +type Gateway struct { + httpClient *resty.Client + config *GatewayConfig + client *turn.Client +} + +func NewGateway(identityToken string) (Gateway, error) { + httpClient, err := util.GetRestyClientWithCustomHeaders() + if err != nil { + return Gateway{}, fmt.Errorf("unable to get client with custom headers [err=%v]", err) + } + + httpClient.SetAuthToken(identityToken) + + return Gateway{ + httpClient: httpClient, + config: &GatewayConfig{}, + }, nil +} + +func (g *Gateway) ConnectWithRelay() error { + relayDetails, err := api.CallRegisterGatewayIdentityV1(g.httpClient) + if err != nil { + return err + } + relayAddress, relayPort := strings.Split(relayDetails.TurnServerAddress, ":")[0], strings.Split(relayDetails.TurnServerAddress, ":")[1] + + // Start a new TURN Client and wrap our net.Conn in a STUNConn + // This allows us to simulate datagram based communication over a net.Conn + logger := logging.NewDefaultLoggerFactory() + if os.Getenv("LOG_LEVEL") == "debug" { + logger.DefaultLogLevel = logging.LogLevelDebug + } + + turnClientCfg := &turn.ClientConfig{ + STUNServerAddr: relayDetails.TurnServerAddress, + TURNServerAddr: relayDetails.TurnServerAddress, + Username: relayDetails.TurnServerUsername, + Password: relayDetails.TurnServerPassword, + Realm: relayDetails.TurnServerRealm, + LoggerFactory: logger, + } + + turnAddr, err := net.ResolveUDPAddr("udp4", relayDetails.TurnServerAddress) + if err != nil { + return fmt.Errorf("Failed to parse turn server address: %w", err) + } + + // Dial TURN Server + if relayPort == "5349" { + log.Info().Msgf("Provided relay port %s. Using TLS", relayPort) + conn, err := dtls.Dial("udp", turnAddr, &dtls.Config{ + ServerName: relayAddress, + }) + if err != nil { + return fmt.Errorf("Failed to connect with relay server: %w", err) + } + turnClientCfg.Conn = turn.NewSTUNConn(conn) + } else { + log.Info().Msgf("Provided relay port %s. Using non TLS connection.", relayPort) + conn, err := net.ListenPacket("udp4", "0.0.0.0:0") + if err != nil { + return fmt.Errorf("Failed to connect with relay server: %w", err) + } + + turnClientCfg.Conn = conn + } + + client, err := turn.NewClient(turnClientCfg) + if err != nil { + return fmt.Errorf("Failed to create relay client: %w", err) + } + + g.config = &GatewayConfig{ + TurnServerUsername: relayDetails.TurnServerUsername, + TurnServerPassword: relayDetails.TurnServerPassword, + TurnServerAddress: relayDetails.TurnServerAddress, + InfisicalStaticIp: relayDetails.InfisicalStaticIp, + } + + g.client = client + return nil +} + +func (g *Gateway) Listen(ctx context.Context) error { + defer g.client.Close() + err := g.client.Listen() + if err != nil { + return fmt.Errorf("Failed to listen to relay server: %w", err) + } + + log.Info().Msg("Connected with relay") + + // Allocate a relay socket on the TURN server. On success, it + // will return a net.PacketConn which represents the remote + // socket. + relayUdpConnection, err := g.client.Allocate() + if err != nil { + return fmt.Errorf("Failed to allocate relay connection: %w", err) + } + + log.Info().Msg(relayUdpConnection.LocalAddr().String()) + defer func() { + if closeErr := relayUdpConnection.Close(); closeErr != nil { + log.Error().Msgf("Failed to close connection: %s", closeErr) + } + }() + + gatewayCert, err := api.CallExchangeRelayCertV1(g.httpClient, api.ExchangeRelayCertRequestV1{ + RelayAddress: relayUdpConnection.LocalAddr().String(), + }) + if err != nil { + return err + } + + g.config.SerialNumber = gatewayCert.SerialNumber + g.config.PrivateKey = gatewayCert.PrivateKey + g.config.Certificate = gatewayCert.Certificate + g.config.CertificateChain = gatewayCert.CertificateChain + + errCh := make(chan error, 1) + shutdownCh := make(chan bool, 1) + + if err = g.createPermissionForStaticIps(g.config.InfisicalStaticIp); err != nil { + return err + } + + g.registerHeartBeat(ctx, errCh) + + cert, err := tls.X509KeyPair([]byte(gatewayCert.Certificate), []byte(gatewayCert.PrivateKey)) + if err != nil { + return fmt.Errorf("failed to parse cert: %w", err) + } + + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM([]byte(gatewayCert.CertificateChain)) + + // Setup QUIC server + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + MinVersion: tls.VersionTLS12, + ClientCAs: caCertPool, + ClientAuth: tls.RequireAndVerifyClientCert, + NextProtos: []string{"infisical-gateway"}, + } + // Setup QUIC listener on the relayConn + quicConfig := &quic.Config{ + EnableDatagrams: true, + MaxIdleTimeout: 10 * time.Second, + KeepAlivePeriod: 2 * time.Second, + } + + quicListener, err := quic.Listen(relayUdpConnection, tlsConfig, quicConfig) + if err != nil { + return fmt.Errorf("Failed to listen for QUIC: %w", err) + } + defer quicListener.Close() + + log.Printf("Listener started on %s", quicListener.Addr()) + + g.registerRelayIsActive(ctx, errCh) + + log.Info().Msg("Gateway started successfully") + + var wg sync.WaitGroup + + go func() { + for { + select { + case <-ctx.Done(): + return + case <-shutdownCh: + return + default: + // Accept new relay connection + quicConn, err := quicListener.Accept(context.Background()) + if err != nil { + log.Printf("Failed to accept QUIC connection: %v", err) + continue + } + + tlsState := quicConn.ConnectionState().TLS + if len(tlsState.PeerCertificates) > 0 { + organizationUnit := tlsState.PeerCertificates[0].Subject.OrganizationalUnit + commonName := tlsState.PeerCertificates[0].Subject.CommonName + if organizationUnit[0] != "gateway-client" || commonName != "cloud" { + errMsg := fmt.Sprintf("Client certificate verification failed. Received %s, %s", organizationUnit, commonName) + log.Error().Msg(errMsg) + quicConn.CloseWithError(1, errMsg) + continue + } + } + + // Handle the connection in a goroutine + wg.Add(1) + go func(c quic.Connection) { + defer wg.Done() + defer c.CloseWithError(0, "connection closed") + + // Monitor parent context to close this connection when needed + go func() { + select { + case <-ctx.Done(): + c.CloseWithError(0, "connection closed") // Force close connection when context is canceled + case <-shutdownCh: + c.CloseWithError(0, "connection closed") // Force close connection when accepting loop is done + } + }() + + handleConnection(ctx, c) + }(quicConn) + } + } + }() + + // make this compatiable with systemd notify mode + systemd.SdNotify(false, systemd.SdNotifyReady) + select { + case <-ctx.Done(): + log.Info().Msg("Shutting down gateway...") + case err = <-errCh: + log.Error().Err(err).Msg("Gateway error occurred") + } + + // Signal the accept loop to stop + close(shutdownCh) + + // Set a timeout for waiting on connections to close + waitCh := make(chan struct{}) + go func() { + wg.Wait() + close(waitCh) + }() + + select { + case <-waitCh: + // All connections closed normally + case <-time.After(5 * time.Second): + log.Warn().Msg("Timeout waiting for connections to close gracefully") + } + + return err +} + +func (g *Gateway) registerHeartBeat(ctx context.Context, errCh chan error) { + ticker := time.NewTicker(30 * time.Minute) + defer ticker.Stop() + + go func() { + for { + if err := api.CallGatewayHeartBeatV1(g.httpClient); err != nil { + errCh <- err + } else { + log.Info().Msg("Gateway is reachable by Infisical") + } + + select { + case <-ctx.Done(): + return + case <-ticker.C: + } + } + }() +} + +func (g *Gateway) createPermissionForStaticIps(staticIps string) error { + if staticIps == "" { + return fmt.Errorf("Missing Infisical static ips for permission") + } + + splittedIps := strings.Split(staticIps, ",") + resolvedIps := make([]net.Addr, 0) + for _, ip := range splittedIps { + ip = strings.TrimSpace(ip) + if ip == "" { + continue + } + + // if port not specific allow all port + if !strings.Contains(ip, ":") { + ip = ip + ":0" + } + + peerAddr, err := net.ResolveUDPAddr("udp", ip) + if err != nil { + return fmt.Errorf("Failed to resolve static ip for permission: %w", err) + } + + resolvedIps = append(resolvedIps, peerAddr) + } + + if err := g.client.CreatePermission(resolvedIps...); err != nil { + return fmt.Errorf("Failed to set ip permission: %w", err) + } + return nil +} + +func (g *Gateway) registerRelayIsActive(ctx context.Context, errCh chan error) error { + ticker := time.NewTicker(15 * time.Second) + maxFailures := 3 + failures := 0 + + log.Info().Msg("Starting relay connection health check") + go func() { + time.Sleep(5 * time.Second) + for { + select { + case <-ctx.Done(): + log.Info().Msg("Stopping relay connection health check") + return + case <-ticker.C: + log.Debug().Msg("Performing relay connection health check") + err := g.createPermissionForStaticIps(g.config.InfisicalStaticIp) + // try again error message from server happens to avoid congestion + // https://github.com/pion/turn/blob/master/internal/client/udp_conn.go#L382 + if err != nil && !strings.Contains(err.Error(), "try again") { + failures++ + log.Warn().Err(err).Int("failures", failures).Msg("Failed to refresh TURN permissions") + if failures >= maxFailures { + errCh <- fmt.Errorf("relay connection check failed: %w", err) + return + } + continue + } + failures = 0 // reset + } + } + }() + + return nil +} diff --git a/cli/packages/gateway/relay.go b/cli/packages/gateway/relay.go new file mode 100644 index 000000000..08a5eb247 --- /dev/null +++ b/cli/packages/gateway/relay.go @@ -0,0 +1,188 @@ +//go:build !windows +// +build !windows + +package gateway + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "net" + "os" + "os/signal" + + // "runtime" + "strconv" + "syscall" + + "github.com/Infisical/infisical-merge/packages/systemd" + "github.com/pion/dtls/v3" + "github.com/pion/logging" + "github.com/pion/turn/v4" + "github.com/rs/zerolog/log" + "gopkg.in/yaml.v2" +) + +var ( + errMissingTlsCert = errors.New("Missing TLS files") +) + +type GatewayRelay struct { + Config *GatewayRelayConfig +} + +type GatewayRelayConfig struct { + PublicIP string `yaml:"public_ip"` + Port int `yaml:"port"` + Realm string `yaml:"realm"` + AuthSecret string `yaml:"auth_secret"` + RelayMinPort uint16 `yaml:"relay_min_port"` + RelayMaxPort uint16 `yaml:"relay_max_port"` + TlsCertPath string `yaml:"tls_cert_path"` + TlsPrivateKeyPath string `yaml:"tls_private_key_path"` + TlsCaPath string `yaml:"tls_ca_path"` + + tls tls.Certificate + tlsCa string + isTlsEnabled bool +} + +func NewGatewayRelay(configFilePath string) (*GatewayRelay, error) { + cfgFile, err := os.ReadFile(configFilePath) + if err != nil { + return nil, err + } + var cfg GatewayRelayConfig + if err := yaml.Unmarshal(cfgFile, &cfg); err != nil { + return nil, err + } + + if cfg.PublicIP == "" { + return nil, fmt.Errorf("Missing public ip") + } + + if cfg.AuthSecret == "" { + return nil, fmt.Errorf("Missing auth secret") + } + + if cfg.Realm == "" { + cfg.Realm = "infisical.org" + } + + if cfg.RelayMinPort == 0 { + cfg.RelayMinPort = 49152 + } + + if cfg.RelayMaxPort == 0 { + cfg.RelayMaxPort = 65535 + } + + if cfg.Port == 0 { + cfg.Port = 3478 + } else if cfg.Port == 5349 { + if cfg.TlsCertPath == "" || cfg.TlsPrivateKeyPath == "" { + return nil, errMissingTlsCert + } + + cert, err := tls.LoadX509KeyPair(cfg.TlsCertPath, cfg.TlsPrivateKeyPath) + if err != nil { + return nil, fmt.Errorf("Failed to read load server tls key pair: %w", err) + } + + if cfg.TlsCaPath != "" { + ca, err := os.ReadFile(cfg.TlsCaPath) + if err != nil { + return nil, fmt.Errorf("Failed to read tls ca: %w", err) + } + cfg.tlsCa = string(ca) + } + + cfg.tls = cert + cfg.isTlsEnabled = true + } + + return &GatewayRelay{ + Config: &cfg, + }, nil +} + +func (g *GatewayRelay) Run() error { + addr, err := net.ResolveUDPAddr("udp", "0.0.0.0:"+strconv.Itoa(g.Config.Port)) + if err != nil { + return fmt.Errorf("Failed to parse server address: %s", err) + } + + // NewLongTermAuthHandler takes a pion.LeveledLogger. This allows you to intercept messages + // and process them yourself. + logger := logging.NewDefaultLeveledLoggerForScope("lt-creds", logging.LogLevelTrace, os.Stdout) + + publicIP := g.Config.PublicIP + relayAddressGenerator := &turn.RelayAddressGeneratorPortRange{ + RelayAddress: net.ParseIP(publicIP), // Claim that we are listening on IP passed by user + Address: "0.0.0.0", // But actually be listening on every interface + MinPort: g.Config.RelayMinPort, + MaxPort: g.Config.RelayMaxPort, + } + + loggerF := logging.NewDefaultLoggerFactory() + loggerF.DefaultLogLevel = logging.LogLevelDebug + + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM([]byte(g.Config.tlsCa)) + + listenerConfigs := make([]turn.ListenerConfig, 0) + packetConfigs := make([]turn.PacketConnConfig, 0) + + if g.Config.isTlsEnabled { + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM([]byte(g.Config.tlsCa)) + dtlsServer, err := dtls.Listen("udp", addr, &dtls.Config{ + Certificates: []tls.Certificate{g.Config.tls}, + ClientCAs: caCertPool, + }) + if err != nil { + return fmt.Errorf("Failed to start dtls server: %w", err) + } + listenerConfigs = append(listenerConfigs, turn.ListenerConfig{ + RelayAddressGenerator: relayAddressGenerator, + Listener: dtlsServer, + }) + } else { + udpListener, err := net.ListenPacket("udp4", "0.0.0.0:"+strconv.Itoa(g.Config.Port)) + if err != nil { + return fmt.Errorf("Failed to relay udp listener: %w", err) + } + packetConfigs = append(packetConfigs, turn.PacketConnConfig{ + RelayAddressGenerator: relayAddressGenerator, + PacketConn: udpListener, + }) + } + + server, err := turn.NewServer(turn.ServerConfig{ + Realm: g.Config.Realm, + AuthHandler: turn.LongTermTURNRESTAuthHandler(g.Config.AuthSecret, logger), + // PacketConnConfigs is a list of UDP Listeners and the configuration around them + ListenerConfigs: listenerConfigs, + PacketConnConfigs: packetConfigs, + LoggerFactory: loggerF, + }) + + if err != nil { + return fmt.Errorf("Failed to start server: %w", err) + } + + log.Info().Msgf("Relay listening on %d\n", g.Config.Port) + + // make this compatiable with systemd notify mode + systemd.SdNotify(false, systemd.SdNotifyReady) + // Block until user sends SIGINT or SIGTERM + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + <-sigs + + if err = server.Close(); err != nil { + return fmt.Errorf("Failed to close server: %w", err) + } + return nil +} diff --git a/cli/packages/gateway/relay_windows.go b/cli/packages/gateway/relay_windows.go new file mode 100644 index 000000000..f3bf89bd0 --- /dev/null +++ b/cli/packages/gateway/relay_windows.go @@ -0,0 +1,37 @@ +//go:build windows +// +build windows + +package gateway + +import ( + "errors" +) + +var ( + errMissingTlsCert = errors.New("Missing TLS files") + errWindowsNotSupported = errors.New("Relay is not supported on Windows") +) + +type GatewayRelay struct { + Config *GatewayRelayConfig +} + +type GatewayRelayConfig struct { + PublicIP string + Port int + Realm string + AuthSecret string + RelayMinPort uint16 + RelayMaxPort uint16 + TlsCertPath string + TlsPrivateKeyPath string + TlsCaPath string +} + +func NewGatewayRelay(configFilePath string) (*GatewayRelay, error) { + return nil, errWindowsNotSupported +} + +func (g *GatewayRelay) Run() error { + return errWindowsNotSupported +} diff --git a/cli/packages/gateway/systemd.go b/cli/packages/gateway/systemd.go new file mode 100644 index 000000000..ac6663dff --- /dev/null +++ b/cli/packages/gateway/systemd.go @@ -0,0 +1,121 @@ +package gateway + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + + "github.com/rs/zerolog/log" +) + +const systemdServiceTemplate = `[Unit] +Description=Infisical Gateway Service +After=network.target + +[Service] +Type=notify +NotifyAccess=all +EnvironmentFile=/etc/infisical/gateway.conf +ExecStart=infisical gateway +Restart=on-failure +InaccessibleDirectories=/home +PrivateTmp=yes +LimitCORE=infinity +LimitNOFILE=1000000 +LimitNPROC=60000 +LimitRTPRIO=infinity +LimitRTTIME=7000000 + +[Install] +WantedBy=multi-user.target +` + +func InstallGatewaySystemdService(token string, domain string) error { + if runtime.GOOS != "linux" { + log.Info().Msg("Skipping systemd service installation - not on Linux") + return nil + } + + if os.Geteuid() != 0 { + log.Info().Msg("Skipping systemd service installation - not running as root/sudo") + return nil + } + + configDir := "/etc/infisical" + if err := os.MkdirAll(configDir, 0755); err != nil { + return fmt.Errorf("failed to create config directory: %v", err) + } + + configContent := fmt.Sprintf("INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN=%s\n", token) + if domain != "" { + configContent += fmt.Sprintf("INFISICAL_API_URL=%s\n", domain) + } + + configPath := filepath.Join(configDir, "gateway.conf") + if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil { + return fmt.Errorf("failed to write config file: %v", err) + } + + servicePath := "/etc/systemd/system/infisical-gateway.service" + if err := os.WriteFile(servicePath, []byte(systemdServiceTemplate), 0644); err != nil { + return fmt.Errorf("failed to write systemd service file: %v", err) + } + + reloadCmd := exec.Command("systemctl", "daemon-reload") + if err := reloadCmd.Run(); err != nil { + return fmt.Errorf("failed to reload systemd: %v", err) + } + + log.Info().Msg("Successfully installed systemd service") + log.Info().Msg("To start the service, run: sudo systemctl start infisical-gateway") + log.Info().Msg("To enable the service on boot, run: sudo systemctl enable infisical-gateway") + + return nil +} + +func UninstallGatewaySystemdService() error { + if runtime.GOOS != "linux" { + log.Info().Msg("Skipping systemd service uninstallation - not on Linux") + return nil + } + + if os.Geteuid() != 0 { + log.Info().Msg("Skipping systemd service uninstallation - not running as root/sudo") + return nil + } + + // Stop the service if it's running + stopCmd := exec.Command("systemctl", "stop", "infisical-gateway") + if err := stopCmd.Run(); err != nil { + log.Warn().Msgf("Failed to stop service: %v", err) + } + + // Disable the service + disableCmd := exec.Command("systemctl", "disable", "infisical-gateway") + if err := disableCmd.Run(); err != nil { + log.Warn().Msgf("Failed to disable service: %v", err) + } + + // Remove the service file + servicePath := "/etc/systemd/system/infisical-gateway.service" + if err := os.Remove(servicePath); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to remove systemd service file: %v", err) + } + + // Remove the configuration file + configPath := "/etc/infisical/gateway.conf" + if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to remove config file: %v", err) + } + + // Reload systemd to apply changes + reloadCmd := exec.Command("systemctl", "daemon-reload") + if err := reloadCmd.Run(); err != nil { + return fmt.Errorf("failed to reload systemd: %v", err) + } + + log.Info().Msg("Successfully uninstalled Infisical Gateway systemd service") + return nil +} diff --git a/cli/packages/gateway/udp_listener/listener_unix.go b/cli/packages/gateway/udp_listener/listener_unix.go new file mode 100644 index 000000000..8de2828b4 --- /dev/null +++ b/cli/packages/gateway/udp_listener/listener_unix.go @@ -0,0 +1,26 @@ +//go:build !windows +// +build !windows + +package udplistener + +import ( + "net" + "syscall" + + "golang.org/x/sys/unix" + // other imports +) + +func SetupListenerConfig() *net.ListenConfig { + return &net.ListenConfig{ + Control: func(network, address string, conn syscall.RawConn) error { + var operr error + if err := conn.Control(func(fd uintptr) { + operr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, unix.SO_REUSEPORT, 1) + }); err != nil { + return err + } + return operr + }, + } +} diff --git a/cli/packages/gateway/udp_listener/listener_windows.go b/cli/packages/gateway/udp_listener/listener_windows.go new file mode 100644 index 000000000..4904d12e0 --- /dev/null +++ b/cli/packages/gateway/udp_listener/listener_windows.go @@ -0,0 +1,18 @@ +//go:build windows +// +build windows + +package udplistener + +import ( + "fmt" + "net" + "syscall" +) + +func SetupListenerConfig() *net.ListenConfig { + return &net.ListenConfig{ + Control: func(network, address string, conn syscall.RawConn) error { + return fmt.Errorf("Infisical relay not supported for windows.") + }, + } +} diff --git a/cli/packages/systemd/daemon.go b/cli/packages/systemd/daemon.go new file mode 100644 index 000000000..ce3c97394 --- /dev/null +++ b/cli/packages/systemd/daemon.go @@ -0,0 +1,84 @@ +// Copyright 2014 Docker, Inc. +// Copyright 2015-2018 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// Package daemon provides a Go implementation of the sd_notify protocol. +// It can be used to inform systemd of service start-up completion, watchdog +// events, and other status changes. +// +// https://www.freedesktop.org/software/systemd/man/sd_notify.html#Description +package systemd + +import ( + "net" + "os" +) + +const ( + // SdNotifyReady tells the service manager that service startup is finished + // or the service finished loading its configuration. + SdNotifyReady = "READY=1" + + // SdNotifyStopping tells the service manager that the service is beginning + // its shutdown. + SdNotifyStopping = "STOPPING=1" + + // SdNotifyReloading tells the service manager that this service is + // reloading its configuration. Note that you must call SdNotifyReady when + // it completed reloading. + SdNotifyReloading = "RELOADING=1" + + // SdNotifyWatchdog tells the service manager to update the watchdog + // timestamp for the service. + SdNotifyWatchdog = "WATCHDOG=1" +) + +// SdNotify sends a message to the init daemon. It is common to ignore the error. +// If `unsetEnvironment` is true, the environment variable `NOTIFY_SOCKET` +// will be unconditionally unset. +// +// It returns one of the following: +// (false, nil) - notification not supported (i.e. NOTIFY_SOCKET is unset) +// (false, err) - notification supported, but failure happened (e.g. error connecting to NOTIFY_SOCKET or while sending data) +// (true, nil) - notification supported, data has been sent +func SdNotify(unsetEnvironment bool, state string) (bool, error) { + socketAddr := &net.UnixAddr{ + Name: os.Getenv("NOTIFY_SOCKET"), + Net: "unixgram", + } + + // NOTIFY_SOCKET not set + if socketAddr.Name == "" { + return false, nil + } + + if unsetEnvironment { + if err := os.Unsetenv("NOTIFY_SOCKET"); err != nil { + return false, err + } + } + + conn, err := net.DialUnix(socketAddr.Net, nil, socketAddr) + // Error connecting to NOTIFY_SOCKET + if err != nil { + return false, err + } + defer conn.Close() + + if _, err = conn.Write([]byte(state)); err != nil { + return false, err + } + return true, nil +} diff --git a/cli/packages/util/check-for-update.go b/cli/packages/util/check-for-update.go index f3aca2776..4aae75f65 100644 --- a/cli/packages/util/check-for-update.go +++ b/cli/packages/util/check-for-update.go @@ -53,6 +53,25 @@ func CheckForUpdate() { } } +func DisplayAptInstallationChangeBanner(isSilent bool) { + if isSilent { + return + } + + if runtime.GOOS == "linux" { + _, err := exec.LookPath("apt-get") + isApt := err == nil + if isApt { + yellow := color.New(color.FgYellow).SprintFunc() + msg := fmt.Sprintf("%s", + yellow("Update Required: Your current package installation script is outdated and will no longer receive updates.\nPlease update to the new installation script which can be found here https://infisical.com/docs/cli/overview#installation debian section\n"), + ) + + fmt.Fprintln(os.Stderr, msg) + } + } +} + func getLatestTag(repoOwner string, repoName string) (string, string, error) { url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", repoOwner, repoName) resp, err := http.Get(url) diff --git a/cli/packages/util/common.go b/cli/packages/util/common.go index 55907da9d..07618ae87 100644 --- a/cli/packages/util/common.go +++ b/cli/packages/util/common.go @@ -4,8 +4,11 @@ import ( "fmt" "net/http" "os" + "strings" + "unicode" "github.com/Infisical/infisical-merge/packages/config" + "github.com/go-resty/resty/v2" ) func GetHomeDir() (string, error) { @@ -27,3 +30,88 @@ func ValidateInfisicalAPIConnection() (ok bool) { _, err := http.Get(fmt.Sprintf("%v/status", config.INFISICAL_URL)) return err == nil } + +func GetRestyClientWithCustomHeaders() (*resty.Client, error) { + httpClient := resty.New() + customHeaders := os.Getenv("INFISICAL_CUSTOM_HEADERS") + if customHeaders != "" { + headers, err := GetInfisicalCustomHeadersMap() + if err != nil { + return nil, err + } + + httpClient.SetHeaders(headers) + } + return httpClient, nil +} + +func GetInfisicalCustomHeadersMap() (map[string]string, error) { + customHeaders := os.Getenv("INFISICAL_CUSTOM_HEADERS") + if customHeaders == "" { + return nil, nil + } + + headers := map[string]string{} + + pos := 0 + for pos < len(customHeaders) { + for pos < len(customHeaders) && unicode.IsSpace(rune(customHeaders[pos])) { + pos++ + } + + if pos >= len(customHeaders) { + break + } + + keyStart := pos + for pos < len(customHeaders) && customHeaders[pos] != '=' && !unicode.IsSpace(rune(customHeaders[pos])) { + pos++ + } + + if pos >= len(customHeaders) || customHeaders[pos] != '=' { + return nil, fmt.Errorf("invalid custom header format. Expected \"headerKey1=value1 headerKey2=value2 ....\" but got %v", customHeaders) + } + + key := customHeaders[keyStart:pos] + pos++ + + for pos < len(customHeaders) && unicode.IsSpace(rune(customHeaders[pos])) { + pos++ + } + + var value string + + if pos < len(customHeaders) { + if customHeaders[pos] == '"' || customHeaders[pos] == '\'' { + quoteChar := customHeaders[pos] + pos++ + valueStart := pos + + for pos < len(customHeaders) && + (customHeaders[pos] != quoteChar || + (pos > 0 && customHeaders[pos-1] == '\\')) { + pos++ + } + + if pos < len(customHeaders) { + value = customHeaders[valueStart:pos] + pos++ + } else { + value = customHeaders[valueStart:] + } + } else { + valueStart := pos + for pos < len(customHeaders) && !unicode.IsSpace(rune(customHeaders[pos])) { + pos++ + } + value = customHeaders[valueStart:pos] + } + } + + if key != "" && !strings.EqualFold(key, "User-Agent") && !strings.EqualFold(key, "Accept") { + headers[key] = value + } + } + + return headers, nil +} diff --git a/cli/packages/util/config.go b/cli/packages/util/config.go index 02030e1fa..8d44c84d1 100644 --- a/cli/packages/util/config.go +++ b/cli/packages/util/config.go @@ -56,6 +56,7 @@ func WriteInitalConfig(userCredentials *models.UserCredentials) error { LoggedInUsers: existingConfigFile.LoggedInUsers, VaultBackendType: existingConfigFile.VaultBackendType, VaultBackendPassphrase: existingConfigFile.VaultBackendPassphrase, + Domains: existingConfigFile.Domains, } configFileMarshalled, err := json.Marshal(configFile) diff --git a/cli/packages/util/credentials.go b/cli/packages/util/credentials.go index cb5b94080..cd73e47ca 100644 --- a/cli/packages/util/credentials.go +++ b/cli/packages/util/credentials.go @@ -9,7 +9,6 @@ import ( "github.com/Infisical/infisical-merge/packages/api" "github.com/Infisical/infisical-merge/packages/config" "github.com/Infisical/infisical-merge/packages/models" - "github.com/go-resty/resty/v2" "github.com/zalando/go-keyring" ) @@ -55,7 +54,7 @@ func GetUserCredsFromKeyRing(userEmail string) (credentials models.UserCredentia return userCredentials, err } -func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) { +func GetCurrentLoggedInUserDetails(setConfigVariables bool) (LoggedInUserDetails, error) { if ConfigFileExists() { configFile, err := GetConfigFile() if err != nil { @@ -75,18 +74,25 @@ func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) { } } + if setConfigVariables { + config.INFISICAL_URL_MANUAL_OVERRIDE = config.INFISICAL_URL + //configFile.LoggedInUserDomain + //if not empty set as infisical url + if configFile.LoggedInUserDomain != "" { + config.INFISICAL_URL = AppendAPIEndpoint(configFile.LoggedInUserDomain) + } + } + // check to to see if the JWT is still valid - httpClient := resty.New(). + httpClient, err := GetRestyClientWithCustomHeaders() + if err != nil { + return LoggedInUserDetails{}, fmt.Errorf("getCurrentLoggedInUserDetails: unable to get client with custom headers [err=%s]", err) + } + + httpClient. SetAuthToken(userCreds.JTWToken). SetHeader("Accept", "application/json") - config.INFISICAL_URL_MANUAL_OVERRIDE = config.INFISICAL_URL - //configFile.LoggedInUserDomain - //if not empty set as infisical url - if configFile.LoggedInUserDomain != "" { - config.INFISICAL_URL = AppendAPIEndpoint(configFile.LoggedInUserDomain) - } - isAuthenticated := api.CallIsAuthenticated(httpClient) // TODO: add refresh token // if !isAuthenticated { diff --git a/cli/packages/util/folders.go b/cli/packages/util/folders.go index c7f6de630..6bba05842 100644 --- a/cli/packages/util/folders.go +++ b/cli/packages/util/folders.go @@ -6,7 +6,6 @@ import ( "github.com/Infisical/infisical-merge/packages/api" "github.com/Infisical/infisical-merge/packages/models" - "github.com/go-resty/resty/v2" "github.com/rs/zerolog/log" ) @@ -20,7 +19,7 @@ func GetAllFolders(params models.GetAllFoldersParameters) ([]models.SingleFolder log.Debug().Msg("GetAllFolders: Trying to fetch folders using logged in details") - loggedInUserDetails, err := GetCurrentLoggedInUserDetails() + loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true) if err != nil { return nil, err } @@ -65,7 +64,11 @@ func GetAllFolders(params models.GetAllFoldersParameters) ([]models.SingleFolder func GetFoldersViaJTW(JTWToken string, workspaceId string, environmentName string, foldersPath string) ([]models.SingleFolder, error) { // set up resty client - httpClient := resty.New() + httpClient, err := GetRestyClientWithCustomHeaders() + if err != nil { + return nil, err + } + httpClient.SetAuthToken(JTWToken). SetHeader("Accept", "application/json") @@ -100,7 +103,10 @@ func GetFoldersViaServiceToken(fullServiceToken string, workspaceId string, envi serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2]) - httpClient := resty.New() + httpClient, err := GetRestyClientWithCustomHeaders() + if err != nil { + return nil, fmt.Errorf("unable to get client with custom headers [err=%v]", err) + } httpClient.SetAuthToken(serviceToken). SetHeader("Accept", "application/json") @@ -143,7 +149,11 @@ func GetFoldersViaServiceToken(fullServiceToken string, workspaceId string, envi } func GetFoldersViaMachineIdentity(accessToken string, workspaceId string, envSlug string, foldersPath string) ([]models.SingleFolder, error) { - httpClient := resty.New() + httpClient, err := GetRestyClientWithCustomHeaders() + if err != nil { + return nil, err + } + httpClient.SetAuthToken(accessToken). SetHeader("Accept", "application/json") @@ -177,7 +187,7 @@ func CreateFolder(params models.CreateFolderParameters) (models.SingleFolder, er if params.InfisicalToken == "" { RequireLogin() RequireLocalWorkspaceFile() - loggedInUserDetails, err := GetCurrentLoggedInUserDetails() + loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true) if err != nil { return models.SingleFolder{}, err @@ -191,9 +201,12 @@ func CreateFolder(params models.CreateFolderParameters) (models.SingleFolder, er } // set up resty client - httpClient := resty.New() - httpClient. - SetAuthToken(params.InfisicalToken). + httpClient, err := GetRestyClientWithCustomHeaders() + if err != nil { + return models.SingleFolder{}, err + } + + httpClient.SetAuthToken(params.InfisicalToken). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json") @@ -224,7 +237,7 @@ func DeleteFolder(params models.DeleteFolderParameters) ([]models.SingleFolder, RequireLogin() RequireLocalWorkspaceFile() - loggedInUserDetails, err := GetCurrentLoggedInUserDetails() + loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true) if err != nil { return nil, err @@ -238,9 +251,12 @@ func DeleteFolder(params models.DeleteFolderParameters) ([]models.SingleFolder, } // set up resty client - httpClient := resty.New() - httpClient. - SetAuthToken(params.InfisicalToken). + httpClient, err := GetRestyClientWithCustomHeaders() + if err != nil { + return nil, err + } + + httpClient.SetAuthToken(params.InfisicalToken). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json") diff --git a/cli/packages/util/helper.go b/cli/packages/util/helper.go index 11a1e3e0a..346122a64 100644 --- a/cli/packages/util/helper.go +++ b/cli/packages/util/helper.go @@ -16,7 +16,6 @@ import ( "github.com/Infisical/infisical-merge/packages/api" "github.com/Infisical/infisical-merge/packages/models" - "github.com/go-resty/resty/v2" "github.com/spf13/cobra" ) @@ -120,7 +119,11 @@ func GetInfisicalToken(cmd *cobra.Command) (token *models.TokenDetails, err erro } func UniversalAuthLogin(clientId string, clientSecret string) (api.UniversalAuthLoginResponse, error) { - httpClient := resty.New() + httpClient, err := GetRestyClientWithCustomHeaders() + if err != nil { + return api.UniversalAuthLoginResponse{}, err + } + httpClient.SetRetryCount(10000). SetRetryMaxWaitTime(20 * time.Second). SetRetryWaitTime(5 * time.Second) @@ -135,7 +138,11 @@ func UniversalAuthLogin(clientId string, clientSecret string) (api.UniversalAuth func RenewMachineIdentityAccessToken(accessToken string) (string, error) { - httpClient := resty.New() + httpClient, err := GetRestyClientWithCustomHeaders() + if err != nil { + return "", err + } + httpClient.SetRetryCount(10000). SetRetryMaxWaitTime(20 * time.Second). SetRetryWaitTime(5 * time.Second) @@ -245,8 +252,9 @@ func getCurrentBranch() (string, error) { } func AppendAPIEndpoint(address string) string { + // if it's empty return as it is // Ensure the address does not already end with "/api" - if strings.HasSuffix(address, "/api") { + if address == "" || strings.HasSuffix(address, "/api") { return address } diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index 5e19ea664..0693db509 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -14,9 +14,9 @@ import ( "github.com/Infisical/infisical-merge/packages/api" "github.com/Infisical/infisical-merge/packages/crypto" "github.com/Infisical/infisical-merge/packages/models" - "github.com/go-resty/resty/v2" "github.com/rs/zerolog/log" "github.com/zalando/go-keyring" + "gopkg.in/yaml.v3" ) func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool, tagSlugs string, expandSecretReferences bool) ([]models.SingleEnvironmentVariable, error) { @@ -27,7 +27,10 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2]) - httpClient := resty.New() + httpClient, err := GetRestyClientWithCustomHeaders() + if err != nil { + return nil, fmt.Errorf("unable to get client with custom headers [err=%v]", err) + } httpClient.SetAuthToken(serviceToken). SetHeader("Accept", "application/json") @@ -78,7 +81,11 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str } func GetPlainTextSecretsV3(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool, recursive bool, tagSlugs string, expandSecretReferences bool) (models.PlaintextSecretResult, error) { - httpClient := resty.New() + httpClient, err := GetRestyClientWithCustomHeaders() + if err != nil { + return models.PlaintextSecretResult{}, err + } + httpClient.SetAuthToken(accessToken). SetHeader("Accept", "application/json") @@ -121,7 +128,11 @@ func GetPlainTextSecretsV3(accessToken string, workspaceId string, environmentNa } func GetSinglePlainTextSecretByNameV3(accessToken string, workspaceId string, environmentName string, secretsPath string, secretName string) (models.SingleEnvironmentVariable, string, error) { - httpClient := resty.New() + httpClient, err := GetRestyClientWithCustomHeaders() + if err != nil { + return models.SingleEnvironmentVariable{}, "", err + } + httpClient.SetAuthToken(accessToken). SetHeader("Accept", "application/json") @@ -152,7 +163,11 @@ func GetSinglePlainTextSecretByNameV3(accessToken string, workspaceId string, en } func CreateDynamicSecretLease(accessToken string, projectSlug string, environmentName string, secretsPath string, slug string, ttl string) (models.DynamicSecretLease, error) { - httpClient := resty.New() + httpClient, err := GetRestyClientWithCustomHeaders() + if err != nil { + return models.DynamicSecretLease{}, err + } + httpClient.SetAuthToken(accessToken). SetHeader("Accept", "application/json") @@ -246,7 +261,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo log.Debug().Msg("GetAllEnvironmentVariables: Trying to fetch secrets using logged in details") - loggedInUserDetails, err := GetCurrentLoggedInUserDetails() + loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true) isConnected := ValidateInfisicalAPIConnection() if isConnected { @@ -524,7 +539,11 @@ func GetEnvelopmentBasedOnGitBranch(workspaceFile models.WorkspaceConfigFile) st } func GetPlainTextWorkspaceKey(authenticationToken string, receiverPrivateKey string, workspaceId string) ([]byte, error) { - httpClient := resty.New() + httpClient, err := GetRestyClientWithCustomHeaders() + if err != nil { + return nil, fmt.Errorf("GetPlainTextWorkspaceKey: unable to get client with custom headers [err=%v]", err) + } + httpClient.SetAuthToken(authenticationToken). SetHeader("Accept", "application/json") @@ -564,7 +583,99 @@ func GetPlainTextWorkspaceKey(authenticationToken string, receiverPrivateKey str return crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey), nil } -func SetRawSecrets(secretArgs []string, secretType string, environmentName string, secretsPath string, projectId string, tokenDetails *models.TokenDetails) ([]models.SecretSetOperation, error) { +func parseSecrets(fileName string, content string) (map[string]string, error) { + secrets := make(map[string]string) + + if strings.HasSuffix(fileName, ".yaml") || strings.HasSuffix(fileName, ".yml") { + // Handle YAML secrets + var yamlData map[string]interface{} + if err := yaml.Unmarshal([]byte(content), &yamlData); err != nil { + return nil, fmt.Errorf("failed to parse YAML file: %v", err) + } + + for key, value := range yamlData { + if strValue, ok := value.(string); ok { + secrets[key] = strValue + } else { + return nil, fmt.Errorf("YAML secret '%s' must be a string", key) + } + } + } else { + // Handle .env files + lines := strings.Split(content, "\n") + + for _, line := range lines { + line = strings.TrimSpace(line) + + // Ignore empty lines and comments + if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") { + continue + } + + // Ensure it's a valid key=value pair + splitKeyValue := strings.SplitN(line, "=", 2) + if len(splitKeyValue) != 2 { + return nil, fmt.Errorf("invalid format, expected key=value in line: %s", line) + } + + key, value := strings.TrimSpace(splitKeyValue[0]), strings.TrimSpace(splitKeyValue[1]) + + // Handle quoted values + if (strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`)) || + (strings.HasPrefix(value, `'`) && strings.HasSuffix(value, `'`)) { + value = value[1 : len(value)-1] // Remove surrounding quotes + } + + secrets[key] = value + } + } + + return secrets, nil +} + +func validateSecretKey(key string) error { + if key == "" { + return errors.New("secret keys cannot be empty") + } + if unicode.IsNumber(rune(key[0])) { + return fmt.Errorf("secret key '%s' cannot start with a number", key) + } + if strings.Contains(key, " ") { + return fmt.Errorf("secret key '%s' cannot contain spaces", key) + } + return nil +} + +func SetRawSecrets(secretArgs []string, secretType string, environmentName string, secretsPath string, projectId string, tokenDetails *models.TokenDetails, file string) ([]models.SecretSetOperation, error) { + if file != "" { + content, err := os.ReadFile(file) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + PrintErrorMessageAndExit("File does not exist") + } + return nil, fmt.Errorf("unable to process file [err=%v]", err) + } + + parsedSecrets, err := parseSecrets(file, string(content)) + if err != nil { + PrintErrorMessageAndExit(fmt.Sprintf("error parsing secrets: %v", err)) + } + + // Step 2: Validate secrets + for key, value := range parsedSecrets { + if err := validateSecretKey(key); err != nil { + PrintErrorMessageAndExit(err.Error()) + } + if strings.TrimSpace(value) == "" { + PrintErrorMessageAndExit(fmt.Sprintf("Secret key '%s' has an empty value", key)) + } + secretArgs = append(secretArgs, fmt.Sprintf("%s=%s", key, value)) + } + + if len(secretArgs) == 0 { + PrintErrorMessageAndExit("no valid secrets found in the file") + } + } if tokenDetails == nil { return nil, fmt.Errorf("unable to process set secret operations, token details are missing") @@ -579,9 +690,12 @@ func SetRawSecrets(secretArgs []string, secretType string, environmentName strin getAllEnvironmentVariablesRequest.InfisicalToken = tokenDetails.Token } - httpClient := resty.New(). - SetAuthToken(tokenDetails.Token). - SetHeader("Accept", "application/json") + httpClient, err := GetRestyClientWithCustomHeaders() + if err != nil { + return nil, fmt.Errorf("unable to get client with custom headers [err=%v]", err) + } + httpClient.SetAuthToken(tokenDetails.Token) + httpClient.SetHeader("Accept", "application/json") // pull current secrets secrets, err := GetAllEnvironmentVariables(getAllEnvironmentVariablesRequest, "") diff --git a/cli/packages/visualize/dynamic_secret_leases.go b/cli/packages/visualize/dynamic_secret_leases.go new file mode 100644 index 000000000..dbb588624 --- /dev/null +++ b/cli/packages/visualize/dynamic_secret_leases.go @@ -0,0 +1,39 @@ +package visualize + +import infisicalModels "github.com/infisical/go-sdk/packages/models" + +func PrintAllDyamicSecretLeaseCredentials(leaseCredentials map[string]any) { + rows := [][]string{} + for key, value := range leaseCredentials { + if cred, ok := value.(string); ok { + rows = append(rows, []string{key, cred}) + } + } + + headers := []string{"Key", "Value"} + + GenericTable(headers, rows) +} + +func PrintAllDynamicRootCredentials(dynamicRootCredentials []infisicalModels.DynamicSecret) { + rows := [][]string{} + for _, el := range dynamicRootCredentials { + rows = append(rows, []string{el.Name, el.Type, el.DefaultTTL, el.MaxTTL}) + } + + headers := []string{"Name", "Provider", "Default TTL", "Max TTL"} + + GenericTable(headers, rows) +} + +func PrintAllDynamicSecretLeases(dynamicSecretLeases []infisicalModels.DynamicSecretLease) { + rows := [][]string{} + const timeformat = "02-Jan-2006 03:04:05 PM" + for _, el := range dynamicSecretLeases { + rows = append(rows, []string{el.Id, el.ExpireAt.Local().Format(timeformat), el.CreatedAt.Local().Format(timeformat)}) + } + + headers := []string{"ID", "Expire At", "Created At"} + + GenericTable(headers, rows) +} diff --git a/cli/packages/visualize/visualize.go b/cli/packages/visualize/visualize.go index 365cbeac4..7fbd24fb8 100644 --- a/cli/packages/visualize/visualize.go +++ b/cli/packages/visualize/visualize.go @@ -94,6 +94,33 @@ func getLongestValues(rows [][3]string) (longestSecretName, longestSecretType in return } +func GenericTable(headers []string, rows [][]string) { + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.SetStyle(table.StyleLight) + + // t.SetTitle(tableOptions.Title) + t.Style().Options.DrawBorder = true + t.Style().Options.SeparateHeader = true + t.Style().Options.SeparateColumns = true + + tableHeaders := table.Row{} + for _, header := range headers { + tableHeaders = append(tableHeaders, header) + } + + t.AppendHeader(tableHeaders) + for _, row := range rows { + tableRow := table.Row{} + for _, val := range row { + tableRow = append(tableRow, val) + } + t.AppendRow(tableRow) + } + + t.Render() +} + // stringWidth returns the width of a string. // ANSI escape sequences are ignored and double-width characters are handled correctly. func stringWidth(str string) (width int) { diff --git a/cli/scripts/setup.deb.sh b/cli/scripts/setup.deb.sh new file mode 100644 index 000000000..ef24bcadc --- /dev/null +++ b/cli/scripts/setup.deb.sh @@ -0,0 +1,551 @@ +#!/usr/bin/env bash +# +# The core commands execute start from the "MAIN" section below. +# + +test -z "$BASH_SOURCE" && { + self="sudo -E bash" + prefix=" |" +} || { + self=$(readlink -f ${BASH_SOURCE:-$0}) + prefix="" +} + +tmp_log=$(mktemp .s3_setup_XXXXXXXXX) + +# Environment variables that can be set +PKG_URL=${PKG_URL:-"https://artifacts-cli.infisical.com"} +PKG_PATH=${PKG_PATH:-"deb"} +PACKAGE_NAME=${PACKAGE_NAME:-"infisical"} +GPG_KEY_URL=${GPG_KEY_URL:-"${PKG_URL}/infisical.gpg"} + +colours=$(tput colors 2>/dev/null || echo "256") +no_colour="\e[39;49m" +green_colour="\e[32m" +red_colour="\e[41;97m" +bold="\e[1m" +reset="\e[0m" +use_colours=$(test -n "$colours" && test $colours -ge 8 && echo "yes") +test "$use_colours" == "yes" || { + no_colour="" + green_colour="" + red_colour="" + bold="" + reset="" +} + +example_name="Ubuntu/Focal (20.04)" +example_distro="ubuntu" +example_codename="focal" +example_version="20.04" + +function echo_helptext { + local help_text="$*" + echo " ^^^^: ... $help_text" +} + +function die { + local text="$@" + test ! -z "$text" && { + echo_helptext "$text" 1>&2 + } + + local prefix="${red_colour} !!!!${no_colour}" + + echo -e "$prefix: Oh no, your setup failed! :-( ... But we might be able to help. :-)" + echo -e "$prefix: " + echo -e "$prefix: ${bold}Please check your S3 bucket configuration and try again.${reset}" + echo -e "$prefix: " + + test -f "$tmp_log" && { + local n=20 + echo -e "$prefix: Last $n log lines from $tmp_log (might not be errors, nor even relevant):" + echo -e "$prefix:" + check_tool_silent "xargs" && { + check_tool_silent "fmt" && { + tail -n $n $tmp_log | fmt -t | xargs -Ilog echo -e "$prefix: > log" + } || { + tail -n $n $tmp_log | xargs -Ilog echo -e "$prefix: > log" + } + } || { + echo + tail -n $n $tmp_log + } + } + exit 1 +} + +function echo_colour { + local colour="${1:-"no"}_colour"; shift + echo -e "${!colour}$@${no_colour}" +} + +function echo_green_or_red { + local rc="$1" + local good="${2:-YES}" + local bad="${3:-NO}" + + test "$rc" -eq 0 && { + echo_colour "green" "$good" + } || { + echo_colour "red" "$bad" + } + return $rc +} + +function echo_clearline { + local rc="$?" + echo -e -n "\033[1K\r" + return $rc +} + +function echo_status { + local rc="$1" + local good="$2" + local bad="$3" + local text="$4" + local help_text="$5" + local newline=$(test "$6" != "no" && echo "\n" || echo "") + local status_text=$(echo_green_or_red "$rc" "$good" "$bad") + + echo_clearline + local width=$(test "$use_colours" == "yes" && echo "16" || echo "5") + printf "%${width}s %s${newline}" "${status_text}:" "$text" + test $rc -ne 0 && test ! -z "$help_text" && { + echo_helptext "$help_text" + echo + } + + return $rc +} + +function echo_running { + local rc=$? + local text="$1" + echo_status 0 " RUN" " RUN" "$text" "" "no" + return $rc +} + +function echo_okfail_rc { + local rc=$1 + local text="$2" + local help_text="$3" + echo_clearline + echo_status $rc " OK" " NOPE" "$text" "$help_text" + return $rc +} + +function echo_okfail { + echo_okfail_rc $? "$@" + return $? +} + +function check_tool_silent { + local tool=${1} + command -v $tool &>/dev/null || which $tool &>/dev/null + return $? +} + +function check_tool { + local tool=${1} + local optional=${2:-false} + local required_text="optional" + if ! $optional; then required_text="required"; fi + local text="Checking for $required_text executable '$tool' ..." + echo_running "$text" + check_tool_silent "$tool" + echo_okfail "$text" || { + if ! $optional; then + die "$tool is not installed, but is required by this script." + fi + return 1 + } + return 0 +} + +function cleanup { + echo + rm -rf $tmp_log +} + +function shutdown { + echo_colour "red" " !!!!: Operation cancelled by user!" + exit 2 +} + +function check_os { + test ! -z "$distro" && test ! -z "${version}${codename}" + return $? +} + +function detect_os_system { + check_os && return 0 + echo_running "$text" + local text="Detecting your OS distribution and release using system methods ..." + + local tool_rc=1 + test -f '/etc/os-release' && { + . /etc/os-release + distro=${distro:-$ID} + codename=${codename:-$VERSION_CODENAME} + codename=${codename:-$(echo $VERSION | cut -d '(' -f 2 | cut -d ')' -f 1)} + version=${version:-$VERSION_ID} + + test -z "${version}${codename}" && test -f '/etc/debian_version' && { + # Workaround for Debian unstable releases; get the codename from debian_version + codename=$(cat /etc/debian_version | cut -d '/' -f1) + } + + tool_rc=0 + } + + check_os + local rc=$? + echo_okfail_rc $rc "$text" + + test $tool_rc -eq 0 && { + report_os_expanded + } + + return $rc +} + +function report_os_attribute { + local name=$1 + local value=$2 + local coloured="" + echo -n "$name=" + test -z "$value" && { + echo -e -n "${red_colour}${no_colour} " + } || { + echo -e -n "${green_colour}${value}${no_colour} " + } +} + +function report_os_expanded { + echo_helptext "Detected/provided for your OS/distribution, version and architecture:" + echo " >>>>:" + report_os_values +} + +function report_os_values { + echo -n " >>>>: ... " + report_os_attribute "distro" $distro + report_os_attribute "codename" "stable (fixed)" + report_os_attribute "arch" $arch + echo + echo " >>>>:" +} + +function detect_os_legacy_python { + check_os && return 0 + + local text="Detecting your OS distribution and release using legacy python ..." + echo_running "$text" + + IFS='' read -r -d '' script <<-'EOF' +from __future__ import unicode_literals, print_function +import platform; +info = platform.linux_distribution() or ('', '', ''); +for key, value in zip(('distro', 'version', 'codename'), info): + print("local guess_%s=\"%s\"\n" % (key, value.lower().replace(' ', ''))); +EOF + + local tool_rc=1 + check_tool_silent "python" && { + eval $(python -c "$script") + distro=${distro:-$guess_distro} + codename=${codename:-$guess_codename} + version=${version:-$guess_version} + tool_rc=$? + } + + check_os + local rc=$? + echo_okfail_rc $rc "$text" + + check_tool_silent "python" || { + echo_helptext "Python isn't available, so skipping detection method (hint: install python)" + } + + test $tool_rc -eq 0 && { + report_os + } + + return $rc +} + +function detect_os_modern_python { + check_os && return 0 + + check_tool_silent "python" && { + local text="Ensuring python-pip is installed ..." + echo_running "$text" + check_tool_silent "pip" + echo_okfail "$text" || { + local text="Checking if pip can be bootstrapped without get-pip ..." + echo_running "$text" + python -m ensurepip --default-pip &>$tmp_log + echo_okfail "$text" || { + local text="Installing pip via get-pip bootstrap ..." + echo_running "$text" + curl -1sLf https://bootstrap.pypa.io/get-pip.py 2>$tmp/log | python &>$tmp_log + echo_okfail "$text" || die "Failed to install pip!" + } + } + + local text="Installing 'distro' python library ..." + echo_running "$text" + python -c 'import distro' &>$tmp_log || python -m pip install distro &>$tmp_log + echo_okfail "$text" || die "Failed to install required 'distro' python library!" + } + + IFS='' read -r -d '' script <<-'EOF' +from __future__ import unicode_literals, print_function +import distro; +info = distro.linux_distribution(full_distribution_name=False) or ('', '', ''); +for key, value in zip(('distro', 'version', 'codename'), info): + print("local guess_%s=\"%s\"\n" % (key, value.lower().replace(' ', ''))); +EOF + + local text="Detecting your OS distribution and release using modern python ..." + echo_running "$text" + + local tool_rc=1 + check_tool_silent "python" && { + eval $(python -c "$script") + distro=${distro:-$guess_distro} + codename=${codename:-$guess_codename} + version=${version:-$guess_version} + tool_rc=$? + } + + check_os + local rc=$? + echo_okfail_rc $rc "$text" + + check_tool_silent "python" || { + echo_helptext "Python isn't available, so skipping detection method (hint: install python)" + } + + test $tool_rc -eq 0 && { + report_os_expanded + } + + return $rc +} + +function detect_os { + # Backwards compat for old distribution parameter names + distro=${distro:-$os} + + # Always use "stable" as the codename + codename="stable" + + arch=${arch:-$(arch || uname -m)} + + # Only detect OS if not manually specified + if [ -z "$distro" ]; then + detect_os_system || + detect_os_legacy_python || + detect_os_modern_python + fi + + # Always ensure we have a distro + (test -z "$distro") && { + echo_okfail_rc "1" "Unable to detect your OS distribution!" + cat <>>>: + >>>>: The 'distro' value is required. Without it, the install script + >>>>: cannot retrieve the correct configuration for this system. + >>>>: + >>>>: You can force this script to use a particular value by specifying distro + >>>>: via environment variable. E.g., to specify a distro + >>>>: such as $example_name, use the following: + >>>>: + >>>>: $prefix distro=$example_distro $self + >>>>: +EOF + die + } +} + +function create_repo_config { + if [ -z "$PKG_PATH" ]; then + repo_url="${PKG_URL}" + else + repo_url="${PKG_URL}/${PKG_PATH}" + fi + + # Create configuration with GPG key verification + local gpg_keyring_path="/usr/share/keyrings/${PACKAGE_NAME}-archive-keyring.gpg" + local apt_conf=$(cat <>>>: + >>>>: It looks like we can't access the GPG key at ${GPG_KEY_URL} + >>>>: +EOF + die + } +} + +function check_dpkg_tool { + local tool=${1} + local required=${2:-true} + local install=${3:-true} + + local text="Checking for apt dependency '$tool' ..." + echo_running "$text" + dpkg -l | grep "$tool\>" &>$tmp_log + echo_okfail "$text" || { + if $install; then + test "$apt_updated" == "yes" || update_apt + local text="Attempting to install '$tool' ..." + echo_running "$text" + apt-get install -y "$tool" &>$tmp_log + echo_okfail "$text" || { + if $required; then + die "Could not install '$tool', check your permissions, etc." + fi + } + else { + if $required; then + die "$tool is not installed, but is required by this script." + fi + } + fi + } + return 0 +} + +function update_apt { + local text="Updating apt repository metadata cache ..." + local tmp_log=$(mktemp .s3_deb_output_XXXXXXXXX.log) + echo_running "$text" + apt-get update &>$tmp_log + echo_okfail "$text" || { + echo_colour "red" "Failed to update via apt-get update" + cat $tmp_log + rm -rf $tmp_log + die "Failed to update via apt-get update - Context above (maybe no packages?)." + } + rm -rf $tmp_log + apt_updated="yes" +} + +function install_apt_prereqs { + # Debian-archive-keyring has to be installed for apt-transport-https. + test "${distro}" == "debian" && { + check_dpkg_tool "debian-keyring" + check_dpkg_tool "debian-archive-keyring" + } + + check_dpkg_tool "apt-transport-https" + check_dpkg_tool "ca-certificates" false + check_dpkg_tool "gnupg" +} + +function import_gpg_key { + local text="Importing '$PACKAGE_NAME' repository GPG key from S3 ..." + echo_running "$text" + + local gpg_keyring_path="/usr/share/keyrings/${PACKAGE_NAME}-archive-keyring.gpg" + + # Check if GPG key is accessible + check_gpg_key + + # Download and import GPG key + curl -1sLf "${GPG_KEY_URL}" | gpg --dearmor > $gpg_keyring_path + chmod 644 $gpg_keyring_path + + # Check for older apt versions that don't support signed-by + local signed_by_version="1.1" + local detected_version=$(dpkg -s apt | grep Version | cut -d' ' -f2) + + if [ "$(printf "%s\n" $detected_version $signed_by_version | sort -V | head -n 1)" != "$signed_by_version" ]; then + echo_helptext "Detected older apt version without signed-by support. Copying key to trusted.gpg.d." + cp ${gpg_keyring_path} /etc/apt/trusted.gpg.d/${PACKAGE_NAME}.gpg + chmod 644 /etc/apt/trusted.gpg.d/${PACKAGE_NAME}.gpg + fi + + echo_okfail "$text" || die "Could not import the GPG key for this repository" +} + +function setup_repository { + local repo_path="/etc/apt/sources.list.d/${PACKAGE_NAME}.list" + + local text="Installing '$PACKAGE_NAME' repository via apt ..." + echo_running "$text" + create_repo_config > "$repo_path" + chmod 644 $repo_path + echo_okfail "$text" || die "Could not install the repository, do you have permissions?" +} + +function usage () { + cat <]
+ - Action items: + - + +Notable support: +- [Customer company name]
+ - Action items: + - + - + +Comments: + +``` diff --git a/company/documentation/engineering/oncall.mdx b/company/documentation/engineering/oncall.mdx new file mode 100644 index 000000000..bab641845 --- /dev/null +++ b/company/documentation/engineering/oncall.mdx @@ -0,0 +1,78 @@ +--- +title: "On call rotation" +sidebarTitle: "On call rotation" +description: "Learn about call rotation at Infisical" +--- + +Infisical is mission-critical software, which means minimizing service disruptions is a top priority. +To make sure we can react to any issues that come up, we have an on-call rotation that helps us to provide responsive, 24x7x365 support to our customers. +Being part of the on-call rotation is an opportunity to deepen the understanding of our infrastructure, deployment pipelines, and customer-facing systems. +Having this broader understanding of our system not only helps us design better software but also enhances the overall stability of our platform. + +### On-Call Overview + +**Rotation Details** + +Each engineer will be on call once a week, from **Thursday to Thursday**, including weekends. +During this time, the on-call engineer is expected to be available at all times to respond to service disruption alerts. + +While being on call, you are responsible for acting as the first line of defense for critical incidents and answering customer support inquiries. +During your working hours, you must respond to all support tickets or involve relevant team members with sufficient context. +Outside of working hours, you are expected to be available for any high-severity pager alerts and critical support inquiries by customers. + +### Responsibilities While On Call + +During your working hours, prioritize the following in this order: + +1. **Responding to Alerts:** + - Monitor and respond promptly to all PagerDuty alerts. + - Investigate incidents, determine root causes, and mitigate issues. + - Refer to runbooks or any relevant documentation to resolve alarms quickly. +2. **Customer Support:** + - Actively monitor all support inquiries in [**Pylon**](https://app.usepylon.com/issues) and respond to incoming tickets. + - Debug and resolve customer issues. If you encounter a problem outside your expertise, collaborate with the relevant teammates to resolve it. This is an opportunity to learn and build context for future incidents. +3. **Sprint work:** + - Since the current on-call workload does not require all of your working hours, you are expected to work on the sprint items assigned to you. + If the on-call workload increases significantly, inform Maidul to make adjustments. +4. **Continuous Improvement:** + - Take note of recurring patterns, inefficiencies, and opportunities where we can automate to reduce on-call burdens in the future. + + + Outside of working hours, you are expected to be available and respond to any high-severity pager alerts and critical support inquiries by customers. + + +### Before You Get On Call + +- **Set Up PagerDuty:** Ensure you have the PagerDuty mobile app installed, configured, and notifications enabled for Infisical services. +- **Access Required Tools:** Verify access to internal network, runbooks on Notion, [https://grafana.infisical.com](https://grafana.infisical.com/), access to aws accounts and any other access you may require. +- **AWS Permissions:** You will be granted sufficient AWS permissions before the start of your on-call shift in case you need to access production accounts. + +### At the End of Your Shift + +- Post an on-call summary in the Slack channel `#on-call-summaries` at the end of your shift using the following [template](/documentation/engineering/oncall-summery-template). Include notable findings, support inquires and incidents you encountered. + This will helps the rest of the team stay in the loop and open discussions on how to prevent similar issues in the future. +- Do a **handoff meeting/slack huddle** with the next engineer on call to summarize any outstanding work, unresolved issues, or any incidents that require follow-up. Ensure the next on-call engineer is fully briefed so they can pick up where you left off. **Include Maidul in this hand off call.** + +### When to escalate an incident + +If you are paged for incident that you cannot resolve after attempting to debug and mitigate the issue, you should not hesitate to escalate and page others in. +It’s better to get help sooner rather than later to minimize the impact on customers. + +- **Paging relevant teammate:** If you’ve tried resolving an issue on your own and need additional help, page another engineer who might be relevant through PagerDuty. +- **Escalating to Maidul:** You can page Maidul at any time if you think it would be helpful. + +### How to be successful on you rotations + +- Be on top of all changes that get merged into main. This will help you be aware of any changes that might cause issues. +- When responding to support inquiries, double check your replies and make sure they are well written and typo-free. Always acknowledge inquiries quickly to make customers feel valued, and suggest a meeting or huddle if you need more clarity on their issues. +- When customers raise support inquiries, always consider what could have been done to make the inquiry self-serve. Could adding a tooltip next to the relevant feature provide clarity? Maybe the documentation could be more detailed or better organized? +- Document all of your notable support/findings/incidents/feature requests during on call so that it is easy to create your on call summary at the end of your on call shift. + +### Resources + +- [Pylon for support tickets](https://app.usepylon.com/issues) +- [AWS Portal](https://infisical.awsapps.com/start/) +- [View metrics on Grafana](https://grafana.infisical.com/) +- [Metabase](https://analytics.internal.infisical.com/) +- [Run books](https://www.notion.so/Runbooks-19e534883d6b4621b8c712194edbb687?pvs=21) +- [On call summary template](/documentation/engineering/oncall-summery-template) \ No newline at end of file diff --git a/company/handbook/compensation.mdx b/company/handbook/compensation.mdx new file mode 100644 index 000000000..4131c7ee6 --- /dev/null +++ b/company/handbook/compensation.mdx @@ -0,0 +1,28 @@ +--- +title: "Compensation" +sidebarTitle: "Compensation" +description: "This guide explains how various compensation processes work at Infisical." +--- + +## Probation period + +We are fully committed to ensuring that you are set up for success, but also understand that it may take some time to determine whether or not there is a long term fit between you and Infisical. + +The first 3 months of your employment with Infisical is a probation period. During this time, you can choose to end your contract with 1 week's notice. If we chose to end your contract, Infisical will pay you 4 weeks' pay, but usually ask you to finish on the same day. + +People in sales roles, such as Account Executives, have a 6 month probation period - this is to account for the fact that it can be difficult to establish whether or not someone is able to close contracts within their first 3 months, given sales cycles. + +Your manager is responsible for monitoring and specifically reviewing your performance throughout this initial period. If under-performance is a concern, or if there is any hesitation regarding the future at Infisical, this should be discussed immediately with you and your manager. + + +## Severance + +At Infisical, average performance gets a generous severance. + +If Infisical decides to end your contract after the first 3 months of employment have been completed, we will give you 10 weeks' pay. It is likely we will ask you to stop working immediately. + +If the decision to leave is yours, then we just require 1 month of notice. + +We have structured notice in this way as we believe it is in neither Infisical's nor your interest to lock you into a role that is no longer right for you due to financial considerations. This extended notice period only applies in the case of under-performance or a change in business needs - if your contract is terminated due to gross misconduct then you may be dismissed without notice. If this policy conflicts with the requirements of your local jurisdiction, then those local laws will take priority. + + diff --git a/company/handbook/meetings.mdx b/company/handbook/meetings.mdx index af6a3b54e..172c114c8 100644 --- a/company/handbook/meetings.mdx +++ b/company/handbook/meetings.mdx @@ -10,6 +10,10 @@ Being a remote-first company, we try to be as async as possible. When an issue a In other words, we have almost no (recurring) meetings and prefer written communication or quick Slack huddles. +## Daily Standup + +Towards the end of each day, everyone on the Engineering and GTM teams should document their progress in the respective Slack standup channels, ensuring the team stays informed of important updates. On the engineering side, if you are working on something that takes longer than 1-2 days, please add an estimated completion date (ECD) for that item in standup specifying when it will be pushed to production. + ## Weekly All-hands -All-hands is the single recurring meeting that we run every Monday at 8:30am PT. Typically, we would discuss everything important that happened during the previous week and plan out the week ahead. This is also an opportunity to bring up any important topics in front of the whole company (but feel free to post those in Slack too). +All-hands is the single recurring meeting that we run every Monday at 8:00am PT. Typically, we would discuss everything important that happened during the previous week and plan out the week ahead. This is also an opportunity to bring up any important topics in front of the whole company (but feel free to post those in Slack too). diff --git a/company/handbook/onboarding.mdx b/company/handbook/onboarding.mdx index bcf47a339..a406d1967 100644 --- a/company/handbook/onboarding.mdx +++ b/company/handbook/onboarding.mdx @@ -12,18 +12,15 @@ Plus, our team is remote-first and spread across the globe (from San Francisco t ## Onboarding buddy -Every new joiner has an onboarding buddy who should ideally be in the the same timezone. The onboarding buddy should be able to help with any questions that pop up during the first few weeks. Of course, everyone is available to help, but it's good to have a dedicated person that you can go to with any questions. +Every new joiner at Infisical will have an onboarding buddy—a teammate in a similar time zone who’s there to help you settle in. They are your go-to person for any questions that come up. Of course, everyone on the team is happy to help, but it’s always nice to have a dedicated person who’s there for you. Don’t hesitate to reach out to your buddy if you’re unsure about something or need a hand! Your onboarding buddy will set up regular syncs for your first two months—ideally at least 2-3 times a week. + +If you’re joining the engineering team, your onboarding buddy will: +1. Walk you through Infisical’s development process and share any best practices to keep in mind when tackling tickets. +2. Be your go-to person if you are blocked or need to think through your sprint task. +3. Help you ship something small on day one! ## Onboarding Checklist -1. Join the weekly all-hands meeting. It typically happens on Monday's at 8:30am PT. -2. Ship something together on day one – even if tiny! It feels great to hit the ground running, with a development environment all ready to go. -3. Check out the [Areas of Responsibility (AoR) Table](https://docs.google.com/spreadsheets/d/1RnXlGFg83Sgu0dh7ycuydsSobmFfI3A0XkGw7vrVxEI/edit?usp=sharing). This is helpful to know who you can ask about particular areas of Infisical. Feel free to add yourself to the areas you'd be most interesting to dive into. -4. Read the [Infisical Strategy Doc](https://docs.google.com/document/d/1RaJd3RoS2QpWLFHlgfHaXnHqCCwRt6mCGZkbJ75J_D0/edit?usp=sharing). -5. Update your LinkedIn profile with one of [Infisical's official banners](https://drive.google.com/drive/u/0/folders/1oSNWjbpRl9oNYwxM_98IqzKs9fAskrb2) (if you want to). You can also coordinate your social posts in the #marketing Slack channel, so that we can boost it from Infisical's official social media accounts. -6. Over the first few weeks, feel free to schedule 1:1s with folks on the team to get to know them a bit better. -7. Change your Slack username in the users channel to `[NAME] (Infisical)`. -8. Go through the [technical overview](https://infisical.com/docs/internals/overview) of Infisical. -9. Request a company credit card (Maidul will be able to help with that). +Your hiring manager will send you an onboarding checklist doc for your first day. diff --git a/company/handbook/spending-money.mdx b/company/handbook/spending-money.mdx index 046604399..1e32aeaf5 100644 --- a/company/handbook/spending-money.mdx +++ b/company/handbook/spending-money.mdx @@ -24,6 +24,9 @@ Make sure you keep copies for all receipts. If you expense something on a compan You should default to using your company card in all cases - it has no transaction fees. If using your personal card is unavoidable, please reach out to Maidul to get it reimbursed manually. +## Training + +For engineers, you’re welcome to take an approved Udemy course. Please reach out to Maidul. For the GTM team, you may buy a book a month if it’s relevant to your work. # Equipment @@ -55,4 +58,4 @@ For any equipment related questions, please reach out to Maidul. ## Brex -We use Brex as our primary credit card provider. Don't have a company card yet? Reach out to Maidul. \ No newline at end of file +We use Brex as our primary credit card provider. Don't have a company card yet? Reach out to Maidul. diff --git a/company/handbook/time-off.mdx b/company/handbook/time-off.mdx index aae111878..de9c70697 100644 --- a/company/handbook/time-off.mdx +++ b/company/handbook/time-off.mdx @@ -12,6 +12,6 @@ To request time off, just submit a request in Rippling and let Maidul know at le Since Infisical's team is globally distributed, it is hard for us to keep track of all the various national holidays across many different countries. Whether you'd like to celebrate Christmas or National Brisket Day (which, by the way, is on May 28th), you are welcome to take PTO on those days – just let Maidul know at least a week ahead so that we can adjust our planning. -## Winter Break +## Winter break -Every year, Infisical team goes on a company-wide vacation during winter holidays. This year, the winter break period starts on December 21st, 2024 and ends on January 5th, 2025. You should expect to do no scheduled work during this period, but we will have a rotation process for [high and urgent service disruptions](https://infisical.com/sla). \ No newline at end of file +Every year, Infisical team goes on a company-wide vacation during winter holidays. This year, the winter break period starts on December 21st, 2024 and ends on January 5th, 2025. You should expect to do no scheduled work during this period, but we will have a rotation process for [high and urgent service disruptions](https://infisical.com/sla). diff --git a/company/mint.json b/company/mint.json index c29f6c237..247fb5a2b 100644 --- a/company/mint.json +++ b/company/mint.json @@ -58,10 +58,18 @@ "pages": [ "handbook/onboarding", "handbook/spending-money", + "handbook/compensation", "handbook/time-off", "handbook/hiring", "handbook/meetings", - "handbook/talking-to-customers" + "handbook/talking-to-customers", + { + "group": "Engineering", + "pages": [ + "documentation/engineering/oncall", + "documentation/engineering/how-to-write-design-doc" + ] + } ] } ], diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 9e56ae589..590e17763 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -56,20 +56,6 @@ services: POSTGRES_USER: infisical POSTGRES_DB: infisical-test - db-migration: - container_name: infisical-db-migration - depends_on: - - db - build: - context: ./backend - dockerfile: Dockerfile.dev - env_file: .env - environment: - - DB_CONNECTION_URI=postgres://infisical:infisical@db/infisical?sslmode=disable - command: npm run migration:latest - volumes: - - ./backend/src:/app/src - backend: container_name: infisical-dev-api build: @@ -80,12 +66,11 @@ services: condition: service_started redis: condition: service_started - db-migration: - condition: service_completed_successfully env_file: - .env ports: - 4000:4000 + - 9464:9464 # for OTEL collection of Prometheus metrics environment: - NODE_ENV=development - DB_CONNECTION_URI=postgres://infisical:infisical@db/infisical?sslmode=disable @@ -95,6 +80,42 @@ services: extra_hosts: - "host.docker.internal:host-gateway" + prometheus: + image: prom/prometheus + volumes: + - ./prometheus.dev.yml:/etc/prometheus/prometheus.yml + ports: + - "9090:9090" + command: + - "--config.file=/etc/prometheus/prometheus.yml" + profiles: [metrics] + + otel-collector: + image: otel/opentelemetry-collector-contrib + volumes: + - ./otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml + ports: + - 1888:1888 # pprof extension + - 8888:8888 # Prometheus metrics exposed by the Collector + - 8889:8889 # Prometheus exporter metrics + - 13133:13133 # health_check extension + - 4317:4317 # OTLP gRPC receiver + - 4318:4318 # OTLP http receiver + - 55679:55679 # zpages extension + profiles: [metrics-otel] + + grafana: + image: grafana/grafana + container_name: grafana + restart: unless-stopped + environment: + - GF_LOG_LEVEL=debug + ports: + - "3005:3000" + volumes: + - "grafana_storage:/var/lib/grafana" + profiles: [metrics] + frontend: container_name: infisical-dev-frontend restart: unless-stopped @@ -107,13 +128,12 @@ services: - ./frontend/src:/app/src/ # mounted whole src to avoid missing reload on new files - ./frontend/public:/app/public env_file: .env - environment: - - NEXT_PUBLIC_ENV=development - - INFISICAL_TELEMETRY_ENABLED=false pgadmin: image: dpage/pgadmin4 restart: always + volumes: + - ./servers.json:/pgadmin4/servers.json environment: PGADMIN_DEFAULT_EMAIL: admin@example.com PGADMIN_DEFAULT_PASSWORD: pass @@ -159,6 +179,17 @@ services: - openldap profiles: [ldap] + keycloak: + image: quay.io/keycloak/keycloak:26.1.0 + restart: always + environment: + - KC_BOOTSTRAP_ADMIN_PASSWORD=admin + - KC_BOOTSTRAP_ADMIN_USERNAME=admin + command: start-dev + ports: + - 8088:8080 + profiles: [sso] + volumes: postgres-data: driver: local @@ -166,3 +197,4 @@ volumes: driver: local ldap_data: ldap_config: + grafana_storage: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 40c17a7fe..f12d446ef 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,18 +1,6 @@ version: "3" services: - db-migration: - container_name: infisical-db-migration - depends_on: - db: - condition: service_healthy - image: infisical/infisical:latest-postgres - env_file: .env - command: npm run migration:latest - pull_policy: always - networks: - - infisical - backend: container_name: infisical-backend restart: unless-stopped @@ -21,8 +9,6 @@ services: condition: service_healthy redis: condition: service_started - db-migration: - condition: service_completed_successfully image: infisical/infisical:latest-postgres pull_policy: always env_file: .env diff --git a/docker-swarm/.env-example b/docker-swarm/.env-example index 03d05a08e..a30e3bba6 100644 --- a/docker-swarm/.env-example +++ b/docker-swarm/.env-example @@ -20,7 +20,8 @@ SITE_URL=http://localhost:8080 # Mail/SMTP SMTP_HOST= SMTP_PORT= -SMTP_NAME= +SMTP_FROM_ADDRESS= +SMTP_FROM_NAME= SMTP_USERNAME= SMTP_PASSWORD= diff --git a/docs/api-reference/endpoints/app-connections/auth0/available.mdx b/docs/api-reference/endpoints/app-connections/auth0/available.mdx new file mode 100644 index 000000000..6694976dc --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/auth0/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/auth0/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/auth0/create.mdx b/docs/api-reference/endpoints/app-connections/auth0/create.mdx new file mode 100644 index 000000000..11e003163 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/auth0/create.mdx @@ -0,0 +1,9 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/auth0" +--- + + + Check out the configuration docs for [Auth0 Connections](/integrations/app-connections/auth0) to learn how to obtain the + required credentials. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/auth0/delete.mdx b/docs/api-reference/endpoints/app-connections/auth0/delete.mdx new file mode 100644 index 000000000..f1e79e125 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/auth0/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/auth0/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/auth0/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/auth0/get-by-id.mdx new file mode 100644 index 000000000..0b3a1d355 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/auth0/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/auth0/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/auth0/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/auth0/get-by-name.mdx new file mode 100644 index 000000000..691791523 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/auth0/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/auth0/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/auth0/list.mdx b/docs/api-reference/endpoints/app-connections/auth0/list.mdx new file mode 100644 index 000000000..9590b0487 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/auth0/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/auth0" +--- diff --git a/docs/api-reference/endpoints/app-connections/auth0/update.mdx b/docs/api-reference/endpoints/app-connections/auth0/update.mdx new file mode 100644 index 000000000..e7046ba19 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/auth0/update.mdx @@ -0,0 +1,9 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/auth0/{connectionId}" +--- + + + Check out the configuration docs for [Auth0 Connections](/integrations/app-connections/auth0) to learn how to obtain the + required credentials. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/aws/available.mdx b/docs/api-reference/endpoints/app-connections/aws/available.mdx new file mode 100644 index 000000000..1386c068d --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/aws/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/aws/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/aws/create.mdx b/docs/api-reference/endpoints/app-connections/aws/create.mdx new file mode 100644 index 000000000..2fd1602ed --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/aws/create.mdx @@ -0,0 +1,9 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/aws" +--- + + + Check out the configuration docs for [AWS Connections](/integrations/app-connections/aws) to learn how to obtain + the required credentials. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/aws/delete.mdx b/docs/api-reference/endpoints/app-connections/aws/delete.mdx new file mode 100644 index 000000000..e6030257f --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/aws/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/aws/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/aws/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/aws/get-by-id.mdx new file mode 100644 index 000000000..0a057cc1b --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/aws/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/aws/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/aws/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/aws/get-by-name.mdx new file mode 100644 index 000000000..d6db40ade --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/aws/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/aws/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/aws/list.mdx b/docs/api-reference/endpoints/app-connections/aws/list.mdx new file mode 100644 index 000000000..5ea0c50a0 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/aws/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/aws" +--- diff --git a/docs/api-reference/endpoints/app-connections/aws/update.mdx b/docs/api-reference/endpoints/app-connections/aws/update.mdx new file mode 100644 index 000000000..4fd3a4a00 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/aws/update.mdx @@ -0,0 +1,9 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/aws/{connectionId}" +--- + + + Check out the configuration docs for [AWS Connections](/integrations/app-connections/aws) to learn how to obtain + the required credentials. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/azure-app-configuration/available.mdx b/docs/api-reference/endpoints/app-connections/azure-app-configuration/available.mdx new file mode 100644 index 000000000..03d5f3537 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-app-configuration/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/azure-app-configuration/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-app-configuration/create.mdx b/docs/api-reference/endpoints/app-connections/azure-app-configuration/create.mdx new file mode 100644 index 000000000..6c429c369 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-app-configuration/create.mdx @@ -0,0 +1,10 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/azure-app-configuration" +--- + + + Azure App Configuration Connections must be created through the Infisical UI. + Check out the configuration docs for [Azure App Configuration Connections](/integrations/app-connections/azure-app-configuration) for a step-by-step + guide. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/azure-app-configuration/delete.mdx b/docs/api-reference/endpoints/app-connections/azure-app-configuration/delete.mdx new file mode 100644 index 000000000..cc2e4ca38 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-app-configuration/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/azure-app-configuration/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-app-configuration/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/azure-app-configuration/get-by-id.mdx new file mode 100644 index 000000000..c49afe500 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-app-configuration/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/azure-app-configuration/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-app-configuration/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/azure-app-configuration/get-by-name.mdx new file mode 100644 index 000000000..a38365b01 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-app-configuration/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/azure-app-configuration/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-app-configuration/list.mdx b/docs/api-reference/endpoints/app-connections/azure-app-configuration/list.mdx new file mode 100644 index 000000000..4da96476d --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-app-configuration/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/azure-app-configuration" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-app-configuration/update.mdx b/docs/api-reference/endpoints/app-connections/azure-app-configuration/update.mdx new file mode 100644 index 000000000..65d29899e --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-app-configuration/update.mdx @@ -0,0 +1,10 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/azure-app-configuration/{connectionId}" +--- + + + Azure App Configuration Connections must be updated through the Infisical UI. + Check out the configuration docs for [Azure App Configuration Connections](/integrations/app-connections/azure-app-configuration) for a step-by-step + guide. + diff --git a/docs/api-reference/endpoints/app-connections/azure-key-vault/available.mdx b/docs/api-reference/endpoints/app-connections/azure-key-vault/available.mdx new file mode 100644 index 000000000..4b1c758ee --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-key-vault/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/azure-key-vault/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-key-vault/create.mdx b/docs/api-reference/endpoints/app-connections/azure-key-vault/create.mdx new file mode 100644 index 000000000..d0d9f7e6f --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-key-vault/create.mdx @@ -0,0 +1,10 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/azure-key-vault" +--- + + + Azure Key Vault Connections must be created through the Infisical UI. + Check out the configuration docs for [Azure Key Vault Connections](/integrations/app-connections/azure-key-vault) for a step-by-step + guide. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/azure-key-vault/delete.mdx b/docs/api-reference/endpoints/app-connections/azure-key-vault/delete.mdx new file mode 100644 index 000000000..02fbe4a31 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-key-vault/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/azure-key-vault/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-key-vault/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/azure-key-vault/get-by-id.mdx new file mode 100644 index 000000000..e5d3e77a1 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-key-vault/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/azure-key-vault/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-key-vault/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/azure-key-vault/get-by-name.mdx new file mode 100644 index 000000000..55502def8 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-key-vault/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/azure-key-vault/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-key-vault/list.mdx b/docs/api-reference/endpoints/app-connections/azure-key-vault/list.mdx new file mode 100644 index 000000000..76f1f8b88 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-key-vault/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/azure-key-vault" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-key-vault/update.mdx b/docs/api-reference/endpoints/app-connections/azure-key-vault/update.mdx new file mode 100644 index 000000000..8637b838a --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-key-vault/update.mdx @@ -0,0 +1,10 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/azure-key-vault/{connectionId}" +--- + + + Azure Key Vault Connections must be updated through the Infisical UI. + Check out the configuration docs for [Azure Key Vault Connections](/integrations/app-connections/azure-key-vault) for a step-by-step + guide. + diff --git a/docs/api-reference/endpoints/app-connections/camunda/available.mdx b/docs/api-reference/endpoints/app-connections/camunda/available.mdx new file mode 100644 index 000000000..7df54478b --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/camunda/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/camunda/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/camunda/create.mdx b/docs/api-reference/endpoints/app-connections/camunda/create.mdx new file mode 100644 index 000000000..54896202d --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/camunda/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/camunda" +--- diff --git a/docs/api-reference/endpoints/app-connections/camunda/delete.mdx b/docs/api-reference/endpoints/app-connections/camunda/delete.mdx new file mode 100644 index 000000000..a8e6be6b6 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/camunda/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/camunda/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/camunda/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/camunda/get-by-id.mdx new file mode 100644 index 000000000..a8ede9255 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/camunda/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/camunda/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/camunda/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/camunda/get-by-name.mdx new file mode 100644 index 000000000..ae1cced4e --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/camunda/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/camunda/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/camunda/list.mdx b/docs/api-reference/endpoints/app-connections/camunda/list.mdx new file mode 100644 index 000000000..e98e535dd --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/camunda/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/camunda" +--- diff --git a/docs/api-reference/endpoints/app-connections/camunda/update.mdx b/docs/api-reference/endpoints/app-connections/camunda/update.mdx new file mode 100644 index 000000000..0db91e9d9 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/camunda/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/camunda/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/databricks/available.mdx b/docs/api-reference/endpoints/app-connections/databricks/available.mdx new file mode 100644 index 000000000..6c277f702 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/databricks/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/databricks/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/databricks/create.mdx b/docs/api-reference/endpoints/app-connections/databricks/create.mdx new file mode 100644 index 000000000..5361acb4b --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/databricks/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/databricks" +--- diff --git a/docs/api-reference/endpoints/app-connections/databricks/delete.mdx b/docs/api-reference/endpoints/app-connections/databricks/delete.mdx new file mode 100644 index 000000000..fba97fb14 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/databricks/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/databricks/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/databricks/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/databricks/get-by-id.mdx new file mode 100644 index 000000000..1f861328c --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/databricks/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/databricks/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/databricks/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/databricks/get-by-name.mdx new file mode 100644 index 000000000..f89c8a8d7 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/databricks/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/databricks/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/databricks/list.mdx b/docs/api-reference/endpoints/app-connections/databricks/list.mdx new file mode 100644 index 000000000..1449b5166 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/databricks/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/databricks" +--- diff --git a/docs/api-reference/endpoints/app-connections/databricks/update.mdx b/docs/api-reference/endpoints/app-connections/databricks/update.mdx new file mode 100644 index 000000000..69ddbf617 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/databricks/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/databricks/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/gcp/available.mdx b/docs/api-reference/endpoints/app-connections/gcp/available.mdx new file mode 100644 index 000000000..edf5e84f6 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/gcp/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/gcp/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/gcp/create.mdx b/docs/api-reference/endpoints/app-connections/gcp/create.mdx new file mode 100644 index 000000000..ebe7f1295 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/gcp/create.mdx @@ -0,0 +1,10 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/gcp" +--- + + + Check out the configuration docs for [GCP + Connections](/integrations/app-connections/gcp) to learn how to obtain the + required credentials. + diff --git a/docs/api-reference/endpoints/app-connections/gcp/delete.mdx b/docs/api-reference/endpoints/app-connections/gcp/delete.mdx new file mode 100644 index 000000000..7cfbc10ba --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/gcp/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/gcp/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/gcp/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/gcp/get-by-id.mdx new file mode 100644 index 000000000..33a6009af --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/gcp/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/gcp/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/gcp/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/gcp/get-by-name.mdx new file mode 100644 index 000000000..ae2bb42a4 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/gcp/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/gcp/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/gcp/list.mdx b/docs/api-reference/endpoints/app-connections/gcp/list.mdx new file mode 100644 index 000000000..177af6ed9 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/gcp/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/gcp" +--- diff --git a/docs/api-reference/endpoints/app-connections/gcp/update.mdx b/docs/api-reference/endpoints/app-connections/gcp/update.mdx new file mode 100644 index 000000000..0c711fd8e --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/gcp/update.mdx @@ -0,0 +1,10 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/gcp/{connectionId}" +--- + + + Check out the configuration docs for [GCP + Connections](/integrations/app-connections/gcp) to learn how to obtain the + required credentials. + diff --git a/docs/api-reference/endpoints/app-connections/github/available.mdx b/docs/api-reference/endpoints/app-connections/github/available.mdx new file mode 100644 index 000000000..6d5596629 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/github/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/github/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/github/create.mdx b/docs/api-reference/endpoints/app-connections/github/create.mdx new file mode 100644 index 000000000..1e06fd64f --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/github/create.mdx @@ -0,0 +1,10 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/github" +--- + + + GitHub Connections must be created through the Infisical UI. + Check out the configuration docs for [GitHub Connections](/integrations/app-connections/github) for a step-by-step + guide. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/github/delete.mdx b/docs/api-reference/endpoints/app-connections/github/delete.mdx new file mode 100644 index 000000000..6b4f2e676 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/github/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/github/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/github/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/github/get-by-id.mdx new file mode 100644 index 000000000..c85d41d37 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/github/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/github/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/github/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/github/get-by-name.mdx new file mode 100644 index 000000000..cf959827b --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/github/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/github/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/github/list.mdx b/docs/api-reference/endpoints/app-connections/github/list.mdx new file mode 100644 index 000000000..c4b13b8eb --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/github/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/github" +--- diff --git a/docs/api-reference/endpoints/app-connections/github/update.mdx b/docs/api-reference/endpoints/app-connections/github/update.mdx new file mode 100644 index 000000000..7e2326c60 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/github/update.mdx @@ -0,0 +1,10 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/github/{connectionId}" +--- + + + GitHub Connections must be updated through the Infisical UI. + Check out the configuration docs for [GitHub Connections](/integrations/app-connections/github) for a step-by-step + guide. + diff --git a/docs/api-reference/endpoints/app-connections/humanitec/available.mdx b/docs/api-reference/endpoints/app-connections/humanitec/available.mdx new file mode 100644 index 000000000..eb95b2e54 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/humanitec/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/humanitec/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/humanitec/create.mdx b/docs/api-reference/endpoints/app-connections/humanitec/create.mdx new file mode 100644 index 000000000..a4d196911 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/humanitec/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/humanitec" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/humanitec/delete.mdx b/docs/api-reference/endpoints/app-connections/humanitec/delete.mdx new file mode 100644 index 000000000..d8786e08a --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/humanitec/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/humanitec/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/humanitec/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/humanitec/get-by-id.mdx new file mode 100644 index 000000000..22473a7a1 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/humanitec/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/humanitec/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/humanitec/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/humanitec/get-by-name.mdx new file mode 100644 index 000000000..fd848ae4f --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/humanitec/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/humanitec/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/humanitec/list.mdx b/docs/api-reference/endpoints/app-connections/humanitec/list.mdx new file mode 100644 index 000000000..07f30f674 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/humanitec/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/humanitec" +--- diff --git a/docs/api-reference/endpoints/app-connections/humanitec/update.mdx b/docs/api-reference/endpoints/app-connections/humanitec/update.mdx new file mode 100644 index 000000000..2a0806324 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/humanitec/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/humanitec/{connectionId}" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/list.mdx b/docs/api-reference/endpoints/app-connections/list.mdx new file mode 100644 index 000000000..e7ee6b009 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections" +--- diff --git a/docs/api-reference/endpoints/app-connections/mssql/available.mdx b/docs/api-reference/endpoints/app-connections/mssql/available.mdx new file mode 100644 index 000000000..cb8949c4a --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/mssql/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/mssql/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/mssql/create.mdx b/docs/api-reference/endpoints/app-connections/mssql/create.mdx new file mode 100644 index 000000000..996fe8e10 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/mssql/create.mdx @@ -0,0 +1,10 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/mssql" +--- + + + Check out the configuration docs for [Microsoft SQL Server + Connections](/integrations/app-connections/mssql) to learn how to obtain the + required credentials. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/mssql/delete.mdx b/docs/api-reference/endpoints/app-connections/mssql/delete.mdx new file mode 100644 index 000000000..af45cb416 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/mssql/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/mssql/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/mssql/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/mssql/get-by-id.mdx new file mode 100644 index 000000000..9eb08c97d --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/mssql/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/mssql/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/mssql/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/mssql/get-by-name.mdx new file mode 100644 index 000000000..c916d2219 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/mssql/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/mssql/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/mssql/list.mdx b/docs/api-reference/endpoints/app-connections/mssql/list.mdx new file mode 100644 index 000000000..490bb497b --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/mssql/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/mssql" +--- diff --git a/docs/api-reference/endpoints/app-connections/mssql/update.mdx b/docs/api-reference/endpoints/app-connections/mssql/update.mdx new file mode 100644 index 000000000..75f91dc3b --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/mssql/update.mdx @@ -0,0 +1,10 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/mssql/{connectionId}" +--- + + + Check out the configuration docs for [Microsoft SQL Server + Connections](/integrations/app-connections/mssql) to learn how to obtain the + required credentials. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/options.mdx b/docs/api-reference/endpoints/app-connections/options.mdx new file mode 100644 index 000000000..7cc03aca3 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/options.mdx @@ -0,0 +1,4 @@ +--- +title: "Options" +openapi: "GET /api/v1/app-connections/options" +--- diff --git a/docs/api-reference/endpoints/app-connections/postgres/available.mdx b/docs/api-reference/endpoints/app-connections/postgres/available.mdx new file mode 100644 index 000000000..92e360d06 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/postgres/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/postgres/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/postgres/create.mdx b/docs/api-reference/endpoints/app-connections/postgres/create.mdx new file mode 100644 index 000000000..3dca12325 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/postgres/create.mdx @@ -0,0 +1,10 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/postgres" +--- + + + Check out the configuration docs for [PostgreSQL + Connections](/integrations/app-connections/postgres) to learn how to obtain the + required credentials. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/postgres/delete.mdx b/docs/api-reference/endpoints/app-connections/postgres/delete.mdx new file mode 100644 index 000000000..927bfec49 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/postgres/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/postgres/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/postgres/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/postgres/get-by-id.mdx new file mode 100644 index 000000000..3ee3f5996 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/postgres/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/postgres/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/postgres/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/postgres/get-by-name.mdx new file mode 100644 index 000000000..c9b29cb66 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/postgres/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/postgres/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/postgres/list.mdx b/docs/api-reference/endpoints/app-connections/postgres/list.mdx new file mode 100644 index 000000000..5d1be4664 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/postgres/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/postgres" +--- diff --git a/docs/api-reference/endpoints/app-connections/postgres/update.mdx b/docs/api-reference/endpoints/app-connections/postgres/update.mdx new file mode 100644 index 000000000..32a4217c1 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/postgres/update.mdx @@ -0,0 +1,10 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/postgres/{connectionId}" +--- + + + Check out the configuration docs for [PostgreSQL + Connections](/integrations/app-connections/postgres) to learn how to obtain the + required credentials. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/terraform-cloud/available.mdx b/docs/api-reference/endpoints/app-connections/terraform-cloud/available.mdx new file mode 100644 index 000000000..fb2dbdeaf --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/terraform-cloud/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/terraform-cloud/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/terraform-cloud/create.mdx b/docs/api-reference/endpoints/app-connections/terraform-cloud/create.mdx new file mode 100644 index 000000000..ad7d4a5d1 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/terraform-cloud/create.mdx @@ -0,0 +1,9 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/terraform-cloud" +--- + + + Check out the configuration docs for [Terraform Cloud Connections](/integrations/app-connections/terraform-cloud) to learn how to obtain + the required credentials. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/terraform-cloud/delete.mdx b/docs/api-reference/endpoints/app-connections/terraform-cloud/delete.mdx new file mode 100644 index 000000000..daa558f5a --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/terraform-cloud/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/terraform-cloud/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/terraform-cloud/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/terraform-cloud/get-by-id.mdx new file mode 100644 index 000000000..587cc8f11 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/terraform-cloud/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/terraform-cloud/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/terraform-cloud/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/terraform-cloud/get-by-name.mdx new file mode 100644 index 000000000..722381605 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/terraform-cloud/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/terraform-cloud/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/terraform-cloud/list.mdx b/docs/api-reference/endpoints/app-connections/terraform-cloud/list.mdx new file mode 100644 index 000000000..831846155 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/terraform-cloud/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/terraform-cloud" +--- diff --git a/docs/api-reference/endpoints/app-connections/terraform-cloud/update.mdx b/docs/api-reference/endpoints/app-connections/terraform-cloud/update.mdx new file mode 100644 index 000000000..b8f526a88 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/terraform-cloud/update.mdx @@ -0,0 +1,9 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/terraform-cloud/{connectionId}" +--- + + + Check out the configuration docs for [Terraform Cloud Connections](/integrations/app-connections/terraform-cloud) to learn how to obtain + the required credentials. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/vercel/available.mdx b/docs/api-reference/endpoints/app-connections/vercel/available.mdx new file mode 100644 index 000000000..16859bded --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/vercel/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/vercel/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/vercel/create.mdx b/docs/api-reference/endpoints/app-connections/vercel/create.mdx new file mode 100644 index 000000000..63998ac32 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/vercel/create.mdx @@ -0,0 +1,9 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/vercel" +--- + + + Check out the configuration docs for [Vercel Connections](/integrations/app-connections/vercel) to learn how to obtain + the required credentials. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/vercel/delete.mdx b/docs/api-reference/endpoints/app-connections/vercel/delete.mdx new file mode 100644 index 000000000..4e5b12eff --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/vercel/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/vercel/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/vercel/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/vercel/get-by-id.mdx new file mode 100644 index 000000000..fdeb715a8 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/vercel/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/vercel/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/vercel/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/vercel/get-by-name.mdx new file mode 100644 index 000000000..258ed67c7 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/vercel/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/vercel/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/vercel/list.mdx b/docs/api-reference/endpoints/app-connections/vercel/list.mdx new file mode 100644 index 000000000..5412d35bb --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/vercel/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/vercel" +--- diff --git a/docs/api-reference/endpoints/app-connections/vercel/update.mdx b/docs/api-reference/endpoints/app-connections/vercel/update.mdx new file mode 100644 index 000000000..d0e2f4ae2 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/vercel/update.mdx @@ -0,0 +1,9 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/vercel/{connectionId}" +--- + + + Check out the configuration docs for [Vercel Connections](/integrations/app-connections/vercel) to learn how to obtain + the required credentials. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/windmill/available.mdx b/docs/api-reference/endpoints/app-connections/windmill/available.mdx new file mode 100644 index 000000000..c202bf368 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/windmill/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/windmill/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/windmill/create.mdx b/docs/api-reference/endpoints/app-connections/windmill/create.mdx new file mode 100644 index 000000000..0894dad27 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/windmill/create.mdx @@ -0,0 +1,9 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/windmill" +--- + + + Check out the configuration docs for [Windmill Connections](/integrations/app-connections/windmill) to learn how to obtain + the required credentials. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/windmill/delete.mdx b/docs/api-reference/endpoints/app-connections/windmill/delete.mdx new file mode 100644 index 000000000..7a966c338 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/windmill/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/windmill/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/windmill/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/windmill/get-by-id.mdx new file mode 100644 index 000000000..95ae6dc5c --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/windmill/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/windmill/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/windmill/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/windmill/get-by-name.mdx new file mode 100644 index 000000000..fdfdbcb48 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/windmill/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/windmill/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/windmill/list.mdx b/docs/api-reference/endpoints/app-connections/windmill/list.mdx new file mode 100644 index 000000000..a69d46451 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/windmill/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/windmill" +--- diff --git a/docs/api-reference/endpoints/app-connections/windmill/update.mdx b/docs/api-reference/endpoints/app-connections/windmill/update.mdx new file mode 100644 index 000000000..a700ffea9 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/windmill/update.mdx @@ -0,0 +1,9 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/windmill/{connectionId}" +--- + + + Check out the configuration docs for [Windmill Connections](/integrations/app-connections/windmill) to learn how to obtain + the required credentials. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/identities/search.mdx b/docs/api-reference/endpoints/identities/search.mdx new file mode 100644 index 000000000..93906a33b --- /dev/null +++ b/docs/api-reference/endpoints/identities/search.mdx @@ -0,0 +1,4 @@ +--- +title: "Search" +openapi: "POST /api/v1/identities/search" +--- diff --git a/docs/api-reference/endpoints/jwt-auth/attach.mdx b/docs/api-reference/endpoints/jwt-auth/attach.mdx new file mode 100644 index 000000000..f2905f1a0 --- /dev/null +++ b/docs/api-reference/endpoints/jwt-auth/attach.mdx @@ -0,0 +1,4 @@ +--- +title: "Attach" +openapi: "POST /api/v1/auth/jwt-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/jwt-auth/login.mdx b/docs/api-reference/endpoints/jwt-auth/login.mdx new file mode 100644 index 000000000..c037fbf7f --- /dev/null +++ b/docs/api-reference/endpoints/jwt-auth/login.mdx @@ -0,0 +1,4 @@ +--- +title: "Login" +openapi: "POST /api/v1/auth/jwt-auth/login" +--- diff --git a/docs/api-reference/endpoints/jwt-auth/retrieve.mdx b/docs/api-reference/endpoints/jwt-auth/retrieve.mdx new file mode 100644 index 000000000..8100ef843 --- /dev/null +++ b/docs/api-reference/endpoints/jwt-auth/retrieve.mdx @@ -0,0 +1,4 @@ +--- +title: "Retrieve" +openapi: "GET /api/v1/auth/jwt-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/jwt-auth/revoke.mdx b/docs/api-reference/endpoints/jwt-auth/revoke.mdx new file mode 100644 index 000000000..13a61475a --- /dev/null +++ b/docs/api-reference/endpoints/jwt-auth/revoke.mdx @@ -0,0 +1,4 @@ +--- +title: "Revoke" +openapi: "DELETE /api/v1/auth/jwt-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/jwt-auth/update.mdx b/docs/api-reference/endpoints/jwt-auth/update.mdx new file mode 100644 index 000000000..8a53907ab --- /dev/null +++ b/docs/api-reference/endpoints/jwt-auth/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/auth/jwt-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/kms/keys/decrypt.mdx b/docs/api-reference/endpoints/kms/encryption/decrypt.mdx similarity index 100% rename from docs/api-reference/endpoints/kms/keys/decrypt.mdx rename to docs/api-reference/endpoints/kms/encryption/decrypt.mdx diff --git a/docs/api-reference/endpoints/kms/keys/encrypt.mdx b/docs/api-reference/endpoints/kms/encryption/encrypt.mdx similarity index 100% rename from docs/api-reference/endpoints/kms/keys/encrypt.mdx rename to docs/api-reference/endpoints/kms/encryption/encrypt.mdx diff --git a/docs/api-reference/endpoints/kms/keys/get-by-id.mdx b/docs/api-reference/endpoints/kms/keys/get-by-id.mdx new file mode 100644 index 000000000..a896c640e --- /dev/null +++ b/docs/api-reference/endpoints/kms/keys/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Key by ID" +openapi: "Get /api/v1/kms/keys/{keyId}" +--- diff --git a/docs/api-reference/endpoints/kms/keys/get-by-name.mdx b/docs/api-reference/endpoints/kms/keys/get-by-name.mdx new file mode 100644 index 000000000..fe94da19b --- /dev/null +++ b/docs/api-reference/endpoints/kms/keys/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Key by Name" +openapi: "Get /api/v1/kms/keys/key-name/{keyName}" +--- diff --git a/docs/api-reference/endpoints/kms/signing/public-key.mdx b/docs/api-reference/endpoints/kms/signing/public-key.mdx new file mode 100644 index 000000000..4c8e1fda5 --- /dev/null +++ b/docs/api-reference/endpoints/kms/signing/public-key.mdx @@ -0,0 +1,4 @@ +--- +title: "Retrieve Public Key" +openapi: "GET /api/v1/kms/keys/{keyId}/public-key" +--- diff --git a/docs/api-reference/endpoints/kms/signing/sign.mdx b/docs/api-reference/endpoints/kms/signing/sign.mdx new file mode 100644 index 000000000..ebeca5924 --- /dev/null +++ b/docs/api-reference/endpoints/kms/signing/sign.mdx @@ -0,0 +1,4 @@ +--- +title: "Sign Data" +openapi: "POST /api/v1/kms/keys/{keyId}/sign" +--- diff --git a/docs/api-reference/endpoints/kms/signing/signing-algorithms.mdx b/docs/api-reference/endpoints/kms/signing/signing-algorithms.mdx new file mode 100644 index 000000000..0a09ef9e0 --- /dev/null +++ b/docs/api-reference/endpoints/kms/signing/signing-algorithms.mdx @@ -0,0 +1,4 @@ +--- +title: "List Signing Algorithms" +openapi: "GET /api/v1/kms/keys/{keyId}/signing-algorithms" +--- diff --git a/docs/api-reference/endpoints/kms/signing/verify.mdx b/docs/api-reference/endpoints/kms/signing/verify.mdx new file mode 100644 index 000000000..a76270fc3 --- /dev/null +++ b/docs/api-reference/endpoints/kms/signing/verify.mdx @@ -0,0 +1,4 @@ +--- +title: "Verify Signature" +openapi: "POST /api/v1/kms/keys/{keyId}/verify" +--- diff --git a/docs/api-reference/endpoints/project-groups/create.mdx b/docs/api-reference/endpoints/project-groups/create.mdx index 6b468085e..6dd7f1a4f 100644 --- a/docs/api-reference/endpoints/project-groups/create.mdx +++ b/docs/api-reference/endpoints/project-groups/create.mdx @@ -1,4 +1,4 @@ --- title: "Create Project Membership" -openapi: "POST /api/v2/workspace/{projectId}/groups/{groupId}" +openapi: "POST /api/v2/workspace/{projectId}/groups/{groupIdOrName}" --- diff --git a/docs/api-reference/endpoints/project-roles/create.mdx b/docs/api-reference/endpoints/project-roles/create.mdx index 7ebfff262..97570ec36 100644 --- a/docs/api-reference/endpoints/project-roles/create.mdx +++ b/docs/api-reference/endpoints/project-roles/create.mdx @@ -1,8 +1,10 @@ --- title: "Create" -openapi: "POST /api/v1/workspace/{projectSlug}/roles" +openapi: "POST /api/v2/workspace/{projectId}/roles" --- - You can read more about the permissions field in the [permissions documentation](/internals/permissions). - \ No newline at end of file + You can read more about the permissions field in the [permissions + documentation](/internals/permissions). + + diff --git a/docs/api-reference/endpoints/project-roles/delete.mdx b/docs/api-reference/endpoints/project-roles/delete.mdx index 6362c2154..41edfa7c3 100644 --- a/docs/api-reference/endpoints/project-roles/delete.mdx +++ b/docs/api-reference/endpoints/project-roles/delete.mdx @@ -1,4 +1,4 @@ --- title: "Delete" -openapi: "DELETE /api/v1/workspace/{projectSlug}/roles/{roleId}" +openapi: "DELETE /api/v2/workspace/{projectId}/roles/{roleId}" --- diff --git a/docs/api-reference/endpoints/project-roles/get-by-slug.mdx b/docs/api-reference/endpoints/project-roles/get-by-slug.mdx index 18817bca9..dfc5c582a 100644 --- a/docs/api-reference/endpoints/project-roles/get-by-slug.mdx +++ b/docs/api-reference/endpoints/project-roles/get-by-slug.mdx @@ -1,4 +1,4 @@ --- title: "Get By Slug" -openapi: "GET /api/v1/workspace/{projectSlug}/roles/slug/{slug}" +openapi: "GET /api/v2/workspace/{projectId}/roles/slug/{roleSlug}" --- diff --git a/docs/api-reference/endpoints/project-roles/list.mdx b/docs/api-reference/endpoints/project-roles/list.mdx index ca83d6e7d..8d8dc10c1 100644 --- a/docs/api-reference/endpoints/project-roles/list.mdx +++ b/docs/api-reference/endpoints/project-roles/list.mdx @@ -1,4 +1,4 @@ --- title: "List" -openapi: "GET /api/v1/workspace/{projectSlug}/roles" +openapi: "GET /api/v2/workspace/{projectId}/roles" --- diff --git a/docs/api-reference/endpoints/project-roles/update.mdx b/docs/api-reference/endpoints/project-roles/update.mdx index 5a3d9668e..662d5e617 100644 --- a/docs/api-reference/endpoints/project-roles/update.mdx +++ b/docs/api-reference/endpoints/project-roles/update.mdx @@ -1,4 +1,4 @@ --- title: "Update" -openapi: "PATCH /api/v1/workspace/{projectSlug}/roles/{roleId}" +openapi: "PATCH /api/v2/workspace/{projectId}/roles/{roleId}" --- diff --git a/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/create.mdx b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/create.mdx new file mode 100644 index 000000000..c279da181 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/create.mdx @@ -0,0 +1,9 @@ +--- +title: "Create" +openapi: "POST /api/v2/secret-rotations/auth0-client-secret" +--- + + + Check out the configuration docs for [Auth0 Client Secret Rotations](/documentation/platform/secret-rotation/auth0-client-secret) to learn how to obtain the + required parameters. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/delete.mdx b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/delete.mdx new file mode 100644 index 000000000..8cf4227d7 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v2/secret-rotations/auth0-client-secret/{rotationId}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-id.mdx b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-id.mdx new file mode 100644 index 000000000..60d9ad0a1 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v2/secret-rotations/auth0-client-secret/{rotationId}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-name.mdx b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-name.mdx new file mode 100644 index 000000000..e513f74ec --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v2/secret-rotations/auth0-client-secret/rotation-name/{rotationName}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/get-generated-credentials-by-id.mdx b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/get-generated-credentials-by-id.mdx new file mode 100644 index 000000000..a4489053f --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/get-generated-credentials-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Credentials by ID" +openapi: "GET /api/v2/secret-rotations/auth0-client-secret/{rotationId}/generated-credentials" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/list.mdx b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/list.mdx new file mode 100644 index 000000000..a1b1a70cc --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v2/secret-rotations/auth0-client-secret" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/rotate-secrets.mdx b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/rotate-secrets.mdx new file mode 100644 index 000000000..45349ca30 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/rotate-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Rotate Secrets" +openapi: "POST /api/v2/secret-rotations/auth0-client-secret/{rotationId}/rotate-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/update.mdx b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/update.mdx new file mode 100644 index 000000000..514730100 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/auth0-client-secret/update.mdx @@ -0,0 +1,9 @@ +--- +title: "Update" +openapi: "PATCH /api/v2/secret-rotations/auth0-client-secret/{rotationId}" +--- + + + Check out the configuration docs for [Auth0 Client Secret Rotations](/documentation/platform/secret-rotation/auth0-client-secret) to learn how to obtain the + required parameters. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/secret-rotations/list.mdx b/docs/api-reference/endpoints/secret-rotations/list.mdx new file mode 100644 index 000000000..8b3e931f0 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v2/secret-rotations" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/mssql-credentials/create.mdx b/docs/api-reference/endpoints/secret-rotations/mssql-credentials/create.mdx new file mode 100644 index 000000000..5ed8c08b9 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/mssql-credentials/create.mdx @@ -0,0 +1,10 @@ +--- +title: "Create" +openapi: "POST /api/v2/secret-rotations/mssql-credentials" +--- + + + Check out the configuration docs for [Microsoft SQL Server + Credentials Rotations](/documentation/platform/secret-rotation/mssql-credentials) to learn how to obtain the + required parameters. + diff --git a/docs/api-reference/endpoints/secret-rotations/mssql-credentials/delete.mdx b/docs/api-reference/endpoints/secret-rotations/mssql-credentials/delete.mdx new file mode 100644 index 000000000..117948674 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/mssql-credentials/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v2/secret-rotations/mssql-credentials/{rotationId}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/mssql-credentials/get-by-id.mdx b/docs/api-reference/endpoints/secret-rotations/mssql-credentials/get-by-id.mdx new file mode 100644 index 000000000..e0fc208ee --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/mssql-credentials/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v2/secret-rotations/mssql-credentials/{rotationId}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/mssql-credentials/get-by-name.mdx b/docs/api-reference/endpoints/secret-rotations/mssql-credentials/get-by-name.mdx new file mode 100644 index 000000000..442ab5bd7 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/mssql-credentials/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v2/secret-rotations/mssql-credentials/rotation-name/{rotationName}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/mssql-credentials/get-generated-credentials-by-id.mdx b/docs/api-reference/endpoints/secret-rotations/mssql-credentials/get-generated-credentials-by-id.mdx new file mode 100644 index 000000000..311715879 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/mssql-credentials/get-generated-credentials-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Credentials by ID" +openapi: "GET /api/v2/secret-rotations/mssql-credentials/{rotationId}/generated-credentials" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/mssql-credentials/list.mdx b/docs/api-reference/endpoints/secret-rotations/mssql-credentials/list.mdx new file mode 100644 index 000000000..e79ee758b --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/mssql-credentials/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v2/secret-rotations/mssql-credentials" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/mssql-credentials/rotate-secrets.mdx b/docs/api-reference/endpoints/secret-rotations/mssql-credentials/rotate-secrets.mdx new file mode 100644 index 000000000..543acb9e3 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/mssql-credentials/rotate-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Rotate Secrets" +openapi: "POST /api/v2/secret-rotations/mssql-credentials/{rotationId}/rotate-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/mssql-credentials/update.mdx b/docs/api-reference/endpoints/secret-rotations/mssql-credentials/update.mdx new file mode 100644 index 000000000..027d83a1f --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/mssql-credentials/update.mdx @@ -0,0 +1,10 @@ +--- +title: "Update" +openapi: "PATCH /api/v2/secret-rotations/mssql-credentials/{rotationId}" +--- + + + Check out the configuration docs for [Microsoft SQL Server + Credentials Rotations](/documentation/platform/secret-rotation/mssql-credentials) to learn how to obtain the + required parameters. + diff --git a/docs/api-reference/endpoints/secret-rotations/options.mdx b/docs/api-reference/endpoints/secret-rotations/options.mdx new file mode 100644 index 000000000..9e1a4e544 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/options.mdx @@ -0,0 +1,4 @@ +--- +title: "Options" +openapi: "GET /api/v2/secret-rotations/options" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/postgres-credentials/create.mdx b/docs/api-reference/endpoints/secret-rotations/postgres-credentials/create.mdx new file mode 100644 index 000000000..e22b89f81 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/postgres-credentials/create.mdx @@ -0,0 +1,10 @@ +--- +title: "Create" +openapi: "POST /api/v2/secret-rotations/postgres-credentials" +--- + + + Check out the configuration docs for [PostgreSQL + Credentials Rotations](/documentation/platform/secret-rotation/postgres-credentials) to learn how to obtain the + required parameters. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/secret-rotations/postgres-credentials/delete.mdx b/docs/api-reference/endpoints/secret-rotations/postgres-credentials/delete.mdx new file mode 100644 index 000000000..7919313b6 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/postgres-credentials/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v2/secret-rotations/postgres-credentials/{rotationId}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/postgres-credentials/get-by-id.mdx b/docs/api-reference/endpoints/secret-rotations/postgres-credentials/get-by-id.mdx new file mode 100644 index 000000000..7914eac7e --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/postgres-credentials/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v2/secret-rotations/postgres-credentials/{rotationId}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/postgres-credentials/get-by-name.mdx b/docs/api-reference/endpoints/secret-rotations/postgres-credentials/get-by-name.mdx new file mode 100644 index 000000000..f215a1d7b --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/postgres-credentials/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v2/secret-rotations/postgres-credentials/rotation-name/{rotationName}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/postgres-credentials/get-generated-credentials-by-id.mdx b/docs/api-reference/endpoints/secret-rotations/postgres-credentials/get-generated-credentials-by-id.mdx new file mode 100644 index 000000000..34f308514 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/postgres-credentials/get-generated-credentials-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Credentials by ID" +openapi: "GET /api/v2/secret-rotations/postgres-credentials/{rotationId}/generated-credentials" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/postgres-credentials/list.mdx b/docs/api-reference/endpoints/secret-rotations/postgres-credentials/list.mdx new file mode 100644 index 000000000..6c93a2790 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/postgres-credentials/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v2/secret-rotations/postgres-credentials" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/postgres-credentials/rotate-secrets.mdx b/docs/api-reference/endpoints/secret-rotations/postgres-credentials/rotate-secrets.mdx new file mode 100644 index 000000000..687c15279 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/postgres-credentials/rotate-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Rotate Secrets" +openapi: "POST /api/v2/secret-rotations/postgres-credentials/{rotationId}/rotate-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/postgres-credentials/update.mdx b/docs/api-reference/endpoints/secret-rotations/postgres-credentials/update.mdx new file mode 100644 index 000000000..46438427f --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/postgres-credentials/update.mdx @@ -0,0 +1,10 @@ +--- +title: "Update" +openapi: "PATCH /api/v2/secret-rotations/postgres-credentials/{rotationId}" +--- + + + Check out the configuration docs for [PostgreSQL + Credentials Rotations](/documentation/platform/secret-rotation/postgres-credentials) to learn how to obtain the + required parameters. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/create.mdx b/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/create.mdx new file mode 100644 index 000000000..2e29b8a5a --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/aws-parameter-store" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/delete.mdx b/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/delete.mdx new file mode 100644 index 000000000..2c801aba3 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/aws-parameter-store/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/get-by-id.mdx new file mode 100644 index 000000000..aeecf16e1 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/aws-parameter-store/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/get-by-name.mdx new file mode 100644 index 000000000..67930be3c --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/aws-parameter-store/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/import-secrets.mdx new file mode 100644 index 000000000..217fd849c --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/aws-parameter-store/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/list.mdx b/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/list.mdx new file mode 100644 index 000000000..8a0c2281d --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/aws-parameter-store" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/remove-secrets.mdx new file mode 100644 index 000000000..bc617b40d --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/aws-parameter-store/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/sync-secrets.mdx new file mode 100644 index 000000000..12b723054 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/aws-parameter-store/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/update.mdx b/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/update.mdx new file mode 100644 index 000000000..b290ddfa4 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-parameter-store/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/aws-parameter-store/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/create.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/create.mdx new file mode 100644 index 000000000..c7fbe5a19 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/aws-secrets-manager" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/delete.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/delete.mdx new file mode 100644 index 000000000..aacd95571 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/aws-secrets-manager/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-id.mdx new file mode 100644 index 000000000..31805aaf6 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/aws-secrets-manager/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-name.mdx new file mode 100644 index 000000000..4eac37dc2 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/aws-secrets-manager/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/import-secrets.mdx new file mode 100644 index 000000000..c3cc65be1 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/aws-secrets-manager/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/list.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/list.mdx new file mode 100644 index 000000000..1775ba450 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/aws-secrets-manager" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/remove-secrets.mdx new file mode 100644 index 000000000..5398766b9 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/aws-secrets-manager/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/sync-secrets.mdx new file mode 100644 index 000000000..9119f6147 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/aws-secrets-manager/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/update.mdx b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/update.mdx new file mode 100644 index 000000000..6d886630d --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/aws-secrets-manager/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/aws-secrets-manager/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/create.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/create.mdx new file mode 100644 index 000000000..82456fb6c --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/azure-app-configuration" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/delete.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/delete.mdx new file mode 100644 index 000000000..23c2ad27b --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/azure-app-configuration/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-id.mdx new file mode 100644 index 000000000..45418f047 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/azure-app-configuration/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-name.mdx new file mode 100644 index 000000000..488a15974 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/azure-app-configuration/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/import-secrets.mdx new file mode 100644 index 000000000..b44944951 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/azure-app-configuration/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/list.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/list.mdx new file mode 100644 index 000000000..2ea72fb20 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/azure-app-configuration" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/remove-secrets.mdx new file mode 100644 index 000000000..952a852d2 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/azure-app-configuration/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/sync-secrets.mdx new file mode 100644 index 000000000..e3d37dd56 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/azure-app-configuration/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/update.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/update.mdx new file mode 100644 index 000000000..b22a302dc --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/azure-app-configuration/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/create.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/create.mdx new file mode 100644 index 000000000..493e41abd --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/azure-key-vault" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/delete.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/delete.mdx new file mode 100644 index 000000000..7b02b6f68 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/azure-key-vault/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/get-by-id.mdx new file mode 100644 index 000000000..5b8ecd8c7 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/azure-key-vault/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/get-by-name.mdx new file mode 100644 index 000000000..dbb7b6f5c --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/azure-key-vault/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/import-secrets.mdx new file mode 100644 index 000000000..08ac487c9 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/azure-key-vault/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/list.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/list.mdx new file mode 100644 index 000000000..a462739f7 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/azure-key-vault" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/remove-secrets.mdx new file mode 100644 index 000000000..8882c5e47 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/azure-key-vault/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/sync-secrets.mdx new file mode 100644 index 000000000..87f6b4f56 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/azure-key-vault/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/update.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/update.mdx new file mode 100644 index 000000000..2d390ab8b --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/azure-key-vault/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/camunda/create.mdx b/docs/api-reference/endpoints/secret-syncs/camunda/create.mdx new file mode 100644 index 000000000..d1a6b4354 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/camunda/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/camunda" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/camunda/delete.mdx b/docs/api-reference/endpoints/secret-syncs/camunda/delete.mdx new file mode 100644 index 000000000..8ac85b1e9 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/camunda/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/camunda/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/camunda/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/camunda/get-by-id.mdx new file mode 100644 index 000000000..2579281fe --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/camunda/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/camunda/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/camunda/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/camunda/get-by-name.mdx new file mode 100644 index 000000000..876808543 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/camunda/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/camunda/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/camunda/list.mdx b/docs/api-reference/endpoints/secret-syncs/camunda/list.mdx new file mode 100644 index 000000000..54040e359 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/camunda/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/camunda" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/camunda/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/camunda/remove-secrets.mdx new file mode 100644 index 000000000..5757238f4 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/camunda/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/camunda/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/camunda/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/camunda/sync-secrets.mdx new file mode 100644 index 000000000..24a28909a --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/camunda/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/camunda/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/camunda/update.mdx b/docs/api-reference/endpoints/secret-syncs/camunda/update.mdx new file mode 100644 index 000000000..bc10cb500 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/camunda/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/camunda/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/databricks/create.mdx b/docs/api-reference/endpoints/secret-syncs/databricks/create.mdx new file mode 100644 index 000000000..ba91528f5 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/databricks/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/databricks" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/databricks/delete.mdx b/docs/api-reference/endpoints/secret-syncs/databricks/delete.mdx new file mode 100644 index 000000000..862681613 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/databricks/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/databricks/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/databricks/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/databricks/get-by-id.mdx new file mode 100644 index 000000000..7cf8fed53 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/databricks/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/databricks/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/databricks/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/databricks/get-by-name.mdx new file mode 100644 index 000000000..fe2d239ff --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/databricks/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/databricks/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/databricks/list.mdx b/docs/api-reference/endpoints/secret-syncs/databricks/list.mdx new file mode 100644 index 000000000..dd705408f --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/databricks/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/databricks" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/databricks/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/databricks/remove-secrets.mdx new file mode 100644 index 000000000..5e4e69fce --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/databricks/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/databricks/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/databricks/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/databricks/sync-secrets.mdx new file mode 100644 index 000000000..002fea158 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/databricks/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/databricks/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/databricks/update.mdx b/docs/api-reference/endpoints/secret-syncs/databricks/update.mdx new file mode 100644 index 000000000..4a43311a3 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/databricks/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/databricks/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/create.mdx b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/create.mdx new file mode 100644 index 000000000..f877d1e1b --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/gcp-secret-manager" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/delete.mdx b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/delete.mdx new file mode 100644 index 000000000..edb765728 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/gcp-secret-manager/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-id.mdx new file mode 100644 index 000000000..51ab1019e --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/gcp-secret-manager/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-name.mdx new file mode 100644 index 000000000..3a09872af --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/gcp-secret-manager/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/import-secrets.mdx new file mode 100644 index 000000000..a975d83bf --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/gcp-secret-manager/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/list.mdx b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/list.mdx new file mode 100644 index 000000000..ca2e59be8 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/gcp-secret-manager" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/remove-secrets.mdx new file mode 100644 index 000000000..a2a67ae93 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/gcp-secret-manager/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/sync-secrets.mdx new file mode 100644 index 000000000..899e72d7c --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/gcp-secret-manager/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/update.mdx b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/update.mdx new file mode 100644 index 000000000..fce03c90a --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/gcp-secret-manager/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/gcp-secret-manager/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/github/create.mdx b/docs/api-reference/endpoints/secret-syncs/github/create.mdx new file mode 100644 index 000000000..d0260b8ea --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/github/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/github" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/github/delete.mdx b/docs/api-reference/endpoints/secret-syncs/github/delete.mdx new file mode 100644 index 000000000..409a65cda --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/github/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/github/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/github/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/github/get-by-id.mdx new file mode 100644 index 000000000..d3c6da848 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/github/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/github/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/github/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/github/get-by-name.mdx new file mode 100644 index 000000000..b4c17b4d8 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/github/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/github/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/github/list.mdx b/docs/api-reference/endpoints/secret-syncs/github/list.mdx new file mode 100644 index 000000000..c3c0e10ab --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/github/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/github" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/github/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/github/remove-secrets.mdx new file mode 100644 index 000000000..1c133da8c --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/github/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/github/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/github/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/github/sync-secrets.mdx new file mode 100644 index 000000000..e1bcf1045 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/github/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/github/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/github/update.mdx b/docs/api-reference/endpoints/secret-syncs/github/update.mdx new file mode 100644 index 000000000..62d30327e --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/github/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/github/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/humanitec/create.mdx b/docs/api-reference/endpoints/secret-syncs/humanitec/create.mdx new file mode 100644 index 000000000..f683d67fe --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/humanitec/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/humanitec" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/humanitec/delete.mdx b/docs/api-reference/endpoints/secret-syncs/humanitec/delete.mdx new file mode 100644 index 000000000..ceef1fbb4 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/humanitec/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/humanitec/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/humanitec/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/humanitec/get-by-id.mdx new file mode 100644 index 000000000..a8a2a9bfc --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/humanitec/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/humanitec/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/humanitec/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/humanitec/get-by-name.mdx new file mode 100644 index 000000000..ad2f11290 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/humanitec/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/humanitec/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/humanitec/list.mdx b/docs/api-reference/endpoints/secret-syncs/humanitec/list.mdx new file mode 100644 index 000000000..651e7a435 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/humanitec/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/humanitec" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/humanitec/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/humanitec/remove-secrets.mdx new file mode 100644 index 000000000..7c5148638 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/humanitec/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/humanitec/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/humanitec/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/humanitec/sync-secrets.mdx new file mode 100644 index 000000000..cb0446f11 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/humanitec/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/humanitec/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/humanitec/update.mdx b/docs/api-reference/endpoints/secret-syncs/humanitec/update.mdx new file mode 100644 index 000000000..9e958555f --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/humanitec/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/humanitec/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/list.mdx b/docs/api-reference/endpoints/secret-syncs/list.mdx new file mode 100644 index 000000000..d18b47f9a --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/options.mdx b/docs/api-reference/endpoints/secret-syncs/options.mdx new file mode 100644 index 000000000..cc485111b --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/options.mdx @@ -0,0 +1,4 @@ +--- +title: "Options" +openapi: "GET /api/v1/secret-syncs/options" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/terraform-cloud/create.mdx b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/create.mdx new file mode 100644 index 000000000..491889e16 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/terraform-cloud" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/terraform-cloud/delete.mdx b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/delete.mdx new file mode 100644 index 000000000..dfd3206f5 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/terraform-cloud/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/terraform-cloud/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/get-by-id.mdx new file mode 100644 index 000000000..c25888a53 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/terraform-cloud/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/terraform-cloud/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/get-by-name.mdx new file mode 100644 index 000000000..5a1645866 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/terraform-cloud/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/terraform-cloud/list.mdx b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/list.mdx new file mode 100644 index 000000000..0993c76ef --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/terraform-cloud" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/terraform-cloud/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/remove-secrets.mdx new file mode 100644 index 000000000..6f00362e3 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/terraform-cloud/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/terraform-cloud/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/sync-secrets.mdx new file mode 100644 index 000000000..c71b68e48 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/terraform-cloud/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/terraform-cloud/update.mdx b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/update.mdx new file mode 100644 index 000000000..759fcfc71 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/terraform-cloud/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/terraform-cloud/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/vercel/create.mdx b/docs/api-reference/endpoints/secret-syncs/vercel/create.mdx new file mode 100644 index 000000000..e14d6dddd --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/vercel/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/vercel" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/vercel/delete.mdx b/docs/api-reference/endpoints/secret-syncs/vercel/delete.mdx new file mode 100644 index 000000000..746e7ffe5 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/vercel/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/vercel/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/vercel/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/vercel/get-by-id.mdx new file mode 100644 index 000000000..9a4efd1e6 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/vercel/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/vercel/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/vercel/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/vercel/get-by-name.mdx new file mode 100644 index 000000000..3f71a6b3b --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/vercel/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/vercel/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/vercel/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/vercel/import-secrets.mdx new file mode 100644 index 000000000..807eb2850 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/vercel/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/vercel/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/vercel/list.mdx b/docs/api-reference/endpoints/secret-syncs/vercel/list.mdx new file mode 100644 index 000000000..905470d0d --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/vercel/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/vercel" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/vercel/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/vercel/remove-secrets.mdx new file mode 100644 index 000000000..49c76ef99 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/vercel/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/vercel/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/vercel/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/vercel/sync-secrets.mdx new file mode 100644 index 000000000..2b3bc8324 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/vercel/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/vercel/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/vercel/update.mdx b/docs/api-reference/endpoints/secret-syncs/vercel/update.mdx new file mode 100644 index 000000000..75be8dd89 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/vercel/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/vercel/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/windmill/create.mdx b/docs/api-reference/endpoints/secret-syncs/windmill/create.mdx new file mode 100644 index 000000000..422cab9c1 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/windmill/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/windmill" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/windmill/delete.mdx b/docs/api-reference/endpoints/secret-syncs/windmill/delete.mdx new file mode 100644 index 000000000..05d039cb8 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/windmill/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/windmill/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/windmill/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/windmill/get-by-id.mdx new file mode 100644 index 000000000..25f040de5 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/windmill/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/windmill/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/windmill/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/windmill/get-by-name.mdx new file mode 100644 index 000000000..cf10c1c6e --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/windmill/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/windmill/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/windmill/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/windmill/import-secrets.mdx new file mode 100644 index 000000000..c2cf0cdc4 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/windmill/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/windmill/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/windmill/list.mdx b/docs/api-reference/endpoints/secret-syncs/windmill/list.mdx new file mode 100644 index 000000000..175a72f9c --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/windmill/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/windmill" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/windmill/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/windmill/remove-secrets.mdx new file mode 100644 index 000000000..8f6bfb02e --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/windmill/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/windmill/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/windmill/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/windmill/sync-secrets.mdx new file mode 100644 index 000000000..040345641 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/windmill/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/windmill/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/windmill/update.mdx b/docs/api-reference/endpoints/secret-syncs/windmill/update.mdx new file mode 100644 index 000000000..2b846691e --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/windmill/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/windmill/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secrets/create-many.mdx b/docs/api-reference/endpoints/secrets/create-many.mdx index 9b0609c0a..227c5470d 100644 --- a/docs/api-reference/endpoints/secrets/create-many.mdx +++ b/docs/api-reference/endpoints/secrets/create-many.mdx @@ -3,6 +3,3 @@ title: "Bulk Create" openapi: "POST /api/v3/secrets/batch/raw" --- - - This endpoint requires you to disable end-to-end encryption. For more information, you should consult this [note](https://infisical.com/docs/api-reference/overview/examples/note). - diff --git a/docs/api-reference/endpoints/secrets/create.mdx b/docs/api-reference/endpoints/secrets/create.mdx index 16591ca70..afee0a719 100644 --- a/docs/api-reference/endpoints/secrets/create.mdx +++ b/docs/api-reference/endpoints/secrets/create.mdx @@ -3,6 +3,3 @@ title: "Create" openapi: "POST /api/v3/secrets/raw/{secretName}" --- - - This endpoint requires you to disable end-to-end encryption. For more information, you should consult this [note](https://infisical.com/docs/api-reference/overview/examples/note). - diff --git a/docs/api-reference/endpoints/secrets/delete-many.mdx b/docs/api-reference/endpoints/secrets/delete-many.mdx index 6477b2a98..57c8588d8 100644 --- a/docs/api-reference/endpoints/secrets/delete-many.mdx +++ b/docs/api-reference/endpoints/secrets/delete-many.mdx @@ -3,6 +3,3 @@ title: "Bulk Delete" openapi: "DELETE /api/v3/secrets/batch/raw" --- - - This endpoint requires you to disable end-to-end encryption. For more information, you should consult this [note](https://infisical.com/docs/api-reference/overview/examples/note). - diff --git a/docs/api-reference/endpoints/secrets/delete.mdx b/docs/api-reference/endpoints/secrets/delete.mdx index c117d5e2c..ef3abc722 100644 --- a/docs/api-reference/endpoints/secrets/delete.mdx +++ b/docs/api-reference/endpoints/secrets/delete.mdx @@ -3,6 +3,3 @@ title: "Delete" openapi: "DELETE /api/v3/secrets/raw/{secretName}" --- - - This endpoint requires you to disable end-to-end encryption. For more information, you should consult this [note](https://infisical.com/docs/api-reference/overview/examples/note). - \ No newline at end of file diff --git a/docs/api-reference/endpoints/secrets/list.mdx b/docs/api-reference/endpoints/secrets/list.mdx index 09505762e..4808f6690 100644 --- a/docs/api-reference/endpoints/secrets/list.mdx +++ b/docs/api-reference/endpoints/secrets/list.mdx @@ -2,7 +2,3 @@ title: "List" openapi: "GET /api/v3/secrets/raw" --- - - - This endpoint requires you to disable end-to-end encryption. For more information, you should consult this [note](https://infisical.com/docs/api-reference/overview/examples/note). - \ No newline at end of file diff --git a/docs/api-reference/endpoints/secrets/read.mdx b/docs/api-reference/endpoints/secrets/read.mdx index 5c423cce3..6af308e0f 100644 --- a/docs/api-reference/endpoints/secrets/read.mdx +++ b/docs/api-reference/endpoints/secrets/read.mdx @@ -3,6 +3,3 @@ title: "Retrieve" openapi: "GET /api/v3/secrets/raw/{secretName}" --- - - This endpoint requires you to disable end-to-end encryption. For more information, you should consult this [note](https://infisical.com/docs/api-reference/overview/examples/note). - \ No newline at end of file diff --git a/docs/api-reference/endpoints/secrets/update-many.mdx b/docs/api-reference/endpoints/secrets/update-many.mdx index 9feaf2ca2..7586d91bc 100644 --- a/docs/api-reference/endpoints/secrets/update-many.mdx +++ b/docs/api-reference/endpoints/secrets/update-many.mdx @@ -3,6 +3,3 @@ title: "Bulk Update" openapi: "PATCH /api/v3/secrets/batch/raw" --- - - This endpoint requires you to disable end-to-end encryption. For more information, you should consult this [note](https://infisical.com/docs/api-reference/overview/examples/note). - diff --git a/docs/api-reference/endpoints/secrets/update.mdx b/docs/api-reference/endpoints/secrets/update.mdx index 3ece203ca..ce68c492e 100644 --- a/docs/api-reference/endpoints/secrets/update.mdx +++ b/docs/api-reference/endpoints/secrets/update.mdx @@ -2,7 +2,3 @@ title: "Update" openapi: "PATCH /api/v3/secrets/raw/{secretName}" --- - - - This endpoint requires you to disable end-to-end encryption. For more information, you should consult this [note](https://infisical.com/docs/api-reference/overview/examples/note). - \ No newline at end of file diff --git a/docs/api-reference/endpoints/ssh/ca/create.mdx b/docs/api-reference/endpoints/ssh/ca/create.mdx new file mode 100644 index 000000000..b053d0133 --- /dev/null +++ b/docs/api-reference/endpoints/ssh/ca/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/ssh/ca" +--- diff --git a/docs/api-reference/endpoints/ssh/ca/delete.mdx b/docs/api-reference/endpoints/ssh/ca/delete.mdx new file mode 100644 index 000000000..989fd1c4b --- /dev/null +++ b/docs/api-reference/endpoints/ssh/ca/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/ssh/ca/{sshCaId}" +--- diff --git a/docs/api-reference/endpoints/ssh/ca/list-certificate-templates.mdx b/docs/api-reference/endpoints/ssh/ca/list-certificate-templates.mdx new file mode 100644 index 000000000..632a9f8c0 --- /dev/null +++ b/docs/api-reference/endpoints/ssh/ca/list-certificate-templates.mdx @@ -0,0 +1,4 @@ +--- +title: "List templates" +openapi: "GET /api/v1/ssh/ca/{sshCaId}/certificate-templates" +--- diff --git a/docs/api-reference/endpoints/ssh/ca/list.mdx b/docs/api-reference/endpoints/ssh/ca/list.mdx new file mode 100644 index 000000000..c31dd4099 --- /dev/null +++ b/docs/api-reference/endpoints/ssh/ca/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v2/workspace/{projectId}/ssh-cas" +--- diff --git a/docs/api-reference/endpoints/ssh/ca/public-key.mdx b/docs/api-reference/endpoints/ssh/ca/public-key.mdx new file mode 100644 index 000000000..1f9b570d1 --- /dev/null +++ b/docs/api-reference/endpoints/ssh/ca/public-key.mdx @@ -0,0 +1,4 @@ +--- +title: "Retrieve public key" +openapi: "GET /api/v1/ssh/ca/{sshCaId}/public-key" +--- diff --git a/docs/api-reference/endpoints/ssh/ca/read.mdx b/docs/api-reference/endpoints/ssh/ca/read.mdx new file mode 100644 index 000000000..9f5eda90a --- /dev/null +++ b/docs/api-reference/endpoints/ssh/ca/read.mdx @@ -0,0 +1,4 @@ +--- +title: "Retrieve" +openapi: "GET /api/v1/ssh/ca/{sshCaId}" +--- diff --git a/docs/api-reference/endpoints/ssh/ca/update.mdx b/docs/api-reference/endpoints/ssh/ca/update.mdx new file mode 100644 index 000000000..8ec2dc7ad --- /dev/null +++ b/docs/api-reference/endpoints/ssh/ca/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/ssh/ca/{sshCaId}" +--- diff --git a/docs/api-reference/endpoints/ssh/certificate-templates/create.mdx b/docs/api-reference/endpoints/ssh/certificate-templates/create.mdx new file mode 100644 index 000000000..6e3beef1a --- /dev/null +++ b/docs/api-reference/endpoints/ssh/certificate-templates/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/ssh/certificate-templates" +--- diff --git a/docs/api-reference/endpoints/ssh/certificate-templates/delete.mdx b/docs/api-reference/endpoints/ssh/certificate-templates/delete.mdx new file mode 100644 index 000000000..1fa776276 --- /dev/null +++ b/docs/api-reference/endpoints/ssh/certificate-templates/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/ssh/certificate-templates/{certificateTemplateId}" +--- diff --git a/docs/api-reference/endpoints/ssh/certificate-templates/list.mdx b/docs/api-reference/endpoints/ssh/certificate-templates/list.mdx new file mode 100644 index 000000000..4331db1de --- /dev/null +++ b/docs/api-reference/endpoints/ssh/certificate-templates/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v2/workspace/{projectId}/ssh-certificate-templates" +--- diff --git a/docs/api-reference/endpoints/ssh/certificate-templates/read.mdx b/docs/api-reference/endpoints/ssh/certificate-templates/read.mdx new file mode 100644 index 000000000..13a356688 --- /dev/null +++ b/docs/api-reference/endpoints/ssh/certificate-templates/read.mdx @@ -0,0 +1,4 @@ +--- +title: "Retrieve" +openapi: "GET /api/v1/ssh/certificate-templates/{certificateTemplateId}" +--- diff --git a/docs/api-reference/endpoints/ssh/certificate-templates/update.mdx b/docs/api-reference/endpoints/ssh/certificate-templates/update.mdx new file mode 100644 index 000000000..f566d7535 --- /dev/null +++ b/docs/api-reference/endpoints/ssh/certificate-templates/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/ssh/certificate-templates/{certificateTemplateId}" +--- diff --git a/docs/api-reference/endpoints/ssh/certificates/issue-credentials.mdx b/docs/api-reference/endpoints/ssh/certificates/issue-credentials.mdx new file mode 100644 index 000000000..4a6da70b3 --- /dev/null +++ b/docs/api-reference/endpoints/ssh/certificates/issue-credentials.mdx @@ -0,0 +1,4 @@ +--- +title: "Issue SSH Credentials" +openapi: "POST /api/v1/ssh/certificates/issue" +--- diff --git a/docs/api-reference/endpoints/ssh/certificates/sign-key.mdx b/docs/api-reference/endpoints/ssh/certificates/sign-key.mdx new file mode 100644 index 000000000..0843b34a2 --- /dev/null +++ b/docs/api-reference/endpoints/ssh/certificates/sign-key.mdx @@ -0,0 +1,4 @@ +--- +title: "Sign SSH Public Key" +openapi: "POST /api/v1/ssh/certificates/sign" +--- diff --git a/docs/api-reference/overview/authentication.mdx b/docs/api-reference/overview/authentication.mdx index f2224577e..bdd7df83b 100644 --- a/docs/api-reference/overview/authentication.mdx +++ b/docs/api-reference/overview/authentication.mdx @@ -11,13 +11,6 @@ To interact with the Infisical API, you will need to obtain an access token. Fol **FAQ** - - The Service Token and API Key authentication modes are being deprecated out in favor of [Identities](/documentation/platform/identity). - We expect to make a deprecation notice in the coming months alongside a larger deprecation initiative planned for Q1/Q2 2024. - - With identities, we're improving significantly over the shortcomings of Service Tokens and API Keys. Amongst many differences, identities provide broader access over the Infisical API, utilizes the same role-based - permission system used by users, and comes with ample more configurable security measures. - There are a few reasons for why this might happen: diff --git a/docs/changelog/overview.mdx b/docs/changelog/overview.mdx index 9739fd9e9..6d1440ff1 100644 --- a/docs/changelog/overview.mdx +++ b/docs/changelog/overview.mdx @@ -4,6 +4,66 @@ title: "Changelog" The changelog below reflects new product developments and updates on a monthly basis. +## March 2025 + +- Released [Infisical Gateway](https://infisical.com/docs/documentation/platform/gateways/overview) for secure access to private resources without needing direct inbound connections to private networks. +- Enhanced [Terraform](https://infisical.com/docs/integrations/frameworks/terraform#terraform) capabilities with token authentication, ability to import existing Infisical secrets as resources, and support for project templates. +- Self-hosted improvements: Usage and billing visibility for enabled features, ability to delete users, and support for multiple super admins. +- UI and UX updates: Improved secret import interface on the overview page, password reset without backup PDF. +- CLI enhancements: Various improvements including multiline secret support and ability to pass headers. +- Kubernetes operator updates: Auto-reloading for DaemonSets and StatefulSets (previously only Deployments), added support for ConfigMaps. +- Implemented powerful [Access Control](https://infisical.com/docs/documentation/platform/access-controls/overview#access-controls) updates including \"**Grant Privileges**\" feature for designating specific users for policy management, **Access Tree** visualization for simulating permissions, and ability to restrict scope of secret sharing within organizations. +- Released new **Secret Requests** feature under Secret Share, added support for reminders with webhook triggers and implementing password policies for dynamic secrets. +- Enhanced secret version history to show who made changes. +- New integrations and syncs: **Crossplane** provider, **Humanitec** secret sync, **Airflow** system integration +- Performed significant performance optimizations including a 50% reduction in database usage and optimized client secret handling for universal auth. +- Enhanced security features with ability to add custom instance banners (useful for regulated industries), short-lived tokens for Kubernetes auth, and OIDC claim passing from machine identity login to permissions. +- [Golang SDK](https://infisical.com/docs/sdks/languages/go#infisical-go-sdk): New API added for enhanced functionality +- Added capability to programmatically configure an Infisical instance from start to finish without UI interaction. + +## February 2025 + +- Released [KMIP integration](https://infisical.com/docs/documentation/platform/kms/kmip) with PKI structure, auth model integration with machine identities, complete set of client operations, and client certificate authentication flow. +- Added new [AWS App Connection](https://infisical.com/docs/integrations/app-connections/aws) and [Secret Sync](https://infisical.com/docs/integrations/secret-syncs/aws-secrets-manager) functionality for enhanced AWS integration. +- Released new [Azure Key Vault App Connection](https://infisical.com/docs/integrations/app-connections/azure-key-vault) and [Secret Sync](https://infisical.com/docs/integrations/secret-syncs/azure-key-vault), plus Terraform provider support. +- Introduced more comprehensive logging with detailed records for secret sharing and metadata in audit logs. +- Introduced new [permission types](https://infisical.com/docs/internals/permissions/project-permissions#subject-secrets): \"View Value\" vs \"Describe Value\" for more granular access control over secrets. +- Updated encryption logic with unified approach for all platform data, ensuring consistency across the system. +- Added support for [OIDC group mapping](https://infisical.com/docs/documentation/platform/sso/general-oidc) to automatically map groups to Infisical for role-based access control. +- Added [Terraform Cloud support for OIDC](https://infisical.com/docs/documentation/platform/identities/oidc-auth/terraform-cloud#terraform-cloud). + +## January 2025 + +- Released new integration architecture with decoupled authentication, replacing native integrations with [App Connections](https://infisical.com/docs/integrations/app-connections/overview) and [Secret Syncs](https://infisical.com/docs/integrations/secret-syncs/overview). Initial support for AWS Parameter Store, GitHub, and GCP Secret Manager with improved API and Terraform integration capabilities. +- Added support for OIDC group mapping in [Keycloak](https://infisical.com/docs/documentation/platform/sso/keycloak-oidc/overview), enabling automatic mapping of Keycloak groups to Infisical for role-based access control. +- Enhanced [Kubernetes operator](https://infisical.com/docs/integrations/platforms/kubernetes/overview#kubernetes-operator) with namespaced group support, bi-directional secret sync (push to Infisical), [dynamic secrets](https://infisical.com/docs/documentation/platform/dynamic-secrets/overview#dynamic-secrets) capabilities, and support for multiple operator instances. +- Restructured navigation with dedicated sections for Secrets Management, [Certificate Management (PKI)](https://infisical.com/docs/documentation/platform/pki/overview), [Key Management (KMS)](https://infisical.com/docs/documentation/platform/kms/overview#key-management-service-kms), and [SSH Key Management](https://infisical.com/docs/documentation/platform/ssh). +- Added [ephemeral Terraform resource](https://infisical.com/docs/integrations/frameworks/terraform#terraform-provider) support and improved secret sync architecture. +- Released [.NET provider](https://github.com/Infisical/infisical-dotnet-configuration) with first-party Azure authentication support and Azure CLI integration. +- Implemented secret Access Visibility allowing users to view all entities with access to specific secrets in the secret side panel. +- Added secret filtering by metadata and SSH assigned certificates (Version 1). + +## December 2024 +- Added [GCP KMS](https://infisical.com/docs/documentation/platform/kms/overview) integration support. +- Added support for [K8s CSI integration](https://infisical.com/docs/integrations/platforms/kubernetes-csi) and ability to point K8s operator to specific secret versions. +- Fixed [Java SDK](https://github.com/Infisical/java-sdk) compatibility issues with Alpine Linux. +- Fixed SCIM group role assignment issues. +- Added Group View Page for improved team management. +- Added instance URL to email verification for Infisical accounts. +- Added ability to copy full path of nested folders. +- Added custom templating support for K8s operator, allowing flexible secret key mapping and additional fields. +- Optimized secrets versions table performance. + +## November 2024 +- Improved EnvKey migration functionality with support for Blocks, Inheritance, and Branches. +- Added [Hardware Security Module (HSM) Encryption](https://infisical.com/docs/documentation/platform/kms/hsm-integration) support. +- Updated permissions handling in [Infisical Terraform Provider](https://registry.terraform.io/providers/Infisical/infisical/latest/docs) to use lists instead of sets. +- Enhanced [SCIM](https://infisical.com/docs/documentation/platform/scim/overview) implementation to remove SAML dependency. +- Enhanced [OIDC Authentication](https://infisical.com/docs/documentation/platform/identities/oidc-auth/general) implementation and added Default Org Slug support. +- Added support for multiple authentication methods per identity. +- Added AWS Parameter Store integration sync improvements. +- Added new screen and API for managing additional privileges. +- Added Dynamic Secrets support for SQL Server. ## October 2024 - Significantly improved performance of audit log operations in UI. @@ -113,7 +173,7 @@ The changelog below reflects new product developments and updates on a monthly b - Replaced internal [Winston](https://github.com/winstonjs/winston) with [Pino](https://github.com/pinojs/pino) logging library with external logging to AWS CloudWatch - Added admin panel to self-hosting experience. -- Released [secret rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview) feature with preliminary support for rotating [SendGrid](https://infisical.com/docs/documentation/platform/secret-rotation/sendgrid), [PostgreSQL/CockroachDB](https://infisical.com/docs/documentation/platform/secret-rotation/postgres), and [MySQL/MariaDB](https://infisical.com/docs/documentation/platform/secret-rotation/mysql) credentials. +- Released [secret rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview) feature with preliminary support for rotating [SendGrid](https://infisical.com/docs/documentation/platform/secret-rotation/sendgrid), [PostgreSQL/CockroachDB](https://infisical.com/docs/documentation/platform/secret-rotation/postgres-credentials), and [MySQL/MariaDB](https://infisical.com/docs/documentation/platform/secret-rotation/mysql) credentials. - Released secret reminders feature. ## Oct 2023 diff --git a/docs/cli/commands/bootstrap.mdx b/docs/cli/commands/bootstrap.mdx new file mode 100644 index 000000000..77f8b38f1 --- /dev/null +++ b/docs/cli/commands/bootstrap.mdx @@ -0,0 +1,132 @@ +--- +title: "infisical bootstrap" +description: "Automate the initial setup of a new Infisical instance for headless deployment and infrastructure-as-code workflows" +--- + +```bash +infisical bootstrap --domain= --email= --password= --organization= +``` + +## Description + +The `infisical bootstrap` command is used when deploying Infisical in automated environments where manual UI setup is not feasible. It's ideal for: + +- Containerized deployments in Kubernetes or Docker environments +- Infrastructure-as-code pipelines with Terraform or similar tools +- Continuous deployment workflows +- DevOps automation scenarios + +The command initializes a fresh Infisical instance by creating an admin user, organization, and instance admin machine identity, enabling subsequent programmatic configuration without human intervention. + + + This command creates an instance admin machine identity with the highest level + of privileges. The returned token should be treated with the utmost security, + similar to a root credential. Unauthorized access to this token could + compromise your entire Infisical instance. + + +## Flags + + + The URL of your Infisical instance. This can be set using the `INFISICAL_API_URL` environment variable. + +```bash +# Example +infisical bootstrap --domain=https://your-infisical-instance.com +``` + +This flag is required. + + + + + Email address for the admin user account that will be created. This can be set using the `INFISICAL_ADMIN_EMAIL` environment variable. + +```bash +# Example +infisical bootstrap --email=admin@example.com +``` + +This flag is required. + + + + + Password for the admin user account. This can be set using the `INFISICAL_ADMIN_PASSWORD` environment variable. + +```bash +# Example +infisical bootstrap --password=your-secure-password +``` + +This flag is required. + + + + + Name of the organization that will be created within the instance. This can be set using the `INFISICAL_ADMIN_ORGANIZATION` environment variable. + +```bash +# Example +infisical bootstrap --organization=your-org-name +``` + +This flag is required. + + + +## Response + +The command returns a JSON response with details about the created user, organization, and machine identity: + +```json +{ + "identity": { + "credentials": { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eUlkIjoiZGIyMjQ3OTItZWQxOC00Mjc3LTlkYWUtNTdlNzUyMzE1ODU0IiwiaWRlbnRpdHlBY2Nlc3NUb2tlbklkIjoiZmVkZmZmMGEtYmU3Yy00NjViLWEwZWEtZjM5OTNjMTg4OGRlIiwiYXV0aFRva2VuVHlwZSI6ImlkZW50aXR5QWNjZXNzVG9rZW4iLCJpYXQiOjE3NDIzMjI0ODl9.mqcZZqIFqER1e9ubrQXp8FbzGYi8nqqZwfMvz09g-8Y" + }, + "id": "db224792-ed18-4277-9dae-57e752315854", + "name": "Instance Admin Identity" + }, + "message": "Successfully bootstrapped instance", + "organization": { + "id": "b56bece0-42f5-4262-b25e-be7bf5f84957", + "name": "dog", + "slug": "dog-v-e5l" + }, + "user": { + "email": "admin@example.com", + "firstName": "Admin", + "id": "a418f355-c8da-453c-bbc8-6c07208eeb3c", + "lastName": "User", + "superAdmin": true, + "username": "admin@example.com" + } +} +``` + +## Usage with Automation + +For automation purposes, you can extract just the machine identity token from the response: + +```bash +infisical bootstrap --domain=https://your-infisical-instance.com --email=admin@example.com --password=your-secure-password --organization=your-org-name | jq ".identity.credentials.token" +``` + +This extracts only the token, which can be captured in a variable or piped to other commands. + +## Example: Capture Token in a Variable + +```bash +TOKEN=$(infisical bootstrap --domain=https://your-infisical-instance.com --email=admin@example.com --password=your-secure-password --organization=your-org-name | jq -r ".identity.credentials.token") + +# Now use the token for further automation +echo "Token has been captured and can be used for authentication" +``` + +## Notes + +- The bootstrap process can only be performed once on a fresh Infisical instance +- All flags are required for the bootstrap process to complete successfully +- Security controls prevent privilege escalation: instance admin identities cannot be managed by non-instance admin users and identities +- The generated admin user account can be used to log in via the UI if needed diff --git a/docs/cli/commands/commands.mdx b/docs/cli/commands/commands.mdx index 78870defc..278f061b7 100644 --- a/docs/cli/commands/commands.mdx +++ b/docs/cli/commands/commands.mdx @@ -11,6 +11,7 @@ description: "Infisical CLI command overview" | `init` | Used to link a local project to the platform. | | `run` | Used to inject envars from the platform into an application process. | | `vault` | Used to manage where your login credentials are stored at rest | + ## Global options | Option | Description | diff --git a/docs/cli/commands/dynamic-secrets.mdx b/docs/cli/commands/dynamic-secrets.mdx new file mode 100644 index 000000000..c345c3e2d --- /dev/null +++ b/docs/cli/commands/dynamic-secrets.mdx @@ -0,0 +1,295 @@ +--- +title: "infisical dynamic-secrets" +description: "Perform dynamic secret operations directly with the CLI" +--- + +``` +infisical dynamic-secrets +``` + +## Description + +Dynamic secrets are unique secrets generated on demand based on the provided configuration settings. For more details, refer to [dynamics secrets section](/documentation/platform/dynamic-secrets/overview). + +This command enables you to perform list, lease, renew lease, and revoke lease operations on dynamic secrets within your Infisical project. + +### Sub-commands + + + Use this command to print out all of the dynamic secrets in your project. + +```bash +$ infisical dynamic-secrets +``` + +### Environment variables + + + Used to fetch dynamic secrets via a [machine identity](/documentation/platform/identities/machine-identities) instead of logged-in credentials. Simply, export this variable in the terminal before running this command. + +```bash +# Example +export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id= --client-secret= --silent --plain) # --plain flag will output only the token, so it can be fed to an environment variable. --silent will disable any update messages. +``` + + + + + Used to disable the check for new CLI versions. This can improve the time it takes to run this command. Recommended for production environments. + +To use, simply export this variable in the terminal before running this command. + +```bash +# Example +export INFISICAL_DISABLE_UPDATE_CHECK=true +``` + + + +### Flags + + + The project ID to fetch dynamic secrets from. This is required when using a machine identity to authenticate. + +```bash +# Example +infisical dynamic-secrets --projectId= +``` + + + + + The authenticated token to fetch dynamic secrets from. This is required when using a machine identity to authenticate. + +```bash +# Example +infisical dynamic-secrets --token= +``` + + + + + Used to select the environment name on which actions should be taken. Default + value: `dev` + + + + Use to select the project folder on which dynamic secrets will be accessed. + +```bash +# Example +infisical dynamic-secrets --path="/" --env=dev +``` + + + + + This command is used to create a new lease for a dynamic secret. + +```bash +$ infisical dynamic-secrets lease create +``` + +### Flags + + + Used to select the environment name on which actions should be taken. Default + value: `dev` + + + + The `--plain` flag will output dynamic secret lease credentials values without formatting, one per line. + Default value: `false` + +```bash +# Example +infisical dynamic-secrets lease create dynamic-secret-postgres --plain +``` + + + + + The `--path` flag indicates which project folder dynamic secrets will be injected from. + +```bash +# Example +infisical dynamic-secrets lease create --path="/" --env=dev +``` + + + + + The project ID of the dynamic secrets to lease from. This is required when using a machine identity to authenticate. + +```bash +# Example +infisical dynamic-secrets lease create --projectId= +``` + + + + + The authenticated token to create dynamic secret leases. This is required when using a machine identity to authenticate. + +```bash +# Example +infisical dynamic-secrets lease create --token= +``` + + + + + The lease lifetime. If not provided, the default TTL of the dynamic secret root credential will be used. + +```bash +# Example +infisical dynamic-secrets lease create --ttl= +``` + + + + + + This command is used to list leases for a dynamic secret. + +```bash +$ infisical dynamic-secrets lease list +``` + +### Flags + + + Used to select the environment name on which actions should be taken. Default + value: `dev` + + + + The `--path` flag indicates which project folder dynamic secrets will be injected from. + +```bash +# Example +infisical dynamic-secrets lease list --path="/" --env=dev +``` + + + + + The project ID of the dynamic secrets to list leases from. This is required when using a machine identity to authenticate. + +```bash +# Example +infisical dynamic-secrets lease list --projectId= +``` + + + + + The authenticated token to list dynamic secret leases. This is required when using a machine identity to authenticate. + +```bash +# Example +infisical dynamic-secrets lease list --token= +``` + + + + + + This command is used to renew a lease before it expires. + +```bash +$ infisical dynamic-secrets lease renew +``` + +### Flags + + + Used to select the environment name on which actions should be taken. Default + value: `dev` + + + + The `--path` flag indicates which project folder dynamic secrets will be renewed from. + +```bash +# Example +infisical dynamic-secrets lease renew --path="/" --env=dev +``` + + + + + The project ID of the dynamic secret's lease from. This is required when using a machine identity to authenticate. + +```bash +# Example +infisical dynamic-secrets lease renew --projectId= +``` + + + + + The authenticated token to create dynamic secret leases. This is required when using a machine identity to authenticate. + +```bash +# Example +infisical dynamic-secrets lease renew --token= +``` + + + + + The lease lifetime. If not provided, the default TTL of the dynamic secret root credential will be used. + +```bash +# Example +infisical dynamic-secrets lease renew --ttl= +``` + + + + + + This command is used to delete a lease. + +```bash +$ infisical dynamic-secrets lease delete +``` + +### Flags + + + Used to select the environment name on which actions should be taken. Default + value: `dev` + + + + The `--path` flag indicates which project folder dynamic secrets will be deleted from. + +```bash +# Example +infisical dynamic-secrets lease delete --path="/" --env=dev +``` + + + + + The project ID of the dynamic secret's lease from. This is required when using a machine identity to authenticate. + +```bash +# Example +infisical dynamic-secrets lease delete --projectId= +``` + + + + + The authenticated token to delete dynamic secret leases. This is required when using a machine identity to authenticate. + +```bash +# Example +infisical dynamic-secrets lease delete --token= +``` + + + diff --git a/docs/cli/commands/gateway.mdx b/docs/cli/commands/gateway.mdx new file mode 100644 index 000000000..fd035f1fd --- /dev/null +++ b/docs/cli/commands/gateway.mdx @@ -0,0 +1,107 @@ +--- +title: "infisical gateway" +description: "Run the Infisical gateway or manage its systemd service" +--- + + + + ```bash + infisical gateway --token= + ``` + + + ```bash + sudo infisical gateway install --token= --domain= + ``` + + + +## Description + +Run the Infisical gateway in the foreground or manage its systemd service installation. The gateway allows secure communication between your self-hosted Infisical instance and client applications. + +## Subcommands & flags + + + Run the Infisical gateway in the foreground. The gateway will connect to the relay service and maintain a persistent connection. + + ```bash + infisical gateway --token= --domain= + ``` + + ### Flags + + + The machine identity access token to authenticate with Infisical. + + ```bash + # Example + infisical gateway --token= + ``` + + You may also expose the token to the CLI by setting the environment variable `INFISICAL_TOKEN` before executing the gateway command. + + + + Domain of your self-hosted Infisical instance. + + ```bash + # Example + sudo infisical gateway install --domain=https://app.your-domain.com + ``` + + + + + Install and enable the gateway as a systemd service. This command must be run with sudo on Linux. + + ```bash + sudo infisical gateway install --token= --domain= + ``` + + ### Requirements + - Must be run on Linux + - Must be run with root/sudo privileges + - Requires systemd + + ### Flags + + + The machine identity access token to authenticate with Infisical. + + ```bash + # Example + sudo infisical gateway install --token= + ``` + + You may also expose the token to the CLI by setting the environment variable `INFISICAL_TOKEN` before executing the install command. + + + + Domain of your self-hosted Infisical instance. + + ```bash + # Example + sudo infisical gateway install --domain=https://app.your-domain.com + ``` + + + ### Service Details + The systemd service is installed with secure defaults: + - Service file: `/etc/systemd/system/infisical-gateway.service` + - Config file: `/etc/infisical/gateway.conf` + - Runs with restricted privileges: + - InaccessibleDirectories=/home + - PrivateTmp=yes + - Resource limits configured for stability + - Automatically restarts on failure + - Enabled to start on boot + + After installation, manage the service with standard systemd commands: + ```bash + sudo systemctl start infisical-gateway # Start the service + sudo systemctl stop infisical-gateway # Stop the service + sudo systemctl status infisical-gateway # Check service status + sudo systemctl disable infisical-gateway # Disable auto-start on boot + ``` + diff --git a/docs/cli/commands/secrets.mdx b/docs/cli/commands/secrets.mdx index 2dff5cf7b..1bb575ebd 100644 --- a/docs/cli/commands/secrets.mdx +++ b/docs/cli/commands/secrets.mdx @@ -186,12 +186,28 @@ This command allows you to set or update secrets in your environment. If the sec If the secret key does not exist, a new secret will be created using both the key and value provided. ```bash -$ infisical secrets set ... +$ infisical secrets set ... ## Example -$ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jebhfbwe +$ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jebhfbwe SECRET_PEM_KEY=@secret.pem ``` + + When setting secret values: + - Use `secretName=@path/to/file` to load the secret value from a file + - Use `secretName=\@value` if you need the literal '@' character at the beginning of your value + + Example: + + ```bash + # Set a secret with the value loaded from a certificate file + $ secrets set CERTIFICATE=@/path/to/certificate.pem + + # Set a secret with the literal value "@example.com" + $ secrets set email="\@example.com" + ``` + + ### Flags @@ -219,6 +235,21 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb ``` + + Used to set secrets from a file, supporting both `.env` and `YAML` formats. The file path can be either absolute or relative to the current working directory. + + The file should contain secrets in the following formats: + - `key=value` for `.env` files + - `key: value` for YAML files + + Comments can be written using `# comment` or `// comment`. Empty lines will be ignored during processing. + + + ```bash + # Example + infisical secrets set --file="./.env" + ``` + diff --git a/docs/cli/commands/ssh.mdx b/docs/cli/commands/ssh.mdx new file mode 100644 index 000000000..78712ba6f --- /dev/null +++ b/docs/cli/commands/ssh.mdx @@ -0,0 +1,116 @@ +--- +title: "infisical ssh" +description: "Generate SSH credentials with the CLI" +--- + +## Description + +[Infisical SSH](/documentation/platform/ssh) lets you issue SSH credentials to clients to provide short-lived, secure SSH access to infrastructure. + +This command enables you to obtain SSH credentials used to access a remote host; we recommend using the `issue-credentials` sub-command to generate dynamic SSH credentials for each SSH session. + +### Sub-commands + + + This command is used to issue SSH credentials (SSH certificate, public key, and private key) against a certificate template. + + We recommend using the `--addToAgent` flag to automatically load issued SSH credentials to the SSH agent. + + ```bash + $ infisical ssh issue-credentials --certificateTemplateId= --principals= --addToAgent + ``` + + ### Flags + + The ID of the SSH certificate template to issue SSH credentials for. + + + A comma-separated list of principals (i.e. usernames like `ec2-user` or hostnames) to issue SSH credentials for. + + + Whether to add issued SSH credentials to the SSH agent. + + Default value: `false` + + Note that either the `--outFilePath` or `--addToAgent` flag must be set for the sub-command to execute successfully. + + + The path to write the SSH credentials to such as `~/.ssh`, `./some_folder`, `./some_folder/id_rsa-cert.pub`. If not provided, the credentials will be saved to the current working directory where the command is run. + + Note that either the `--outFilePath` or `--addToAgent` flag must be set for the sub-command to execute successfully. + + + The key algorithm to issue SSH credentials for. + + Default value: `RSA_2048` + + Available options: `RSA_2048`, `RSA_4096`, `EC_prime256v1`, `EC_secp384r1`. + + + The certificate type to issue SSH credentials for. + + Default value: `user` + + Available options: `user` or `host` + + + The time-to-live (TTL) for the issued SSH certificate (e.g. `2 days`, `1d`, `2h`, `1y`). + + Defaults to the Default TTL value set in the certificate template. + + + A custom Key ID to issue SSH credentials for. + + Defaults to the autogenerated Key ID by Infisical. + + + An authenticated token to use to issue SSH credentials. + + + + + This command is used to sign an existing SSH public key against a certificate template; the command outputs the corresponding signed SSH certificate. + + ```bash + $ infisical ssh sign-key --certificateTemplateId= --publicKey= --principals= --outFilePath= + ``` + + The ID of the SSH certificate template to issue the SSH certificate for. + + + The public key to sign. + + Note that either the `--publicKey` or `--publicKeyFilePath` flag must be set for the sub-command to execute successfully. + + + The path to the public key file to sign. + + Note that either the `--publicKey` or `--publicKeyFilePath` flag must be set for the sub-command to execute successfully. + + + A comma-separated list of principals (i.e. usernames like `ec2-user` or hostnames) to issue SSH credentials for. + + + The path to write the SSH certificate to such as `~/.ssh/id_rsa-cert.pub`; the specified file must have the `.pub` extension. If not provided, the credentials will be saved to the directory of the specified `--publicKeyFilePath` or the current working directory where the command is run. + + + The certificate type to issue SSH credentials for. + + Default value: `user` + + Available options: `user` or `host` + + + The time-to-live (TTL) for the issued SSH certificate (e.g. `2 days`, `1d`, `2h`, `1y`). + + Defaults to the Default TTL value set in the certificate template. + + + A custom Key ID to issue SSH credentials for. + + Defaults to the autogenerated Key ID by Infisical. + + + An authenticated token to use to issue SSH credentials. + + \ No newline at end of file diff --git a/docs/cli/faq.mdx b/docs/cli/faq.mdx index 47e89a48f..02c539e61 100644 --- a/docs/cli/faq.mdx +++ b/docs/cli/faq.mdx @@ -33,3 +33,28 @@ Yes. This is simply a configuration file and contains no sensitive data. https://app.infisical.com/project//settings ``` + + + + The Infisical CLI supports custom HTTP headers for requests to servers that require additional authentication. Set these headers using the `INFISICAL_CUSTOM_HEADERS` environment variable: + + ```bash + export INFISICAL_CUSTOM_HEADERS="Access-Client-Id=your-client-id Access-Client-Secret=your-client-secret" + ``` + + After setting this environment variable, run your Infisical commands as usual. + + + + + Custom headers are necessary when your Infisical server is protected by services like Cloudflare Access or other reverse proxies that require specific authentication headers. Without this feature, you would need to implement security workarounds that might compromise your security posture. + + + + + Custom headers should be specified in the format `headername1=headervalue1 headername2=headervalue2`, with spaces separating each header-value pair. For example: + + ```bash + export INFISICAL_CUSTOM_HEADERS="Header1=value1 Header2=value2 Header3=value3" + ``` + \ No newline at end of file diff --git a/docs/cli/overview.mdx b/docs/cli/overview.mdx index ab913ec1a..e0cbbe387 100644 --- a/docs/cli/overview.mdx +++ b/docs/cli/overview.mdx @@ -1,15 +1,20 @@ --- title: 'Install' -description: "Infisical's CLI is one of the best way to manage environments and secrets. Install it here" +description: "Infisical's CLI is one of the best ways to manage environments and secrets. Install it here" --- -The Infisical CLI is powerful command line tool that can be used to retrieve, modify, export and inject secrets into any process or application as environment variables. +The Infisical CLI is a powerful command line tool that can be used to retrieve, modify, export and inject secrets into any process or application as environment variables. You can use it across various environments, whether it's local development, CI/CD, staging, or production. ## Installation + + As of 04/08/25, all future releases for Debian/Ubuntu will be distributed via the official Infisical repository at https://artifacts-cli.infisical.com. + No new releases will be published for Debian/Ubuntu on Cloudsmith going forward. + + - + Use [brew](https://brew.sh/) package manager ```bash @@ -21,26 +26,49 @@ You can use it across various environments, whether it's local development, CI/C ```bash brew update && brew upgrade infisical ``` + + - - - Use [Scoop](https://scoop.sh/) package manager + + Use [Scoop](https://scoop.sh/) package manager - ```bash - scoop bucket add org https://github.com/Infisical/scoop-infisical.git - ``` + ```bash + scoop bucket add org https://github.com/Infisical/scoop-infisical.git + ``` - ```bash - scoop install infisical - ``` + ```bash + scoop install infisical + ``` - ### Updates + ### Updates - ```bash - scoop update infisical - ``` + ```bash + scoop update infisical + ``` + - + + Use [Winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/) package manager + + ```bash + winget install infisical + ``` + + + + + Use [NPM](https://www.npmjs.com/) package manager + + ```bash + npm install -g @infisical/cli + ``` + + ### Updates + + ```bash + npm update -g @infisical/cli + ``` + Install prerequisite ```bash @@ -81,11 +109,12 @@ You can use it across various environments, whether it's local development, CI/C + Add Infisical repository ```bash curl -1sLf \ - 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' \ + 'https://artifacts-cli.infisical.com/setup.deb.sh' \ | sudo -E bash ``` @@ -115,4 +144,4 @@ You can use it across various environments, whether it's local development, CI/C ## Quick Usage Guide Now that you have the CLI installed on your system, follow this guide to make the best use of it - \ No newline at end of file + diff --git a/docs/cli/usage.mdx b/docs/cli/usage.mdx index d5b7acb4a..a77a648d0 100644 --- a/docs/cli/usage.mdx +++ b/docs/cli/usage.mdx @@ -120,6 +120,22 @@ The CLI is designed for a variety of secret management applications ranging from + + ## Custom Request Headers + + The Infisical CLI supports custom HTTP headers for requests to servers protected by authentication services such as Cloudflare Access. Configure these headers using the `INFISICAL_CUSTOM_HEADERS` environment variable: + + ```bash + # Syntax: headername1=headervalue1 headername2=headervalue2 + export INFISICAL_CUSTOM_HEADERS="Access-Client-Id=your-client-id Access-Client-Secret=your-client-secret" + + # Execute Infisical commands after setting the environment variable + infisical secrets ls + ``` + + This functionality enables secure interaction with Infisical instances that require specific authentication headers. + + ## History Your terminal keeps a history with the commands you run. When you create Infisical secrets directly from your terminal, they'll stay there for a while. diff --git a/docs/contributing/platform/backend/how-to-create-a-feature.mdx b/docs/contributing/platform/backend/how-to-create-a-feature.mdx index 8449eb501..f02040cfa 100644 --- a/docs/contributing/platform/backend/how-to-create-a-feature.mdx +++ b/docs/contributing/platform/backend/how-to-create-a-feature.mdx @@ -4,9 +4,6 @@ title: "Backend development guide" Suppose you're interested in implementing a new feature in Infisical's backend, let's call it "feature-x." Here are the general steps you should follow. -## Database schema migration -In order to run [schema migrations](https://en.wikipedia.org/wiki/Schema_migration#:~:text=A%20schema%20migration%20is%20performed,some%20newer%20or%20older%20version) you need to expose your database connection string. Create a `.env.migration` file to set the database connection URI for migration scripts, or alternatively, export the `DB_CONNECTION_URI` environment variable. - ## Creating new database model If your feature involves a change in the database, you need to first address this by generating the necessary database schemas. diff --git a/docs/documentation/guides/local-development.mdx b/docs/documentation/guides/local-development.mdx index 6d606bafe..2d4c21567 100644 --- a/docs/documentation/guides/local-development.mdx +++ b/docs/documentation/guides/local-development.mdx @@ -6,7 +6,7 @@ description: "Learn how to manage secrets in local development environments." ## Problem at hand -There is a number of issues that arise with secret management in local development environment: +There are a number of issues that arise with secret management in local development environment: 1. **Getting secrets onto local machines**. When new developers join or a new project is created, the process of getting the development set of secrets onto local machines is often unclear. As a result, developers end up spending a lot of time onboarding and risk potentially following insecure practices when sharing secrets from one developer to another. 2. **Syncing secrets with teammates**. One of the problems with .env files is that they become unsynced when one of the developers updates a secret or configuration. Even if the rest of the team is notified, developers don't make all the right changes immediately, and later on end up spending a lot of time debugging an issue due to missing environment variables. This leads to a lot of inefficiencies and lost time. 3. **Accidentally leaking secrets**. When developing locally, it's common for developers to accidentally leak a hardcoded secret as part of a commit. As soon as the secret is part of the git history, it becomes hard to get it removed and create a security vulnerability. diff --git a/docs/documentation/guides/node.mdx b/docs/documentation/guides/node.mdx index d1b8fe5e8..9da99441a 100644 --- a/docs/documentation/guides/node.mdx +++ b/docs/documentation/guides/node.mdx @@ -5,7 +5,7 @@ title: "Node" This guide demonstrates how to use Infisical to manage secrets for your Node stack from local development to production. It uses: - Infisical (you can use [Infisical Cloud](https://app.infisical.com) or a [self-hosted instance of Infisical](https://infisical.com/docs/self-hosting/overview)) to store your secrets. -- The [@infisical/sdk](https://github.com/Infisical/sdk/tree/main/languages/node) Node.js client SDK to fetch secrets back to your Node application on demand. +- The [@infisical/sdk](https://github.com/Infisical/node-sdk-v2) Node.js client SDK to fetch secrets back to your Node application on demand. ## Project Setup @@ -46,43 +46,57 @@ Finally, create an index.js file containing the application code. ```js const express = require('express'); -const { InfisicalClient } = require("@infisical/sdk"); +const { InfisicalSDK } = require("@infisical/sdk"); + const app = express(); const PORT = 3000; -const client = new InfisicalClient({ - auth: { - universalAuth: { - clientId: "YOUR_CLIENT_ID", - clientSecret: "YOUR_CLIENT_SECRET", - } - } -}); +let client; + +const setupClient = () => { + + if (client) { + return; + } + + const infisicalSdk = new InfisicalSDK({ + siteUrl: "your-infisical-instance.com" // Optional, defaults to https://app.infisical.com + }); + + await infisicalSdk.auth().universalAuth.login({ + clientId: "", + clientSecret: "" + }); + + // If authentication was successful, assign the client + client = infisicalSdk; +} + + app.get("/", async (req, res) => { - // access value + - const name = await client.getSecret({ - environment: "dev", - projectId: "PROJECT_ID", - path: "/", - type: "shared", - secretName: "NAME" + const name = await client.secrets().getSecret({ + environment: "dev", // dev, staging, prod, etc. + projectId: "", + secretPath: "/", + secretName: "NAME" }); - + res.send(`Hello! My name is: ${name.secretValue}`); }); app.listen(PORT, async () => { - // initialize client - - console.log(`App listening on port ${PORT}`); + // initialize http server and Infisical + await setupClient(); + console.log(`Server listening on port ${PORT}`); }); ``` -Here, we initialized a `client` instance of the Infisical Node SDK with the Infisical Token +Here, we initialized a `client` instance of the Infisical Node SDK with the [Machine Identity](/documentation/platform/identities/overview) that we created earlier, giving access to the secrets in the development environment of the project in Infisical that we created earlier. @@ -94,16 +108,12 @@ node index.js The client fetched the secret with the key `NAME` from Infisical that we returned in the response of the endpoint. -At this stage, you know how to fetch secrets from Infisical back to your Node application. By using Infisical Tokens scoped to different environments, you can easily manage secrets across various stages of your project in Infisical, from local development to production. +At this stage, you know how to fetch secrets from Infisical back to your Node application. +By using Machine Identities scoped to different projects and environments, you can easily manage secrets across various stages of your project in Infisical, from local development to production. ## FAQ - - The client SDK caches every secret and implements a 5-minute waiting period before - re-requesting it. The waiting period can be controlled by setting the `cacheTTL` parameter at - the time of initializing the client. - The SDK caches every secret and falls back to the cached value if a request fails. If no cached value ever-existed, the SDK falls back to whatever value is on `process.env`. @@ -124,4 +134,4 @@ At this stage, you know how to fetch secrets from Infisical back to your Node ap See also: -- Explore the [Node SDK](https://github.com/Infisical/sdk/tree/main/languages/node) +- Explore the [Node SDK](https://github.com/Infisical/node-sdk-v2) diff --git a/docs/documentation/guides/organization-structure.mdx b/docs/documentation/guides/organization-structure.mdx index 6fd672164..3cba64678 100644 --- a/docs/documentation/guides/organization-structure.mdx +++ b/docs/documentation/guides/organization-structure.mdx @@ -6,40 +6,55 @@ description: "Learn how to structure your projects, secrets, and other resources Infisical is designed to provide comprehensive, centralized, and efficient management of secrets, certificates, and encryption keys within organizations. Below is an overview of Infisical's structured components, which developers and administrators can leverage for optimal project management and security posture. -### 1. Projects +### 0. Cluster/Instance + +- **Best Practice**: In most cases, a single Infisical instance or cluster is sufficient. Multiple clusters are typically only necessary for large, globally distributed organizations. +- **Use Cases**: + - **Cloud-hosted** deployments typically use a single cluster. While technically possible, using multiple clusters is not a common practice and is generally unnecessary. + - **Self-hosted** deployments can be configured with multiple clusters if needed. + + +### 1. Organization + +- **Definition**: An Infisical [organization](/documentation/platform/organization) is a set of projects that use the same billing. +- **Use Cases**: + - In **self-hosted** setups, you can create multiple organizations (e.g., one for each department or business unit). + - In **cloud-hosted deployments**, it's standard to use a single organization. + +### 2. Projects - **Definition and Role**: [Projects](/documentation/platform/project) are the highest-level construct within an [organization](/documentation/platform/organization) in Infisical. They serve as the primary container for all functionalities. - **Correspondence to Code Repositories**: Projects typically align with specific code repositories. - **Functional Capabilities**: Each project encompasses features for managing secrets, certificates, and encryption keys, serving as the central hub for these resources. -### 2. Environments +### 3. Environments - **Purpose**: Environments are designed for organizing and compartmentalizing secrets within projects. - **Customization Options**: Environments can be tailored to align with existing infrastructure setups of any project. Default options include **Development**, **Staging**, and **Production**. - **Structure**: Each environment inherently has a root level for storing secrets, but additional sub-organizations can be created through [folders](/documentation/platform/folder) for better secret management. -### 3. Folders +### 4. Folders - **Use Case**: Folders are available for more advanced organizational needs, allowing logical separation of secrets. - **Typical Structure**: Folders can correspond to specific logical units, such as microservices or different layers of an application, providing refined control over secrets. -### 4. Imports +### 5. Imports - **Purpose and Benefits**: To promote reusability and avoid redundancy, Infisical supports the use of imports. This allows secrets, folders, or entire environments to be referenced across multiple projects as needed. - **Best Practice**: Utilizing [secret imports](/documentation/platform/secret-reference#secret-imports) or [references](/documentation/platform/secret-reference#secret-referencing) ensures consistency and minimizes manual overhead. -### 5. Approval Workflows +### 6. Approval Workflows - **Importance**: Implementing approval workflows is recommended for organizations aiming to enhance efficiency and strengthen their security posture. - **Types of Workflows**: - **[Access Requests](/documentation/platform/pr-workflows)**: This workflow allows developers to request access to sensitive resources. Such access can be configured for temporary use, a practice known as "just-in-time" access. - **[Change Requests](/documentation/platform/access-controls/access-requests)**: Facilitates reviews and approvals when changes are proposed for sensitive environments or specific folders, ensuring proper oversight. -### 6. Access Controls +### 7. Access Controls Infisical’s access control framework is unified for both human users and machine identities, ensuring consistent management across the board. -### 6.1 Roles +### 7.1 Roles - **2 Role Types**: - **Organization-Level Roles**: Provide broad access across the organization (e.g., ability to manage billing, configure settings, etc.). @@ -49,17 +64,17 @@ Infisical’s access control framework is unified for both human users and machi Project access is defined not via an organization-level role, but rather through specific project memberships of both human and machine identities. Admin roles bypass this by default. -### 6.2 Additional Privileges +### 7.2 Additional Privileges [Additional privileges](/documentation/platform/access-controls/additional-privileges) can be assigned to users and machines on an ad-hoc basis for specific scenarios where roles alone are insufficient. If you find yourself using additional privileges too much, it is recommended to create custom roles. Additional privileges can be temporary or permanent. -### 6.3 Attribute-Based Access Control (ABAC) +### 7.3 Attribute-Based Access Control (ABAC) [Attribute-based Access Controls](/documentation/platform/access-controls/attribute-based-access-controls) allow restrictions based on tags or attributes linked to secrets. These can be integrated with SAML assertions and other security frameworks for dynamic access management. -### 6.4 User Groups +### 7.4 User Groups - **Application**: Organizations should use users groups in situations when they have a lot of developers with the same level of access (e.g., separated by team, department, seniority, etc.). - **Synchronization**: [User groups](/documentation/platform/groups) can be synced with an identity provider to maintain consistency and reduce manual management. diff --git a/docs/documentation/guides/python.mdx b/docs/documentation/guides/python.mdx index 00b3d6089..113055fca 100644 --- a/docs/documentation/guides/python.mdx +++ b/docs/documentation/guides/python.mdx @@ -5,7 +5,7 @@ title: "Python" This guide demonstrates how to use Infisical to manage secrets for your Python stack from local development to production. It uses: - Infisical (you can use [Infisical Cloud](https://app.infisical.com) or a [self-hosted instance of Infisical](https://infisical.com/docs/self-hosting/overview)) to store your secrets. -- The [infisical-python](https://pypi.org/project/infisical-python/) Python client SDK to fetch secrets back to your Python application on demand. +- The [infisicalsdk](https://pypi.org/project/infisicalsdk/) Python client SDK to fetch secrets back to your Python application on demand. ## Project Setup @@ -36,40 +36,38 @@ python3 -m venv env source env/bin/activate ``` -Install Flask and [infisical-python](https://pypi.org/project/infisical-python/), the client Python SDK for Infisical. +Install Flask and [infisicalsdk](https://pypi.org/project/infisicalsdk/), the client Python SDK for Infisical. ```console -pip install flask infisical-python +pip install flask infisicalsdk ``` Finally, create an `app.py` file containing the application code. ```py from flask import Flask -from infisical_client import ClientSettings, InfisicalClient, GetSecretOptions, AuthenticationOptions, UniversalAuthMethod +from infisical_sdk import InfisicalSDKClient app = Flask(__name__) -client = InfisicalClient(ClientSettings( - auth=AuthenticationOptions( - universal_auth=UniversalAuthMethod( - client_id="CLIENT_ID", - client_secret="CLIENT_SECRET", - ) - ) -)) +client = InfisicalSDKClient(host="https://app.infisical.com") # host is optional, defaults to https://app.infisical.com + +client.auth.universal_auth.login( + "", + "" +) @app.route("/") def hello_world(): # access value + name = client.secrets.get_secret_by_name( + secret_name="NAME", + project_id="", + environment_slug="dev", + secret_path="/" + ) - name = client.getSecret(options=GetSecretOptions( - environment="dev", - project_id="PROJECT_ID", - secret_name="NAME" - )) - - return f"Hello! My name is: {name.secret_value}" + return f"Hello! My name is: {name.secretValue}" ``` Here, we initialized a `client` instance of the Infisical Python SDK with the Infisical Token @@ -89,15 +87,6 @@ At this stage, you know how to fetch secrets from Infisical back to your Python ## FAQ - - The client SDK caches every secret and implements a 5-minute waiting period before - re-requesting it. The waiting period can be controlled by setting the `cacheTTL` parameter at - the time of initializing the client. - - - The SDK caches every secret and falls back to the cached value if a request fails. If no cached - value ever-existed, the SDK falls back to whatever value is on `process.env`. - The token enables the SDK to authenticate with Infisical to fetch back your secrets. Although the SDK requires you to pass in a token, it enables greater efficiency and security @@ -114,6 +103,6 @@ At this stage, you know how to fetch secrets from Infisical back to your Python See also: -- Explore the [Python SDK](https://github.com/Infisical/sdk/tree/main/crates/infisical-py) +- Explore the [Python SDK](https://github.com/Infisical/python-sdk-official) diff --git a/docs/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-1.png b/docs/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-1.png new file mode 100644 index 000000000..e84a258e2 Binary files /dev/null and b/docs/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-1.png differ diff --git a/docs/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-2.png b/docs/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-2.png new file mode 100644 index 000000000..824aabc25 Binary files /dev/null and b/docs/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-2.png differ diff --git a/docs/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-3.png b/docs/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-3.png new file mode 100644 index 000000000..88d9a3da2 Binary files /dev/null and b/docs/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-3.png differ diff --git a/docs/documentation/platform/access-controls/abac/managing-machine-identity-attributes.mdx b/docs/documentation/platform/access-controls/abac/managing-machine-identity-attributes.mdx new file mode 100644 index 000000000..5e1cc7093 --- /dev/null +++ b/docs/documentation/platform/access-controls/abac/managing-machine-identity-attributes.mdx @@ -0,0 +1,68 @@ +--- +title: "Machine identities" +description: "Learn how to set metadata and leverage authentication attributes for machine identities." +--- + +Machine identities can have metadata set manually, just like users. In addition, during the machine authentication process (e.g., via OIDC), extra attributes called claims—are provided, which can be used in your ABAC policies. + +#### Setting Metadata on Machine Identities + + + + + + + + + + + + + + + + + +#### Accessing Attributes From Machine Identity Login + +When machine identities authenticate, they may receive additional payloads/attributes from the service provider. +For methods like OIDC, these come as claims in the token and can be made available in your policies. + + + + 1. Navigate to the Identity Authentication settings and select the OIDC Auth Method. + 2. In the **Advanced section**, locate the Claim Mapping configuration. + 3. Map the OIDC claims to permission attributes by specifying: + - **Attribute Name:** The identifier to be used in your policies (e.g., department). + - **Claim Path:** The dot notation path to the claim in the OIDC token (e.g., user.department). + + For example, if your OIDC provider returns: + + ```json + { + "sub": "machine456", + "name": "Service A", + "user": { + "department": "engineering", + "role": "service" + } + } + ``` + + You might map: + + - **department:** to `user.department` + - **role:** to `user.role` + + Once configured, these attributes become available in your policies using the following format: + + ``` + {{ identity.auth.oidc.claims. }} + ``` + + + + + At the moment we only support OIDC claims. Payloads on other authentication methods are not yet accessible. + + diff --git a/docs/documentation/platform/access-controls/abac/managing-user-metadata.mdx b/docs/documentation/platform/access-controls/abac/managing-user-metadata.mdx new file mode 100644 index 000000000..3f62a3b61 --- /dev/null +++ b/docs/documentation/platform/access-controls/abac/managing-user-metadata.mdx @@ -0,0 +1,39 @@ +--- +title: "Users identities" +description: "How to set and use metadata attributes on user identities for ABAC." +--- + +User identities can have metadata attributes assigned directly. These attributes (such as location or department) are used to define dynamic access policies. + +#### Setting Metadata on Users + + + + + + + + + + + + + + + + + For organizations using SAML for **user logins**, Infisical automatically maps metadata attributes from SAML assertions to user identities on every login. This enables dynamic policies based on the user's SAML attributes. + + + +#### Applying ABAC Policies with User Metadata +Attribute-based access controls are currently only available for polices defined on Secrets Manager projects. +You can set ABAC permissions to dynamically set access to environments, folders, secrets, and secret tags. + + + +In your policies, metadata values are accessed as follows: + +- **User ID:** `{{ identity.id }}` (always available) +- **Username:** `{{ identity.username }}` (always available) +- **Metadata Attributes:** `{{ identity.metadata. }}` (available if set) diff --git a/docs/documentation/platform/access-controls/abac/overview.mdx b/docs/documentation/platform/access-controls/abac/overview.mdx new file mode 100644 index 000000000..0a14719f6 --- /dev/null +++ b/docs/documentation/platform/access-controls/abac/overview.mdx @@ -0,0 +1,15 @@ +--- +title: "Overview" +description: "Learn the basics of ABAC for both users and machine identities." +--- + +Infisical's Attribute-based Access Controls (ABAC) enable dynamic, attribute-driven permissions for both users and machine identities. ABAC enforces fine-grained, context-aware access controls using metadata attributes—stored as key-value pairs—either attached to identities or provided during authentication. + + + + Manage user metadata manually or automatically via SAML logins. + + + Set metadata manually like users and access additional attributes provided during machine authentication (for example, OIDC claims). + + \ No newline at end of file diff --git a/docs/documentation/platform/access-controls/attribute-based-access-controls.mdx b/docs/documentation/platform/access-controls/attribute-based-access-controls.mdx deleted file mode 100644 index 99c49c63a..000000000 --- a/docs/documentation/platform/access-controls/attribute-based-access-controls.mdx +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: "Attribute-based Access Controls" -description: "Learn how to use ABAC to manage permissions based on identity attributes." ---- - -Infisical's Attribute-based Access Controls (ABAC) allow for dynamic, attribute-driven permissions for both user and machine identities. -ABAC policies use metadata attributes—stored as key-value pairs on identities—to enforce fine-grained permissions that are context aware. - -In ABAC, access controls are defined using metadata attributes, such as location or department, which can be set directly on user or machine identities. -During policy execution, these attributes are evaluated, and determine whether said actor can access the requested resource or perform the requested operation. - -## Project-level Permissions - -Attribute-based access controls are currently available for polices defined on projects. You can set ABAC permissions to control access to environments, folders, secrets, and secret tags. - -### Setting Metadata on Identities - - - - - - - - - - - - - - - - - For organizations using SAML for login, Infisical automatically maps metadata attributes from SAML assertions to user identities. - This makes it easy to create policies that dynamically adapt based on the SAML user’s attributes. - - - - -## Defining ABAC Policies - - - -ABAC policies make use of identity metadata to define dynamic permissions. Each attribute must start and end with double curly-brackets `{{ }}`. -The following attributes are available within project permissions: - -- **User ID**: `{{ identity.id }}` -- **Username**: `{{ identity.username }}` -- **Metadata Attributes**: `{{ identity.metadata. }}` - -During policy execution, these placeholders are replaced by their actual values prior to evaluation. - -### Example Use Case - -#### Location-based Access Control - -Suppose you want to restrict access to secrets within a specific folder based on a user's geographic region. -You could assign a `location` attribute to each user (e.g., `identity.metadata.location`). -You could then structure your folders to align with this attribute and define permissions accordingly. - -For example, a policy might restrict access to folders matching the user's location attribute in the following pattern: -``` -/appA/{{ identity.metadata.location }} -``` -Using this structure, users can only access folders that correspond to their configured `location` attribute. -Consequently, if a users attribute changes due to relocation, no policies need to be changed to gain access to the folders associated with their new location. diff --git a/docs/documentation/platform/access-controls/overview.mdx b/docs/documentation/platform/access-controls/overview.mdx index 552117c7e..0afaaac54 100644 --- a/docs/documentation/platform/access-controls/overview.mdx +++ b/docs/documentation/platform/access-controls/overview.mdx @@ -18,7 +18,7 @@ To make sure that users and machine identities are only accessing the resources diff --git a/docs/documentation/platform/access-controls/project-access-requests.mdx b/docs/documentation/platform/access-controls/project-access-requests.mdx new file mode 100644 index 000000000..38812c003 --- /dev/null +++ b/docs/documentation/platform/access-controls/project-access-requests.mdx @@ -0,0 +1,36 @@ +--- +title: "Project Access Requests" +description: "Learn how to request access to projects in Infisical." +--- + +The Project Access Request feature allows users to view all projects within organization, including those they don't currently have access to. +Users can request access to these projects by submitting a request that automatically notifies project administrators via email, along with any comments provided by the user. + +# Viewing Available Projects + +From the Infisical dashboard, users can view all projects within the organization: + +1. Navigate to the main dashboard after logging in +2. The overview page for each product displays two tabs: + + - **My Projects**: Projects you currently have access to + - **All Projects**: Complete list of projects in the organization + +![all-project-view](/images/platform/project-access-requests/all-project-view.png) + +# Requesting Access to a Project + +To request access to a project you don't currently have access for: + +1. Click the **Request Access** button next to the project name + ![all-project-view](/images/platform/project-access-requests/request-access.png) + +2. Add a comment explaining why you need access + ![all-project-view](/images/platform/project-access-requests/access-comment.png) + +3. Click **Submit Request** + + + Project administrators will receive email notification with details regarding + the access request. + diff --git a/docs/documentation/platform/access-controls/role-based-access-controls.mdx b/docs/documentation/platform/access-controls/role-based-access-controls.mdx index 98a2e4659..341c20868 100644 --- a/docs/documentation/platform/access-controls/role-based-access-controls.mdx +++ b/docs/documentation/platform/access-controls/role-based-access-controls.mdx @@ -3,7 +3,7 @@ title: "Role-based Access Controls" description: "Learn how to use RBAC to manage user permissions." --- -Infisical's Role-based Access Controls (RBAC) enable the usage of predefined and custom roles that imply a set of permissions for user and machine identities. Such roles male it possible to restrict access to resources and the range of actions that can be performed. +Infisical's Role-based Access Controls (RBAC) enable the usage of predefined and custom roles that imply a set of permissions for user and machine identities. Such roles make it possible to restrict access to resources and the range of actions that can be performed. In general, access controls can be split up across [projects](/documentation/platform/project) and [organizations](/documentation/platform/organization). @@ -25,7 +25,7 @@ By default, every user in a project is either a **viewer**, **developer**, or an As such: - **Admin**: This role enables identities to have access to all environments, folders, secrets, and actions within the project. -- **Developers**: This role restricts identities from performing project control actions, updating Approval Workflow policies, managing roles/members, and more. +- **Developers**: This role restricts identities from performing project control actions, updating Approval Workflow policies, managing roles, editing and removing project members, and more. - **Viewer**: The most limiting bulit-in role on the project level – it forbids user and machine identities to perform any action and rather shows them in the read-only mode. ![Project member role](/images/platform/access-controls/rbac.png) diff --git a/docs/documentation/platform/admin-panel/org-admin-console.mdx b/docs/documentation/platform/admin-panel/org-admin-console.mdx index 39d7819a4..08a327268 100644 --- a/docs/documentation/platform/admin-panel/org-admin-console.mdx +++ b/docs/documentation/platform/admin-panel/org-admin-console.mdx @@ -4,13 +4,13 @@ description: "View and manage resources across your organization" --- - The Organization Admin Console can only be accessed by organization members with admin status. + The Organization Admin Console can only be accessed by organization members + with admin status. - ## Accessing the Organization Admin Console -On the sidebar, tap on your initials to access the settings dropdown and press the **Organization Admin Console** option. +On the sidebar, hover over **Admin** to access the settings dropdown and press the **Organization Admin Console** option. ![Access Organization Admin Console](/images/platform/admin-panels/access-org-admin-console.png) @@ -20,12 +20,9 @@ The Projects tab lists all the projects within your organization, including thos ![Projects Section](/images/platform/admin-panels/org-admin-console-projects.png) - ### Accessing a Project in Your Organization You can access a project that you are not a member of by tapping on the options menu of the project row and pressing the **Access** button. Doing so will grant you admin permissions for the selected project and add you as a member. ![Access project](/images/platform/admin-panels/org-admin-console-access.png) - - diff --git a/docs/documentation/platform/admin-panel/server-admin.mdx b/docs/documentation/platform/admin-panel/server-admin.mdx index ddcf448a6..198e5c37c 100644 --- a/docs/documentation/platform/admin-panel/server-admin.mdx +++ b/docs/documentation/platform/admin-panel/server-admin.mdx @@ -7,21 +7,22 @@ The Server Admin Console provides **server administrators** with the ability to customize settings and manage users for their entire Infisical instance. - The first user to setup an account on your Infisical instance is designated as the server administrator by default. + The first user to setup an account on your Infisical instance is designated as + the server administrator by default. ## Accessing the Server Admin Console - -On the sidebar, tap on your initials to access the settings dropdown and press the **Server Admin Console** option. +On the sidebar, hover over **Admin** to access the settings dropdown and press the **Server Admin Console** option. ![Access Server Admin Console](/images/platform/admin-panels/access-server-admin-panel.png) ## General Tab + Configure general settings for your instance. ![General Settings](/images/platform/admin-panels/admin-panel-general.png) - +![General Settings 1](/images/platform/admin-panels/admin-panel-general-1.png) ### Allow User Signups @@ -39,6 +40,22 @@ If you're using SAML/LDAP/OIDC for only one organization on your instance, you c By default, users signing up through SAML/LDAP/OIDC will still need to verify their email address to prevent email spoofing. This requirement can be skipped by enabling the switch to trust logins through the respective method. +### Broadcast Messages + +Auth consent content is displayed to users on the login page. They can be used to display important information to users, such as a maintenance message or a new feature announcement. Both HTML and Markdown formatting are supported, allowing for customized styling like below: + +``` +**You are entering a confidential website** +``` + +```html +
You are entering a confidential website
+``` + +![Auth Consent Usage](/images/platform/admin-panels/auth-consent-usage.png) + +Page frame content is displayed as a header and footer in ALL protected pages. Like the auth consent content, both HTML and Markdown formatting are supported here as well. +![Page Frame Usage](/images/platform/admin-panels/page-frame-usage.png) ## Authentication Tab @@ -46,24 +63,23 @@ From this tab, you can configure which login methods are enabled for your instan ![Authentication Settings](/images/platform/admin-panels/admin-panel-auths.png) - ## Rate Limit Tab This tab allows you to set various rate limits for your Infisical instance. You do not need to redeploy when making changes to rate limits as these will be propagated automatically. ![Rate Limit Settings](/images/platform/admin-panels/admin-panel-rate-limits.png) - - Note that rate limit configuration is a paid feature. Please contact sales@infisical.com to purchase a license for its use. + Note that rate limit configuration is a paid feature. Please contact + sales@infisical.com to purchase a license for its use. ## User Management Tab -From this tab, you can view all the users who have signed up for your instance. You can search for users using the search bar and remove them from your instance by pressing the **X** button on their respective row. - +From this tab, you can view all the users who have signed up for your instance. You can search for users using the search bar and remove them from your instance by clicking on the three dots icon on the right. Additionally, the Server Admin can grant server administrator access to other users through this menu. ![User Management](/images/platform/admin-panels/admin-panel-users.png) - Note that rate limit configuration is a paid feature. Please contact sales@infisical.com to purchase a license for its use. + Note that rate limit configuration is a paid feature. Please contact + sales@infisical.com to purchase a license for its use. diff --git a/docs/documentation/platform/audit-log-streams/audit-log-streams.mdx b/docs/documentation/platform/audit-log-streams/audit-log-streams.mdx index fdd8cc7c7..2f38a3930 100644 --- a/docs/documentation/platform/audit-log-streams/audit-log-streams.mdx +++ b/docs/documentation/platform/audit-log-streams/audit-log-streams.mdx @@ -80,3 +80,143 @@ Your Audit Logs are now ready to be streamed. 3. Create a new header with key **DD-API-KEY** and set the value as **API Key**. + +## Audit Log Stream Data + +Each log entry sent to the external logging provider will follow the same structure. + +### Example Log Entry + +```created-secret.json +{ + "id": "7dc1713b-d787-4147-9e21-770be01cc992", + "actor": "user", + "actorMetadata": { + "email": "example@infisical.com", + "userId": "7383b701-d83f-45c0-acb4-04e138b987ab", + "username": "example@infisical.com" + }, + "ipAddress": "127.0.0.1", + "eventType": "create-secret", + "eventMetadata": { + "secretId": "3e5c796e-6599-4181-8dca-51133bb3acd0", + "secretKey": "TEST-SECRET", + "secretPath": "/", + "environment": "dev", + "secretVersion": 1 + }, + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + "userAgentType": "web", + "expiresAt": "2025-01-18T01:11:25.552Z", + "createdAt": "2025-01-15T01:11:25.552Z", + "updatedAt": "2025-01-15T01:11:25.552Z", + "orgId": "785649f1-ff4b-4ef9-a40a-9b9878e46e57", + "projectId": "09bfcc01-0917-4bea-9c7a-2d320584d5b1", + "projectName": "example-project" +} +``` + +### Audit Logs Structure + + The unique identifier for the log entry. + + + + The entity responsible for performing or causing the event; this can be a user or service. + + + + The metadata associated with the actor. This varies based on the actor type. + + + This metadata is present when the `actor` field is set to `user`. + + + The unique identifier for the actor. + + + The email address of the actor. + + + The username of the actor. + + + + + This metadata is present when the `actor` field is set to `identity`. + + + The unique identifier for the identity. + + + The name of the identity. + + + + + This metadata is present when the `actor` field is set to `service`. + + + The unique identifier for the service. + + + The name of the service. + + + + + + If the `actor` field is set to `platform`, `scimClient`, or `unknownUser`, the `actorMetadata` field will be an empty object. + + + + + + The IP address of the actor. + + + + The type of event that occurred. Below you can see a list of possible event types. More event types will be added in the future as we expand our audit logs further. + + `get-secrets`, `delete-secrets`, `get-secret`, `create-secret`, `update-secret`, `delete-secret`, `get-workspace-key`, `authorize-integration`, `update-integration-auth`, `unauthorize-integration`, `create-integration`, `delete-integration`, `add-trusted-ip`, `update-trusted-ip`, `delete-trusted-ip`, `create-service-token`, `delete-service-token`, `create-identity`, `update-identity`, `delete-identity`, `login-identity-universal-auth`, `add-identity-universal-auth`, `update-identity-universal-auth`, `get-identity-universal-auth`, `create-identity-universal-auth-client-secret`, `revoke-identity-universal-auth-client-secret`, `get-identity-universal-auth-client-secret`, `create-environment`, `update-environment`, `delete-environment`, `add-workspace-member`, `remove-workspace-member`, `create-folder`, `update-folder`, `delete-folder`, `create-webhook`, `update-webhook-status`, `delete-webhook`, `get-secret-imports`, `create-secret-import`, `update-secret-import`, `delete-secret-import`, `update-user-workspace-role`, `update-user-workspace-denied-permissions`, `create-certificate-authority`, `get-certificate-authority`, `update-certificate-authority`, `delete-certificate-authority`, `get-certificate-authority-csr`, `get-certificate-authority-cert`, `sign-intermediate`, `import-certificate-authority-cert`, `get-certificate-authority-crl`, `issue-cert`, `get-cert`, `delete-cert`, `revoke-cert`, `get-cert-body`, `create-pki-alert`, `get-pki-alert`, `update-pki-alert`, `delete-pki-alert`, `create-pki-collection`, `get-pki-collection`, `update-pki-collection`, `delete-pki-collection`, `get-pki-collection-items`, `add-pki-collection-item`, `delete-pki-collection-item`, `org-admin-accessed-project`, `create-certificate-template`, `update-certificate-template`, `delete-certificate-template`, `get-certificate-template`, `create-certificate-template-est-config`, `update-certificate-template-est-config`, `get-certificate-template-est-config`, `update-project-slack-config`, `get-project-slack-config`, `integration-synced`, `create-shared-secret`, `delete-shared-secret`, `read-shared-secret`. + + + + The metadata associated with the event. This varies based on the event type. + + + + The user agent of the actor, if applicable. + + + + The type of user agent. + + + + The expiration date of the log entry. When this date is reached, the log entry will be deleted from Infisical. + + + + The creation date of the log entry. + + + + The last update date of the log entry. This is unlikely to be out of sync with the `createdAt` field, as we do not update log entries after they've been created. + + + + The unique identifier for the organization where the event occurred. + + + + The unique identifier for the project where the event occurred. + + The `projectId` field will only be present if the event occurred at the project level, not the organization level. + + + + The name of the project where the event occurred. + + The `projectName` field will only be present if the event occurred at the project level, not the organization level. + \ No newline at end of file diff --git a/docs/documentation/platform/audit-logs.mdx b/docs/documentation/platform/audit-logs.mdx index 594c1f707..ea6b09fa3 100644 --- a/docs/documentation/platform/audit-logs.mdx +++ b/docs/documentation/platform/audit-logs.mdx @@ -1,6 +1,6 @@ --- title: "Overview" -description: "Track evert event action performed within Infisical projects." +description: "Track all actions performed within Infisical" --- @@ -9,20 +9,76 @@ description: "Track evert event action performed within Infisical projects." If you're using Infisical Cloud, then it is available under the **Pro**, and **Enterprise Tier** with varying retention periods. If you're self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it. + Infisical provides audit logs for security and compliance teams to monitor information access. With the Audit Log functionality, teams can: + - **Track** 40+ different events; - **Filter** audit logs by event, actor, source, date or any combination of these filters; - **Inspect** extensive metadata in the event of any suspicious activity or incident review. ![Audit logs](../../images/platform/audit-logs/audit-logs-table.png) +## Audit Log Structure + Each log contains the following data: -- **Event**: The underlying action such as create, list, read, update, or delete secret(s). -- **Actor**: The entity responsible for performing or causing the event; this can be a user or service. -- **Timestamp**: The date and time at which point the event occurred. -- **Source** (User agent + IP): The software (user agent) and network address (IP) from which the event was initiated. -- **Metadata**: Additional data to provide context for each event. For example, this could be the path at which a secret was fetched from etc. +| Field | Type | Description | Purpose | +| ------------------------- | -------- | --------------------------------------------------------- | ------------------------------------------------------------- | +| **event** | Object | Contains details about the action performed | Captures what happened | +| event.type | String | The specific action that occurred (e.g., "create-secret") | Identifies the exact operation | +| event.metadata | Object | Context-specific details about the event | Provides detailed information relevant to the specific action | +| **actor** | Object | Information about who performed the action | Identifies the responsible entity | +| actor.type | String | Category of actor (user, service, identity, etc.) | Distinguishes between human and non-human actors | +| actor.metadata | Object | Details about the specific actor | Provides identity information | +| actor.metadata.userId | String | Unique identifier for user actors | Links to specific user account | +| actor.metadata.email | String | Email address for user actors | Email of the executing user | +| actor.metadata.username | String | Username for user actors | Username of the executing user | +| actor.metadata.serviceId | String | Identifier for service actors | ID of specific service token | +| actor.metadata.identityId | String | Identifier for identity actors | ID to specific identity | +| actor.metadata.permission | Object | Permission context for the action | Shows permission template data when action was performed | +| **orgId** | String | Organization identifier | Indicates which organization the action occurred in | +| **projectId** | String | Project identifier | Indicates which project the action affected | +| **ipAddress** | String | Source IP address | Shows where the request originated from | +| **userAgent** | String | Client application information | Identifies browser or application used | +| **userAgentType** | String | Category of client (web, CLI, SDK, etc.) | Classifies the access method | +| **timestamp** | DateTime | When the action occurred | Records the exact time of the event | + + +```json +{ + "id": "[UUID]", + "ipAddress": "[IP_ADDRESS]", + "userAgent": "[USER_AGENT_STRING]", + "userAgentType": "web", + "expiresAt": "[TIMESTAMP]", + "createdAt": "[TIMESTAMP]", + "updatedAt": "[TIMESTAMP]", + "orgId": "[ORGANIZATION_UUID]", + "projectId": "[PROJECT_UUID]", + "projectName": "[PROJECT_NAME]", + "event": { + "type": "get-secrets", + "metadata": { + "secretPath": "[PATH]", + "environment": "[ENVIRONMENT_NAME]", + "numberOfSecrets": [NUMBER] + } + }, + "actor": { + "type": "user", + "metadata": { + "email": "[EMAIL]", + "userId": "[USER_UUID]", + "username": "[USERNAME]", + "permission": { + "metadata": {}, + "auth": {} + } + } + } +} +``` + diff --git a/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx b/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx index 225b884cb..2cf4edc0e 100644 --- a/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx +++ b/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx @@ -69,7 +69,7 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -131,12 +131,12 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the expiration time of the lease or delete a lease before it's set time to live. +This will allow you to see the expiration time of the lease or delete a lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/aws-iam.mdx b/docs/documentation/platform/dynamic-secrets/aws-iam.mdx index 7a3976e0f..730e2b287 100644 --- a/docs/documentation/platform/dynamic-secrets/aws-iam.mdx +++ b/docs/documentation/platform/dynamic-secrets/aws-iam.mdx @@ -66,7 +66,7 @@ Replace **\** with your AWS account id and **\** w - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -138,12 +138,12 @@ Replace **\** with your AWS account id and **\** w ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the lease details and delete the lease ahead of its expiration time. +This will allow you to see the lease details and delete the lease ahead of its expiration time. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx b/docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx index 8a71772b1..515efabeb 100644 --- a/docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx +++ b/docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx @@ -98,7 +98,7 @@ Click on Add assignments. Search for the application name you created and select - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -151,12 +151,12 @@ Click on Add assignments. Search for the application name you created and select ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the expiration time of the lease or delete a lease before it's set time to live. +This will allow you to see the expiration time of the lease or delete a lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/cassandra.mdx b/docs/documentation/platform/dynamic-secrets/cassandra.mdx index fd46c8288..e7ec4f69d 100644 --- a/docs/documentation/platform/dynamic-secrets/cassandra.mdx +++ b/docs/documentation/platform/dynamic-secrets/cassandra.mdx @@ -39,7 +39,7 @@ The above configuration allows user creation and granting permissions. - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -116,12 +116,12 @@ The above configuration allows user creation and granting permissions. ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the lease details and delete the lease ahead of its expiration time. +This will allow you to see the lease details and delete the lease ahead of its expiration time. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/elastic-search.mdx b/docs/documentation/platform/dynamic-secrets/elastic-search.mdx index 0b2897790..0e1bc5104 100644 --- a/docs/documentation/platform/dynamic-secrets/elastic-search.mdx +++ b/docs/documentation/platform/dynamic-secrets/elastic-search.mdx @@ -34,7 +34,7 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -114,12 +114,12 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the expiration time of the lease or delete a lease before it's set time to live. +This will allow you to see the expiration time of the lease or delete a lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/ldap.mdx b/docs/documentation/platform/dynamic-secrets/ldap.mdx index ac06a7576..a1731432c 100644 --- a/docs/documentation/platform/dynamic-secrets/ldap.mdx +++ b/docs/documentation/platform/dynamic-secrets/ldap.mdx @@ -31,7 +31,7 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -171,7 +171,7 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) diff --git a/docs/documentation/platform/dynamic-secrets/mongo-atlas.mdx b/docs/documentation/platform/dynamic-secrets/mongo-atlas.mdx index f9352f2e5..5eda1669e 100644 --- a/docs/documentation/platform/dynamic-secrets/mongo-atlas.mdx +++ b/docs/documentation/platform/dynamic-secrets/mongo-atlas.mdx @@ -30,7 +30,7 @@ Create a project scopped API Key with the required permission in your Mongo Atla - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -101,12 +101,12 @@ Create a project scopped API Key with the required permission in your Mongo Atla ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the expiration time of the lease or delete a lease before it's set time to live. +This will allow you to see the expiration time of the lease or delete a lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/mongo-db.mdx b/docs/documentation/platform/dynamic-secrets/mongo-db.mdx index f34d578dc..ec384f7a9 100644 --- a/docs/documentation/platform/dynamic-secrets/mongo-db.mdx +++ b/docs/documentation/platform/dynamic-secrets/mongo-db.mdx @@ -31,7 +31,7 @@ Create a user with the required permission in your MongoDB instance. This user w - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -103,12 +103,12 @@ Create a user with the required permission in your MongoDB instance. This user w ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the expiration time of the lease or delete a lease before it's set time to live. +This will allow you to see the expiration time of the lease or delete a lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/mssql.mdx b/docs/documentation/platform/dynamic-secrets/mssql.mdx index fb666adca..2a279ce90 100644 --- a/docs/documentation/platform/dynamic-secrets/mssql.mdx +++ b/docs/documentation/platform/dynamic-secrets/mssql.mdx @@ -28,13 +28,17 @@ Create a user with the required permission in your SQL instance. This user will - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) Maximum time-to-live for a generated secret + + List of key/value metadata pairs + + Choose the service you want to generate dynamic secrets for. This must be selected as **MS SQL**. @@ -105,12 +109,12 @@ Create a user with the required permission in your SQL instance. This user will ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the expiration time of the lease or delete the lease before it's set time to live. +This will allow you to see the expiration time of the lease or delete the lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/mysql.mdx b/docs/documentation/platform/dynamic-secrets/mysql.mdx index d85f4b7bb..f88a88d35 100644 --- a/docs/documentation/platform/dynamic-secrets/mysql.mdx +++ b/docs/documentation/platform/dynamic-secrets/mysql.mdx @@ -27,13 +27,17 @@ Create a user with the required permission in your SQL instance. This user will - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) Maximum time-to-live for a generated secret + + List of key/value metadata pairs + + Choose the service you want to generate dynamic secrets for. This must be selected as **MySQL**. @@ -102,12 +106,12 @@ Create a user with the required permission in your SQL instance. This user will ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the expiration time of the lease or delete a lease before it's set time to live. +This will allow you to see the expiration time of the lease or delete a lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/oracle.mdx b/docs/documentation/platform/dynamic-secrets/oracle.mdx index a6fb68913..c7b34bec9 100644 --- a/docs/documentation/platform/dynamic-secrets/oracle.mdx +++ b/docs/documentation/platform/dynamic-secrets/oracle.mdx @@ -27,13 +27,17 @@ Create a user with the required permission in your SQL instance. This user will - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) Maximum time-to-live for a generated secret + + List of key/value metadata pairs + + Choose the service you want to generate dynamic secrets for. This must be selected as **Oracle**. @@ -62,7 +66,7 @@ Create a user with the required permission in your SQL instance. This user will A CA may be required if your DB requires it for incoming connections. AWS RDS instances with default settings will requires a CA which can be downloaded [here](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.CertificatesAllRegions). - ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-oracle.png) + ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-oracle.png) @@ -102,12 +106,12 @@ Create a user with the required permission in your SQL instance. This user will ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the expiration time of the lease or delete a lease before it's set time to live. +This will allow you to see the expiration time of the lease or delete a lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/overview.mdx b/docs/documentation/platform/dynamic-secrets/overview.mdx index 24c7fae4e..1f17869ef 100644 --- a/docs/documentation/platform/dynamic-secrets/overview.mdx +++ b/docs/documentation/platform/dynamic-secrets/overview.mdx @@ -4,6 +4,13 @@ sidebarTitle: "Overview" description: "Learn how to generate secrets dynamically on-demand." --- + + Note that Dynamic Secrets is a paid feature. + + If you're using Infisical Cloud, then it is available under the **Enterprise Tier** + If you're self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it. + + ## Introduction Contrary to static key-value secrets, which require manual input of data into the secure Infisical storage, **dynamic secrets are generated on-demand upon access**. diff --git a/docs/documentation/platform/dynamic-secrets/postgresql.mdx b/docs/documentation/platform/dynamic-secrets/postgresql.mdx index ebc19b011..feb81d6d6 100644 --- a/docs/documentation/platform/dynamic-secrets/postgresql.mdx +++ b/docs/documentation/platform/dynamic-secrets/postgresql.mdx @@ -28,13 +28,17 @@ Create a user with the required permission in your SQL instance. This user will - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) Maximum time-to-live for a generated secret + + List of key/value metadata pairs + + Choose the service you want to generate dynamic secrets for. This must be selected as **PostgreSQL**. @@ -63,7 +67,7 @@ Create a user with the required permission in your SQL instance. This user will A CA may be required if your DB requires it for incoming connections. AWS RDS instances with default settings will requires a CA which can be downloaded [here](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.CertificatesAllRegions). - ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal.png) + ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-postgresql.png) @@ -105,12 +109,12 @@ Create a user with the required permission in your SQL instance. This user will ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the expiration time of the lease or delete the lease before it's set time to live. +This will allow you to see the expiration time of the lease or delete the lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx b/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx index f8649b727..6ac5ac069 100644 --- a/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx +++ b/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx @@ -28,7 +28,7 @@ The Infisical RabbitMQ dynamic secret allows you to generate RabbitMQ credential - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -103,12 +103,12 @@ The Infisical RabbitMQ dynamic secret allows you to generate RabbitMQ credential ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the expiration time of the lease or delete a lease before it's set time to live. +This will allow you to see the expiration time of the lease or delete a lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/redis.mdx b/docs/documentation/platform/dynamic-secrets/redis.mdx index cb2e6a17e..43fbc6b61 100644 --- a/docs/documentation/platform/dynamic-secrets/redis.mdx +++ b/docs/documentation/platform/dynamic-secrets/redis.mdx @@ -27,7 +27,7 @@ Create a user with the required permission in your Redis instance. This user wil - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -93,12 +93,12 @@ Create a user with the required permission in your Redis instance. This user wil ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the expiration time of the lease or delete a lease before it's set time to live. +This will allow you to see the expiration time of the lease or delete a lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/sap-ase.mdx b/docs/documentation/platform/dynamic-secrets/sap-ase.mdx new file mode 100644 index 000000000..3b7a895fb --- /dev/null +++ b/docs/documentation/platform/dynamic-secrets/sap-ase.mdx @@ -0,0 +1,116 @@ +--- +title: "SAP ASE" +description: "Learn how to dynamically generate SAP ASE database account credentials." +--- + +The Infisical SAP ASE dynamic secret allows you to generate SAP ASE database credentials on demand. + +## Prerequisite + +- Infisical requires that you have a user in your SAP ASE instance, configured with the appropriate permissions. This user will facilitate the creation of new accounts as needed. + Ensure the user possesses privileges for creating, dropping, and granting permissions to roles for it to be able to create dynamic secrets. + The user used for authentication must have access to the `master` database. You can use the `sa` user for this purpose or create a new user with the necessary permissions. + +- The SAP ASE instance should be reachable by Infisical. + +## Set up Dynamic Secrets with SAP ASE + + + + Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret. + + + ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png) + + + ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-modal.png) + + + + Name by which you want the secret to be referenced + + + + Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + + + + The maximum time-to-live for a generated secret + + + + Your SAP ASE instance host (IP or domain) + + + + Your SAP ASE instance port. On default SAP ASE instances this is usually `5000`. + + + + The database name that you want to generate credentials for. This database must exist on the SAP ASE instance. + Please note that the user/password used for authentication must have access to this database, **and** the `master` database. + + + + Username that will be used to create dynamic secrets + + + + Password that will be used to create dynamic secrets + + + ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-setup-modal.png) + + + + If you want to provide specific privileges for the generated dynamic credentials, you can modify the SQL statement to your needs. + ![Modify SQL Statements Modal](../../../images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-statements.png) + + + Due to SAP ASE limitations, the attached SQL statements are not executed as a transaction. + + + + + After submitting the form, you will see a dynamic secret created in the dashboard. + + + Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials. + To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item. + Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section. + + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png) + + When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for. + + ![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png) + + + Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret in step 4. + + + + Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you. + + ![Provision Lease](/images/platform/dynamic-secrets/lease-values.png) + + + + +## Audit or Revoke Leases + +Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. +This will allow you see the lease details and delete the lease ahead of its expiration time. + +![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) + +## Renew Leases + +To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** as illustrated below. +![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) + + + Lease renewals cannot exceed the maximum TTL set when configuring the dynamic + secret. + diff --git a/docs/documentation/platform/dynamic-secrets/sap-hana.mdx b/docs/documentation/platform/dynamic-secrets/sap-hana.mdx index 3c2a837d3..668777549 100644 --- a/docs/documentation/platform/dynamic-secrets/sap-hana.mdx +++ b/docs/documentation/platform/dynamic-secrets/sap-hana.mdx @@ -30,7 +30,7 @@ The Infisical SAP HANA dynamic secret allows you to generate SAP HANA database c - Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) @@ -106,13 +106,13 @@ The Infisical SAP HANA dynamic secret allows you to generate SAP HANA database c ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the lease details and delete the lease ahead of its expiration time. +This will allow you to see the lease details and delete the lease ahead of its expiration time. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases -To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** as illustrated below. +To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) diff --git a/docs/documentation/platform/dynamic-secrets/snowflake.mdx b/docs/documentation/platform/dynamic-secrets/snowflake.mdx index f5e06ba76..75db96c8f 100644 --- a/docs/documentation/platform/dynamic-secrets/snowflake.mdx +++ b/docs/documentation/platform/dynamic-secrets/snowflake.mdx @@ -109,7 +109,7 @@ Infisical's Snowflake dynamic secrets allow you to generate Snowflake user crede ## Audit or Revoke Leases Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. -This will allow you see the lease details and delete the lease ahead of its expiration time. +This will allow you to see the lease details and delete the lease ahead of its expiration time. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) diff --git a/docs/documentation/platform/dynamic-secrets/totp.mdx b/docs/documentation/platform/dynamic-secrets/totp.mdx new file mode 100644 index 000000000..201a402e1 --- /dev/null +++ b/docs/documentation/platform/dynamic-secrets/totp.mdx @@ -0,0 +1,70 @@ +--- +title: "TOTP" +description: "Learn how to dynamically generate time-based one-time passwords." +--- + +The Infisical TOTP dynamic secret allows you to generate time-based one-time passwords on demand. + +## Prerequisite + +- Infisical requires either an OTP url or a secret key from a TOTP provider. + +## Set up Dynamic Secrets with TOTP + + + + Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret. + + + ![Add Dynamic Secret Button](/images/platform/dynamic-secrets/add-dynamic-secret-button.png) + + + ![Dynamic Secret Modal](/images/platform/dynamic-secrets/dynamic-secret-modal-totp.png) + + + + Name by which you want the secret to be referenced + + + There are two supported configuration types - `url` and `manual`. + + When `url` is selected, you can configure the TOTP generator using the OTP URL. + + When `manual` is selected, you can configure the TOTP generator using the secret key along with other configurations like period, number of digits, and algorithm. + + + OTP URL in `otpauth://` format used to generate TOTP codes. + + + Base32 encoded secret used to generate TOTP codes. + + + Time interval in seconds between generating new TOTP codes. + + + Number of digits to generate in each TOTP code. + + + Hash algorithm to use when generating TOTP codes. The supported algorithms are sha1, sha256, and sha512. + + + ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-url.png) + ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-manual.png) + + + + After submitting the form, you will see a dynamic secret created in the dashboard. + + + + Once you've successfully configured the dynamic secret, you're ready to generate on-demand TOTPs. + To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item. + + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) + + Once you click the `Generate` button, a new secret lease will be generated and the TOTP will be shown to you. + + ![Provision Lease](/images/platform/dynamic-secrets/totp-lease-value.png) + + + diff --git a/docs/documentation/platform/gateways/gateway-security.mdx b/docs/documentation/platform/gateways/gateway-security.mdx new file mode 100644 index 000000000..83490fd4d --- /dev/null +++ b/docs/documentation/platform/gateways/gateway-security.mdx @@ -0,0 +1,110 @@ +--- +title: "Gateway Security Architecture" +sidebarTitle: "Architecture" +description: "Understand the security model and tenant isolation of Infisical's Gateway" +--- + +# Gateway Security Architecture + +The Infisical Gateway enables Infisical Cloud to securely interact with private resources using mutual TLS authentication and private PKI (Public Key Infrastructure) system to ensure secure, isolated communication between multiple tenants. +This document explains the internal security architecture and how tenant isolation is maintained. + +## Security Model Overview + +### Private PKI System +Each organization (tenant) in Infisical has its own private PKI system consisting of: + +1. **Root CA**: The ultimate trust anchor for the organization +2. **Intermediate CAs**: + - Client CA: Issues certificates for cloud components + - Gateway CA: Issues certificates for gateway instances + +This hierarchical structure ensures complete isolation between organizations as each has its own independent certificate chain. + +### Certificate Hierarchy +``` +Root CA (Organization Specific) +├── Client CA +│ └── Client Certificates (Cloud Components) +└── Gateway CA + └── Gateway Certificates (Gateway Instances) +``` + +## Communication Security + +### 1. Gateway Registration +When a gateway is first deployed: + +1. Establishes initial connection using machine identity token +2. Allocates a relay address for communication +3. Exchanges certificates through a secure handshake: + - Gateway receives a unique certificate signed by organization's Gateway CA along with certificate chain for verification + +### 2. Mutual TLS Authentication +All communication between gateway and cloud uses mutual TLS (mTLS): + +- **Gateway Authentication**: + - Presents certificate signed by organization's Gateway CA + - Certificate contains unique identifiers (Organization ID, Gateway ID) + - Cloud validates complete certificate chain + +- **Cloud Authentication**: + - Presents certificate signed by organization's Client CA + - Certificate includes required organizational unit ("gateway-client") + - Gateway validates certificate chain back to organization's root CA + +### 3. Relay Communication +The relay system provides secure tunneling: + +1. **Connection Establishment**: + - Uses QUIC protocol over UDP for efficient, secure communication + - Provides built-in encryption, congestion control, and multiplexing + - Enables faster connection establishment and reduced latency + - Each organization's traffic is isolated using separate relay sessions + +2. **Traffic Isolation**: + - Each gateway gets unique relay credentials + - Traffic is end-to-end encrypted using QUIC's TLS 1.3 + - Organization's private keys never leave their environment + +## Tenant Isolation + +### Certificate-Based Isolation +- Each organization has unique root CA and intermediate CAs +- Certificates contain organization-specific identifiers +- Cross-tenant communication is cryptographically impossible + +### Gateway-Project Mapping +- Gateways are explicitly mapped to specific projects +- Access controls enforce organization boundaries +- Project-level permissions determine resource accessibility + +### Resource Access Control +1. **Project Verification**: + - Gateway verifies project membership + - Validates organization ownership + - Enforces project-level permissions + +2. **Resource Restrictions**: + - Gateways only accept connections to approved resources + - Each connection requires explicit project authorization + - Resources remain private to their assigned organization + +## Security Measures + +### Certificate Lifecycle +- Certificates have limited validity periods +- Automatic certificate rotation +- Immediate certificate revocation capabilities + +### Monitoring and Verification +1. **Continuous Verification**: + - Regular heartbeat checks + - Certificate chain validation + - Connection state monitoring + +2. **Security Controls**: + - Automatic connection termination on verification failure + - Audit logging of all access attempts + - Machine identity based authentication + diff --git a/docs/documentation/platform/gateways/images/gateway-highlevel-diagram.png b/docs/documentation/platform/gateways/images/gateway-highlevel-diagram.png new file mode 100644 index 000000000..5f942bcf0 Binary files /dev/null and b/docs/documentation/platform/gateways/images/gateway-highlevel-diagram.png differ diff --git a/docs/documentation/platform/gateways/overview.mdx b/docs/documentation/platform/gateways/overview.mdx new file mode 100644 index 000000000..02d9c863a --- /dev/null +++ b/docs/documentation/platform/gateways/overview.mdx @@ -0,0 +1,116 @@ +--- +title: "Gateway" +sidebarTitle: "Overview" +description: "How to access private network resources from Infisical" +--- + +![Alt text](/documentation/platform/gateways/images/gateway-highlevel-diagram.png) + +The Infisical Gateway provides secure access to private resources within your network without needing direct inbound connections to your environment. +This method keeps your resources fully protected from external access while enabling Infisical to securely interact with resources like databases. +Common use cases include generating dynamic credentials or rotating credentials for private databases. + + + **Note:** Gateway is a paid feature. - **Infisical Cloud users:** Gateway is + available under the **Enterprise Tier**. - **Self-Hosted Infisical:** Please + contact [sales@infisical.com](mailto:sales@infisical.com) to purchase an + enterprise license. + + +## How It Works + +The Gateway serves as a secure intermediary that facilitates direct communication between the Infisical server and your private network. +It’s a lightweight daemon packaged within the Infisical CLI, making it easy to deploy and manage. Once set up, the Gateway establishes a connection with a relay server, ensuring that all communication between Infisical and your Gateway is fully end-to-end encrypted. +This setup guarantees that only the platform and your Gateway can decrypt the transmitted information, keeping communication with your resources secure, private and isolated. + +## Deployment + +The Infisical Gateway is seamlessly integrated into the Infisical CLI under the `gateway` command, making it simple to deploy and manage. +You can install the Gateway in all the same ways you install the Infisical CLI—whether via npm, Docker, or a binary. +For detailed installation instructions, refer to the Infisical [CLI Installation instructions](/cli/overview). + +To function, the Gateway must authenticate with Infisical. This requires a machine identity configured with the appropriate permissions to create and manage a Gateway. +Once authenticated, the Gateway establishes a secure connection with Infisical to allow your private resources to be reachable. + +### Deployment process + + + + 1. Navigate to **Organization Access Control** in your Infisical dashboard. + 2. Create a dedicated machine identity for your Gateway. + 3. **Best Practice:** Assign a unique identity to each Gateway for better security and management. + ![Create Gateway Identity](../../../images/platform/gateways/create-identity-for-gateway.png) + + + + You'll need to choose an authentication method to initiate communication with Infisical. View the available machine identity authentication methods [here](/documentation/platform/identities/machine-identities). + + + + Use the Infisical CLI to deploy the Gateway. You can run it directly or install it as a systemd service for production: + + + + For production deployments on Linux, install the Gateway as a systemd service: + ```bash + sudo infisical gateway install --token --domain + sudo systemctl start infisical-gateway + ``` + This will install and start the Gateway as a secure systemd service that: + - Runs with restricted privileges: + - Runs as root user (required for secure token management) + - Restricted access to home directories + - Private temporary directory + - Automatically restarts on failure + - Starts on system boot + - Manages token and domain configuration securely in `/etc/infisical/gateway.conf` + + + The install command requires: + - Linux operating system + - Root/sudo privileges + - Systemd + + + + + For development or testing, you can run the Gateway directly. Log in with your machine identity and start the Gateway in one command: + ```bash + infisical gateway --token $(infisical login --method=universal-auth --client-id=<> --client-secret=<> --plain) + ``` + + Alternatively, if you already have the token, use it directly with the `--token` flag: + ```bash + infisical gateway --token + ``` + + Or set it as an environment variable: + ```bash + export INFISICAL_TOKEN= + infisical gateway + ``` + + + + For detailed information about the gateway command and its options, see the [gateway command documentation](/cli/commands/gateway). + + + Ensure the deployed Gateway has network access to the private resources you intend to connect with Infisical. + + + + + To confirm your Gateway is working, check the deployment status by looking for the message **"Gateway started successfully"** in the Gateway logs. This indicates the Gateway is running properly. Next, verify its registration by opening your Infisical dashboard, navigating to **Organization Access Control**, and selecting the **Gateways** tab. Your newly deployed Gateway should appear in the list. + ![Gateway List](../../../images/platform/gateways/gateway-list.png) + + + + To enable Infisical features like dynamic secrets or secret rotation to access private resources through the Gateway, you need to link the Gateway to the relevant projects. + + Start by accessing the **Gateway settings** then locate the Gateway in the list, click the options menu (**:**), and select **Edit Details**. + ![Edit Gateway Option](../../../images/platform/gateways/edit-gateway.png) + In the edit modal that appears, choose the projects you want the Gateway to access and click **Save** to confirm your selections. + ![Project Assignment Modal](../../../images/platform/gateways/assign-project.png) + Once added to a project, the Gateway becomes available for use by any feature that supports Gateways within that project. + + diff --git a/docs/documentation/platform/identities/jwt-auth.mdx b/docs/documentation/platform/identities/jwt-auth.mdx new file mode 100644 index 000000000..3dcf12b29 --- /dev/null +++ b/docs/documentation/platform/identities/jwt-auth.mdx @@ -0,0 +1,169 @@ +--- +title: JWT Auth +description: "Learn how to authenticate with Infisical using JWT-based authentication." +--- + +**JWT Auth** is a platform-agnostic authentication method that validates JSON Web Tokens (JWTs) issued by your JWT issuer or authentication system, allowing secure authentication from any platform or environment that can obtain valid JWTs. + +## Diagram + +The following sequence diagram illustrates the JWT Auth workflow for authenticating with Infisical. + +```mermaid +sequenceDiagram + participant Client as Client Application + participant Issuer as JWT Issuer + participant Infis as Infisical + + Client->>Issuer: Step 1: Request JWT token + Issuer-->>Client: Return signed JWT with claims + + Note over Client,Infis: Step 2: Login Operation + Client->>Infis: Send signed JWT to /api/v1/auth/jwt-auth/login + + Note over Infis: Step 3: JWT Validation + Infis->>Infis: Validate JWT signature using configured public keys or JWKS + Infis->>Infis: Verify required claims (aud, sub, iss) + + Note over Infis: Step 4: Token Generation + Infis->>Client: Return short-lived access token + + Note over Client,Infis: Step 5: Access Infisical API with Token + Client->>Infis: Make authenticated requests using the short-lived access token +``` + +## Concept + +At a high-level, Infisical authenticates a client by verifying the JWT and checking that it meets specific requirements (e.g. it is signed by a trusted key) at the `/api/v1/auth/jwt-auth/login` endpoint. If successful, then Infisical returns a short-lived access token that can be used to make authenticated requests to the Infisical API. + +To be more specific: + +1. The client requests a JWT from their JWT issuer. +2. The fetched JWT is sent to Infisical at the `/api/v1/auth/jwt-auth/login` endpoint. +3. Infisical validates the JWT signature using either: + - Pre-configured public keys (Static configuration) + - Public keys fetched from a JWKS endpoint (JWKS configuration) +4. Infisical verifies that the configured claims match in the token. This includes standard claims like subject, audience, and issuer, as well as any additional custom claims specified in the configuration. +5. If all is well, Infisical returns a short-lived access token that the client can use to make authenticated requests to the Infisical API. + + + For JWKS configuration, Infisical needs network-level access to the configured + JWKS endpoint. + + +## Guide + +In the following steps, we explore how to create and use identities to access the Infisical API using the JWT authentication method. + + + + To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. + + ![identities organization](/images/platform/identities/identities-org.png) + + When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles. + + ![identities organization create](/images/platform/identities/identities-org-create.png) + + Now input a few details for your new identity. Here's some guidance for each field: + + - Name (required): A friendly name for the identity. + - Role (required): A role from the **Organization Roles** tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to. + + Once you've created an identity, you'll be redirected to a page where you can manage the identity. + + ![identities page](/images/platform/identities/identities-page.png) + + Since the identity has been configured with Universal Auth by default, you should re-configure it to use JWT Auth instead. To do this, press to edit the **Authentication** section, + remove the existing Universal Auth configuration, and add a new JWT Auth configuration onto the identity. + + ![identities page remove default auth](/images/platform/identities/identities-page-remove-default-auth.png) + + ![identities create jwt auth method](/images/platform/identities/identities-org-create-jwt-auth-method-jwks.png) + ![identities create jwt auth method](/images/platform/identities/identities-org-create-jwt-auth-method-static.png) + + Restrict access by properly configuring the JWT validation settings. + + Here's some more guidance for each field: + + **Static configuration**: + - Public Keys: One or more PEM-encoded public keys (RSA or ECDSA) used to verify JWT signatures. Each key must include the proper BEGIN/END markers. + + **JWKS configuration**: + - JWKS URL: The endpoint URL that serves your JSON Web Key Sets (JWKS). This endpoint must provide the public keys used for JWT signature verification. + - JWKS CA Certificate: Optional PEM-encoded CA certificate used for validating the TLS connection to the JWKS endpoint. + + **Common fields for both configurations**: + - Issuer: The unique identifier of the JWT provider. This value is used to verify the iss (issuer) claim in the JWT. + - Audiences: A list of intended recipients. This value is checked against the aud (audience) claim in the token. + - Subject: The expected principal that is the subject of the JWT. This value is checked against the sub (subject) claim in the token. + - Claims: Additional claims that must be present in the JWT for it to be valid. You can specify required claim names and their expected values. + - Access Token TTL (default is `2592000` equivalent to 30 days): The lifetime for an access token in seconds. This value will be referenced at renewal time. + - Access Token Max TTL (default is `2592000` equivalent to 30 days): The maximum lifetime for an access token in seconds. This value will be referenced at renewal time. + - Access Token Max Number of Uses (default is `0`): The maximum number of times that an access token can be used; a value of `0` implies infinite number of uses. + - Access Token Trusted IPs: The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the `0.0.0.0/0`, allowing usage from any network address. + + The `subject`, `audiences`, and `claims` fields support glob pattern matching; however, we highly recommend using hardcoded values whenever possible. + + + + To enable the identity to access project-level resources such as secrets within a specific project, you should add it to that project. + + To do this, head over to the project you want to add the identity to and go to Project Settings > Access Control > Machine Identities and press **Add identity**. + + Next, select the identity you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this identity can have access to. + + ![identities project](/images/platform/identities/identities-project.png) + + ![identities project create](/images/platform/identities/identities-project-create.png) + + + + To access the Infisical API as the identity, you will need to obtain a JWT from your JWT issuer that meets the validation requirements configured in step 2. + + Once you have obtained a valid JWT, you can use it to authenticate with Infisical at the `/api/v1/auth/jwt-auth/login` endpoint. + + We provide a code example below of how you might use the JWT to authenticate with Infisical to gain access to the [Infisical API](/api-reference/overview/introduction). + + + The shown example uses Node.js but you can use any other language to authenticate with Infisical using your JWT. + + ```javascript + try { + // Obtain JWT from your issuer + const jwt = ""; + + const infisicalUrl = "https://app.infisical.com"; // or your self-hosted Infisical URL + const identityId = ""; + + const { data } = await axios.post( + `{infisicalUrl}/api/v1/auth/jwt-auth/login`, + { + identityId, + jwt, + } + ); + + console.log("result data: ", data); // access token here + } catch(err) { + console.error(err); + } + ``` + + + + We recommend using one of Infisical's clients like SDKs or the Infisical Agent to authenticate with Infisical using JWT Auth as they handle the authentication process for you. + + + + Each identity access token has a time-to-live (TTL) which you can infer from the response of the login operation; + the default TTL is `2592000` seconds (30 days) which can be adjusted in the configuration. + + If an identity access token exceeds its max TTL or maximum number of uses, it can no longer authenticate with the Infisical API. In this case, + a new access token should be obtained by performing another login operation with a valid JWT. + + + + diff --git a/docs/documentation/platform/identities/kubernetes-auth.mdx b/docs/documentation/platform/identities/kubernetes-auth.mdx index b4d7cc1ac..58069f09e 100644 --- a/docs/documentation/platform/identities/kubernetes-auth.mdx +++ b/docs/documentation/platform/identities/kubernetes-auth.mdx @@ -37,7 +37,8 @@ then Infisical returns a short-lived access token that can be used to make authe To be more specific: 1. The application deployed on Kubernetes retrieves its [service account credential](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#opt-out-of-api-credential-automounting) that is a JWT token at the `/var/run/secrets/kubernetes.io/serviceaccount/token` pod path. -2. The application sends the JWT token to Infisical at the `/api/v1/auth/kubernetes-auth/login` endpoint after which Infisical forwards the JWT token to the Kubernetes API Server at the [TokenReview API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/) for verification and to obtain the service account information associated with the JWT token. Infisical is able to authenticate and interact with the TokenReview API by using a long-lived service account JWT token itself (referred to onward as the token reviewer JWT token). +2. The application sends the JWT token to Infisical at the `/api/v1/auth/kubernetes-auth/login` endpoint after which Infisical forwards the JWT token to the Kubernetes API Server at the TokenReview API for verification and to obtain the service account information associated with the JWT token. +Infisical is able to authenticate and interact with the TokenReview API by using either the long lived JWT token set while configuring this authentication method or by using the incoming token itself. The JWT token mentioned in this context is referred as the token reviewer JWT token. 3. Infisical checks the service account properties against set criteria such **Allowed Service Account Names** and **Allowed Namespaces**. 4. If all is well, Infisical returns a short-lived access token that the application can use to make authenticated requests to the Infisical API. @@ -53,6 +54,12 @@ In the following steps, we explore how to create and use identities for your app + + + + + **When to use this option**: Choose this approach when you want centralized authentication management. Only one service account needs special permissions, and your application service accounts remain unchanged. + 1.1. Start by creating a service account in your Kubernetes cluster that will be used by Infisical to authenticate with the Kubernetes API Server. ```yaml infisical-service-account.yaml @@ -61,7 +68,6 @@ In the following steps, we explore how to create and use identities for your app metadata: name: infisical-auth namespace: default - ``` ``` @@ -121,7 +127,40 @@ In the following steps, we explore how to create and use identities for your app Keep this JWT token handy as you will need it for the **Token Reviewer JWT** field when configuring the Kubernetes Auth authentication method for the identity in step 2. - + + + + + **When to use this option**: Choose this approach to eliminate long-lived tokens. This option simplifies Infisical configuration but requires each application service account to have elevated permissions. + + + The self-validation method eliminates the need for a separate long-lived reviewer JWT by using the same token for both authentication and validation. Instead of creating a dedicated reviewer service account, you'll grant the necessary permissions to each application service account. + + For each service account that needs to authenticate with Infisical, add the `system:auth-delegator` role: + + ```yaml client-role-binding.yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: infisical-client-binding-[your-app-name] + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:auth-delegator + subjects: + - kind: ServiceAccount + name: [your-app-service-account] + namespace: [your-app-namespace] + ``` + + ``` + kubectl apply -f client-role-binding.yaml + ``` + + When configuring Kubernetes Auth in Infisical, leave the **Token Reviewer JWT** field empty. Infisical will use the client's own token for validation. + + + To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. @@ -151,7 +190,8 @@ In the following steps, we explore how to create and use identities for your app Here's some more guidance on each field: - Kubernetes Host / Base Kubernetes API URL: The host string, host:port pair, or URL to the base of the Kubernetes API server. This can usually be obtained by running `kubectl cluster-info`. - - Token Reviewer JWT: A long-lived service account JWT token for Infisical to access the [TokenReview API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/) to validate other service account JWT tokens submitted by applications/pods. This is the JWT token obtained from step 1.5. + - Token Reviewer JWT: A long-lived service account JWT token for Infisical to access the [TokenReview API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/) to validate other service account JWT tokens submitted by applications/pods. This is the JWT token obtained from step 1.5(Reviewer Tab). If omitted, the client's own JWT will be used instead, which requires the client to have the `system:auth-delegator` ClusterRole binding. + This is shown in step 1, option 2. - Allowed Service Account Names: A comma-separated list of trusted service account names that are allowed to authenticate with Infisical. - Allowed Namespaces: A comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical. - Allowed Audience: An optional audience claim that the service account JWT token must have to authenticate with Infisical. @@ -176,18 +216,19 @@ In the following steps, we explore how to create and use identities for your app To access the Infisical API as the identity, you should first make sure that the pod running your application is bound to a service account specified in the **Allowed Service Account Names** field of the identity's Kubernetes Auth authentication method configuration in step 2. - + Once bound, the pod will receive automatically mounted service account credentials that is a JWT token at the `/var/run/secrets/kubernetes.io/serviceaccount/token` path. This token should be used to authenticate with Infisical at the `/api/v1/auth/kubernetes-auth/login` endpoint. - + For information on how to configure sevice accounts for pods, refer to the guide [here](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/). - + We provide a code example below of how you might retrieve the JWT token and use it to authenticate with Infisical to gain access to the [Infisical API](/api-reference/overview/introduction). + + > The shown example uses Node.js but you can use any other language to retrieve the service account JWT token and use it to authenticate with Infisical. - - ```javascript + + ```javascript const fs = require("fs"); try { const tokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"; @@ -237,15 +278,16 @@ In the following steps, we explore how to create and use identities for your app There are a few reasons for why this might happen: - - - The access token has expired. - - The identity is insufficently permissioned to interact with the resources you wish to access. - - The client access token is being used from an untrusted IP. + +- The access token has expired. +- The identity is insufficently permissioned to interact with the resources you wish to access. +- The client access token is being used from an untrusted IP. + - A identity access token can have a time-to-live (TTL) or incremental lifetime after which it expires. - - In certain cases, you may want to extend the lifespan of an access token; to do so, you must set a max TTL parameter. +A identity access token can have a time-to-live (TTL) or incremental lifetime after which it expires. + +In certain cases, you may want to extend the lifespan of an access token; to do so, you must set a max TTL parameter. A token can be renewed any number of times where each call to renew it can extend the token's lifetime by increments of the access token's TTL. Regardless of how frequently an access token is renewed, its lifespan remains bound to the maximum TTL determined at its creation. diff --git a/docs/documentation/platform/identities/oidc-auth/gitlab.mdx b/docs/documentation/platform/identities/oidc-auth/gitlab.mdx new file mode 100644 index 000000000..228392aa6 --- /dev/null +++ b/docs/documentation/platform/identities/oidc-auth/gitlab.mdx @@ -0,0 +1,145 @@ +--- +title: GitLab +description: "Learn how to authenticate GitLab pipelines with Infisical using OpenID Connect (OIDC)." +--- + +**OIDC Auth** is a platform-agnostic JWT-based authentication method that can be used to authenticate from any platform or environment using an identity provider with OpenID Connect. + +## Diagram + +The following sequence diagram illustrates the OIDC Auth workflow for authenticating GitLab pipelines with Infisical. + +```mermaid +sequenceDiagram + participant Client as GitLab Pipeline + participant Idp as GitLab Identity Provider + participant Infis as Infisical + + Client->>Idp: Step 1: Request identity token + Idp-->>Client: Return JWT with verifiable claims + + Note over Client,Infis: Step 2: Login Operation + Client->>Infis: Send signed JWT to /api/v1/auth/oidc-auth/login + + Note over Infis,Idp: Step 3: Query verification + Infis->>Idp: Request JWT public key using OIDC Discovery + Idp-->>Infis: Return public key + + Note over Infis: Step 4: JWT validation + Infis->>Client: Return short-lived access token + + Note over Client,Infis: Step 5: Access Infisical API with Token + Client->>Infis: Make authenticated requests using the short-lived access token +``` + +## Concept + +At a high-level, Infisical authenticates a client by verifying the JWT and checking that it meets specific requirements (e.g. it is issued by a trusted identity provider) at the `/api/v1/auth/oidc-auth/login` endpoint. If successful, +then Infisical returns a short-lived access token that can be used to make authenticated requests to the Infisical API. + +To be more specific: + +1. The GitLab pipeline requests an identity token from GitLab's identity provider. +2. The fetched identity token is sent to Infisical at the `/api/v1/auth/oidc-auth/login` endpoint. +3. Infisical fetches the public key that was used to sign the identity token from GitLab's identity provider using OIDC Discovery. +4. Infisical validates the JWT using the public key provided by the identity provider and checks that the subject, audience, and claims of the token matches with the set criteria. +5. If all is well, Infisical returns a short-lived access token that the GitLab pipeline can use to make authenticated requests to the Infisical API. + + + Infisical needs network-level access to GitLab's identity provider endpoints. + + +## Guide + +In the following steps, we explore how to create and use identities to access the Infisical API using the OIDC Auth authentication method. + + + + To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. + + ![identities organization](/images/platform/identities/identities-org.png) + + When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles. + + ![identities organization create](/images/platform/identities/identities-org-create.png) + + Now input a few details for your new identity. Here's some guidance for each field: + + - Name (required): A friendly name for the identity. + - Role (required): A role from the **Organization Roles** tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to. + + Once you've created an identity, you'll be redirected to a page where you can manage the identity. + + ![identities page](/images/platform/identities/identities-page.png) + + Since the identity has been configured with Universal Auth by default, you should re-configure it to use OIDC Auth instead. To do this, press to edit the **Authentication** section, + remove the existing Universal Auth configuration, and add a new OIDC Auth configuration onto the identity. + + ![identities page remove default auth](/images/platform/identities/identities-page-remove-default-auth.png) + + ![identities create oidc auth method](/images/platform/identities/identities-org-create-oidc-auth-method.png) + + Restrict access by configuring the Subject, Audiences, and Claims fields + + Here's some more guidance on each field: + - OIDC Discovery URL: The URL used to retrieve the OpenID Connect configuration from the identity provider. This will be used to fetch the public key needed for verifying the provided JWT. For GitLab SaaS (GitLab.com), this should be set to `https://gitlab.com`. For self-hosted GitLab instances, use the domain of your GitLab instance. + - Issuer: The unique identifier of the identity provider issuing the JWT. This value is used to verify the iss (issuer) claim in the JWT to ensure the token is issued by a trusted provider. This should also be set to the domain of the Gitlab instance. + - CA Certificate: The PEM-encoded CA cert for establishing secure communication with the Identity Provider endpoints. For GitLab.com, this can be left blank. + - Subject: The expected principal that is the subject of the JWT. For GitLab pipelines, this should be set to a string that uniquely identifies the pipeline and its context, in the format `project_path:{group}/{project}:ref_type:{type}:ref:{branch_name}` (e.g., `project_path:example-group/example-project:ref_type:branch:ref:main`). + - Claims: Additional information or attributes that should be present in the JWT for it to be valid. You can refer to GitLab's [documentation](https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html#token-payload) for the list of supported claims. + - Access Token TTL (default is `2592000` equivalent to 30 days): The lifetime for an acccess token in seconds. This value will be referenced at renewal time. + - Access Token Max TTL (default is `2592000` equivalent to 30 days): The maximum lifetime for an acccess token in seconds. This value will be referenced at renewal time. + - Access Token Max Number of Uses (default is `0`): The maximum number of times that an access token can be used; a value of `0` implies infinite number of uses. + - Access Token Trusted IPs: The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the `0.0.0.0/0`, allowing usage from any network address. + For more details on the appropriate values for the OIDC fields, refer to GitLab's [documentation](https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html#token-payload). + The `subject`, `audiences`, and `claims` fields support glob pattern matching; however, we highly recommend using hardcoded values whenever possible. + + + To enable the identity to access project-level resources such as secrets within a specific project, you should add it to that project. + + To do this, head over to the project you want to add the identity to and go to Project Settings > Access Control > Machine Identities and press **Add identity**. + + Next, select the identity you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this identity can have access to. + + ![identities project](/images/platform/identities/identities-project.png) + + ![identities project create](/images/platform/identities/identities-project-create.png) + + + + As demonstration, we will be using the Infisical CLI to fetch Infisical secrets and utilize them within a GitLab pipeline. + + To access Infisical secrets as the identity, you need to use an identity token from GitLab which matches the OIDC configuration defined for the machine identity. + This can be done by defining the `id_tokens` property. The resulting token would then be used to login with OIDC like the following: `infisical login --method=oidc-auth --oidc-jwt=$GITLAB_TOKEN` + + Below is a complete example of how a GitLab pipeline can be configured to work with secrets from Infisical using the Infisical CLI with OIDC Auth: + + ```yaml + image: ubuntu + + stages: + - build + + build-job: + stage: build + id_tokens: + INFISICAL_ID_TOKEN: + aud: infisical-aud-test + script: + - apt update && apt install -y curl + - curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash + - apt-get update && apt-get install -y infisical + - export INFISICAL_TOKEN=$(infisical login --method=oidc-auth --machine-identity-id=4e807a78-1b1c-4bd6-9609-ef2b0cf4fd54 --oidc-jwt=$INFISICAL_ID_TOKEN --silent --plain) + - infisical run --projectId=1d0443c1-cd43-4b3a-91a3-9d5f81254a89 --env=dev -- npm run build + ``` + + The `id_tokens` keyword is used to request an ID token for the job. In this example, an ID token named `INFISICAL_ID_TOKEN` is requested with the audience (`aud`) claim set to "infisical-aud-test". This ID token will be used to authenticate with Infisical. + + Each identity access token has a time-to-live (TTL) which you can infer from the response of the login operation; the default TTL is `7200` seconds, which can be adjusted. + + If an identity access token expires, it can no longer authenticate with the Infisical API. In this case, a new access token should be obtained by performing another login operation. + + + + + diff --git a/docs/documentation/platform/identities/oidc-auth/terraform-cloud.mdx b/docs/documentation/platform/identities/oidc-auth/terraform-cloud.mdx new file mode 100644 index 000000000..5105981ec --- /dev/null +++ b/docs/documentation/platform/identities/oidc-auth/terraform-cloud.mdx @@ -0,0 +1,87 @@ +--- +title: "Terraform Cloud" +description: "How to authenticate with Infisical from Terraform Cloud using OIDC." +--- + +This guide will walk you through setting up Terraform Cloud to inject a [workload identity token](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/dynamic-provider-credentials/workload-identity-tokens) and use it for OIDC-based authentication with the Infisical Terraform provider. You'll start by creating a machine identity in Infisical, then configure Terraform Cloud to pass the injected token into your Terraform runs. + + + + Follow the instructions [in this documentation](/documentation/platform/identities/oidc-auth/general) to create a machine identity with OIDC auth. Infisical OIDC configuration values for Terraform Cloud: + 1. Set the OIDC Discovery URL to https://app.terraform.io. + 2. Set the Issuer to https://app.terraform.io. + 3. Configure the Audience to match the value you will use for **TFC_WORKLOAD_IDENTITY_AUDIENCE** in Terraform Cloud for the next step. + + + To view all possible claims available from Terraform cloud, visit [HashiCorp’s documentation](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/dynamic-provider-credentials/workload-identity-tokens#token-structure). + + + + + + + 1. **Navigate to your workspace** in Terraform Cloud. + 2. **Add a workspace variable** named `TFC_WORKLOAD_IDENTITY_AUDIENCE`: + - **Key**: `TFC_WORKLOAD_IDENTITY_AUDIENCE` + - **Value**: For example, `my-infisical-audience` + - **Category**: Environment + + > **Important**: + > - The presence of `TFC_WORKLOAD_IDENTITY_AUDIENCE` is required for Terraform Cloud to inject a token. + > - If you are self-hosting HCP Terraform agents, ensure they are **v1.7.0 or above**. + + Once set, Terraform Cloud will inject a workload identity token into the run environment as `TFC_WORKLOAD_IDENTITY_TOKEN`. + + + If you need multiple tokens (each with a different audience), create additional variables: + + ``` + TFC_WORKLOAD_IDENTITY_AUDIENCE_[YOUR_TAG_HERE] + ``` + + For example: + - `TFC_WORKLOAD_IDENTITY_AUDIENCE_INFISICAL` + - `TFC_WORKLOAD_IDENTITY_AUDIENCE_OTHER_SERVICE` + + Terraform Cloud will then inject: + - `TFC_WORKLOAD_IDENTITY_TOKEN_INFISICAL` + - `TFC_WORKLOAD_IDENTITY_TOKEN_OTHER_SERVICE` + + > **Note**: + > - The `[YOUR_TAG_HERE]` can only contain letters, numbers, and underscores. + > - You **cannot** use the reserved keyword `TYPE`. + > - Generating multiple tokens requires **v1.12.0 or later** if you are self-hosting agents. + + + + + If you are running on self-hosted HCP Terraform agents, you must use v1.7.0 or later to enable token injection. If you need to generate multiple tokens, you must use v1.12.0 or later. + + + + In your Terraform configuration, reference the injected token by name. For example: + + ```hcl + provider "infisical" { + host = "https://app.infisical.com" + + auth = { + oidc = { + identity_id = "" + # This must match the environment variable Terraform injects: + token_environment_variable_name = "TFC_WORKLOAD_IDENTITY_TOKEN" + } + } + } + ``` + + - **`host`**: Defaults to `https://app.infisical.com`. Override if using a self-hosted Infisical instance. + - **`identity_id`**: The OIDC identity ID from Infisical. + - **`token_environment_variable_name`**: Must match the injected variable name from Terraform Cloud. If using single token, use `TFC_WORKLOAD_IDENTITY_TOKEN`. If using multiple tokens, choose the one you want to use (e.g., `TFC_WORKLOAD_IDENTITY_TOKEN_INFISICAL`). + + + 1. Run a plan and apply in Terraform Cloud. + 2. Verify the Infisical provider authenticates successfully without issues. If you run into authentication errors, double-check the Infisical identity has the correct roles/permissions in Infisical. + + + diff --git a/docs/documentation/platform/identities/universal-auth.mdx b/docs/documentation/platform/identities/universal-auth.mdx index 597978093..4d66e30b4 100644 --- a/docs/documentation/platform/identities/universal-auth.mdx +++ b/docs/documentation/platform/identities/universal-auth.mdx @@ -114,6 +114,13 @@ using the Universal Auth authentication method. that is to exchange the **Client ID** and **Client Secret** of the identity for an access token by making a request to the `/api/v1/auth/universal-auth/login` endpoint. + + Choose the correct base URL based on your region: + + - For Infisical Cloud US users: `https://app.infisical.com` + - For Infisical Cloud EU users: `https://eu.infisical.com` + + #### Sample request ```bash Request diff --git a/docs/documentation/platform/kms-configuration/aws-kms.mdx b/docs/documentation/platform/kms-configuration/aws-kms.mdx index f9fa54b4d..3fc5404ae 100644 --- a/docs/documentation/platform/kms-configuration/aws-kms.mdx +++ b/docs/documentation/platform/kms-configuration/aws-kms.mdx @@ -19,7 +19,7 @@ Before you begin, you'll first need to choose a method of authentication with AW ![IAM Role Creation](/images/integrations/aws/integration-aws-iam-assume-role.png) 2. Select **AWS Account** as the **Trusted Entity Type**. - 3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If you are self-hosting, provide the AWS account number where Infisical is hosted. + 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. 4. Optionally, enable **Require external ID** and enter your Infisical **project ID** to further enhance security. @@ -74,22 +74,22 @@ Next, you will need to follow the steps listed below to add AWS KMS for your org - ![Open encryption org settings](../../../images/platform/kms/aws/encryption-org-settings.png) + ![Open encryption org settings](../../../images/platform/kms/encryption-org-settings.png) - ![Add encryption org settings](../../../images/platform/kms/aws/encryption-org-settings-add.png) + ![Add encryption org settings](../../../images/platform/kms/encryption-org-settings-add.png) Click the 'Add' button to begin adding a new external KMS. - ![Select Encryption Provider](../../../images/platform/kms/aws/encryption-modal-provider-select.png) + ![Select Encryption Provider](../../../images/platform/kms/encryption-modal-provider-select.png) Choose 'AWS KMS' from the list of encryption providers. - Selecting AWS as the provider will require you input the following fields. + Selecting AWS as the provider will require you input the following fields. - - Name for referencing the AWS KMS key within the organization. - + + Name for referencing the AWS KMS key within the organization. + Short description of the AWS KMS key. diff --git a/docs/documentation/platform/kms-configuration/gcp-kms.mdx b/docs/documentation/platform/kms-configuration/gcp-kms.mdx new file mode 100644 index 000000000..5682814fe --- /dev/null +++ b/docs/documentation/platform/kms-configuration/gcp-kms.mdx @@ -0,0 +1,132 @@ +--- +title: "GCP Key Management Service" +description: "Learn how to manage encryption using GCP KMS" +--- + +To enhance the security of your Infisical projects, you can now encrypt your secrets using an external Key Management Service (KMS). +When external KMS is configured for your project, all encryption and decryption operations will be handled by the chosen KMS. +This guide will walk you through the steps needed to configure external KMS support with Google Cloud KMS. + +## Prerequisites + +Before you begin, you'll first need to set up a GCP Service Account, add a KMS key and set the required permissions. + + + + 1. Navigate to the [Create Service Account](https://console.cloud.google.com/iam-admin/serviceaccounts/create) page in your GCP Console. + ![GCP Service Account Creation](/images/platform/kms/gcp/service-account-form.png) + + 2. Give the service account a suitable **name** and **description**. Then click **Create and Continue**. + 3. Under **Grant this service account access to project**, click **Select a role** and select the + **Cloud KMS Viewer** and **Cloud KMS CryptoKey Encrypter/Decrypter*** roles, then click **Continue**. + ![GCP Service Account Permissions](/images/platform/kms/gcp/service-account-permissions.png) + 3. You can skip the **Grant users access to this service account** options. + 4. Click Done. + 5. You should see the service account in the list of service accounts. Click it to view the service account details. + 6. Select the **Keys** tab, click **Add Key**, select **Create new key**, select **JSON** as the key type, then click **Create**. + 7. You will be prompted to download a JSON file that we will need later on. + + Remember to keep the JSON file in a secure location. It will be used to authenticate your GCP service account. + + Once you have successfully set up GCP KMS with Infisical, you should permanently delete the JSON file. + + + + + 1. Navigate to the [KMS](https://console.cloud.google.com/security/kms) page in your GCP Console. + + If you have not used GCP KMS before, you will be redirected to the **Cloud Key Management Service (KMS) API** page. + + Click **Enable** to enable the KMS API, then continue the steps below. + + It may take a few minutes for the API to be enabled and KMS section of the Cloud Console to become viewable. + + + 2. In the KMS section, click **Create Key Ring**. + ![GCP Create Key Ring](/images/platform/kms/gcp/keyring-create.png) + + 3. Give the key ring a **Name** and select a **Region**, then click **Create**. + + We don't currently support multi-region key rings. + + + 4. On the "Create Key" page, give the key a **Name** and set the **Protection Level** based on your requirements (or use default *Software*), then click **Continue**. + + 5. Under **Key Material**, select **Generated Key**, then click **Continue**. + + 6. Under **Purpose**, select **Symmetric encrypt/decrypt**, then click **Continue**. + + 7. For **Key Rotation Period**, select **Never (manual rotation)**, then click **Continue** followed by **Create**. + + 8. You should see the key in the list of keys. We're now ready to set it up in Infisical. + + + + +## Setup GCP KMS in the Organization Settings + +Next, you will need to follow the steps listed below to add GCP KMS for your organization. + + + + ![Open encryption org settings](../../../images/platform/kms/encryption-org-settings.png) + + + ![Add encryption org settings](../../../images/platform/kms/encryption-org-settings-add.png) + Click the 'Add' button to begin adding a new external KMS. + + + ![Select Encryption Provider](../../../images/platform/kms/encryption-modal-provider-select.png) + Choose 'GCP KMS' from the list of encryption providers. + + + + ![GCP Create KMS Modal](/images/platform/kms/gcp/gcp-add-modal-filled.png) + Selecting GCP as the provider will require you input the following fields. + + + Name for referencing the GCP KMS key within the organization. + + + + Short description of the GCP KMS key. + + + + The GCP region where the GCP KMS key ring is located. + + + + Upload the JSON file you downloaded earlier when creating the GCP service account. + + + + This field will be populated with the list of GCP KMS keys in the selected region. Select the key you created earlier. + + + + + Save your configuration to apply the settings. + + + +You now have a GCP KMS Key configured at the organization level. You can assign these GCP KMS keys to existing Infisical projects by visiting the 'Project Settings' page. + +## Assign GCP KMS Key to an Existing Project + +To assign the GCP KMS key you added to your organization, follow the steps below. + + + + ![Open encryption project + settings](../../../images/platform/kms/gcp/project-settings.png) + + + ![Select encryption project + settings](../../../images/platform/kms/gcp/select-gcp-kms-in-project.png) + Choose the GCP KMS key you configured earlier. + + + Once you have selected the KMS of choice, click save. + + diff --git a/docs/documentation/platform/kms-configuration/overview.mdx b/docs/documentation/platform/kms-configuration/overview.mdx index 327481bc4..159d71dd3 100644 --- a/docs/documentation/platform/kms-configuration/overview.mdx +++ b/docs/documentation/platform/kms-configuration/overview.mdx @@ -25,4 +25,4 @@ For existing projects, you can configure the KMS from the Project Settings page. ## External KMS -Infisical supports the use of external KMS solutions to enhance security and compliance. You can configure your project to use services like [AWS Key Management Service](./aws-kms) for managing encryption. \ No newline at end of file +Infisical supports the use of external KMS solutions to enhance security and compliance. You can configure your project to use services like [AWS Key Management Service](./aws-kms) or [GCP Key Management Service](./gcp-kms) for managing encryption. diff --git a/docs/documentation/platform/kms/hsm-integration.mdx b/docs/documentation/platform/kms/hsm-integration.mdx new file mode 100644 index 000000000..633377b3d --- /dev/null +++ b/docs/documentation/platform/kms/hsm-integration.mdx @@ -0,0 +1,544 @@ +--- +title: "HSM Integration" +description: "Learn more about integrating an HSM with Infisical KMS." +--- + + + Changing the encryption strategy for your instance is an Enterprise-only feature. + This section is intended for users who have obtained an Enterprise license and are on-premise. + + + Please reach out to sales@infisical.com if you have any questions. + + +## Overview + +Infisical KMS currently supports two encryption strategies: +1. **Standard Encryption**: This is the default encryption strategy used by Infisical KMS. It uses a software-protected encryption key to encrypt KMS keys within your Infisical instance. The root encryption key is defined by setting the `ENCRYPTION_KEY` environment variable. +2. **Hardware Security Module (HSM)**: This encryption strategy uses a Hardware Security Module (HSM) to create a root encryption key that is stored on a physical device to encrypt the KMS keys within your instance. + +## Hardware Security Module (HSM) + +![HSM Illustration](/images/platform/kms/hsm/hsm-illustration.png) + +Using a hardware security module comes with the added benefit of having a secure and tamper-proof device to store your encryption keys. This ensures that your data is protected from unauthorized access. + + + All encryption keys used for cryptographic operations are stored within the HSM. This means that if the HSM is lost or destroyed, you will no longer be able to decrypt your data stored within Infisical. Most providers offer recovery options for HSM devices, which you should consider when setting up an HSM device. + + +Enabling HSM encryption has a set of key benefits: +1. **Root Key Wrapping**: The root KMS encryption key that is used to secure your Infisical instance will be encrypted using the HSM device rather than the standard software-protected key. +2. **FIPS 140-2/3 Compliance**: Using an HSM device ensures that your Infisical instance is FIPS 140-2 or FIPS 140-3 compliant. For FIPS 140-3, ensure that your HSM is FIPS 140-3 validated. + +#### Caveats +- **Performance**: Using an HSM device can have a performance impact on your Infisical instance. This is due to the additional latency introduced by the HSM device. This is however only noticeable when your instance(s) start up or when the encryption strategy is changed. +- **Key Recovery**: If the HSM device is lost or destroyed, you will no longer be able to decrypt your data stored within Infisical. Most HSM providers offer recovery options, which you should consider when setting up an HSM device. + +### Requirements +- An Infisical instance with a version number that is equal to or greater than `v0.91.0`. +- If you are using Docker, your instance must be using the `infisical/infisical-fips` image. +- An HSM device from a provider such as [Thales Luna HSM](https://cpl.thalesgroup.com/encryption/data-protection-on-demand/services/luna-cloud-hsm), [AWS CloudHSM](https://aws.amazon.com/cloudhsm/), or others. + + +### FIPS Compliance +FIPS, also known as the Federal Information Processing Standard, is a set of standards that are used to accredit cryptographic modules. FIPS 140-2 and FIPS 140-3 are the two most common standards used for cryptographic modules. If your HSM uses FIPS 140-3 validated hardware, Infisical will automatically be FIPS 140-3 compliant. If your HSM uses FIPS 140-2 validated hardware, Infisical will be FIPS 140-2 compliant. + +HSM devices are especially useful for organizations that operate in regulated industries such as healthcare, finance, and government, where data security and compliance are of the utmost importance. + +For organizations that work with US government agencies, FIPS compliance is almost always a requirement when dealing with sensitive information. FIPS compliance ensures that the cryptographic modules used by the organization meet the security requirements set by the US government. + +## Setup Instructions + + + + + To set up HSM encryption, you need to configure an HSM provider and HSM key. The HSM provider is used to connect to the HSM device, and the HSM key is used to encrypt Infisical's KMS keys. We recommend using a Cloud HSM provider such as [Thales Luna HSM](https://cpl.thalesgroup.com/encryption/data-protection-on-demand/services/luna-cloud-hsm) or [AWS CloudHSM](https://aws.amazon.com/cloudhsm/). + + You need to follow the instructions provided by the HSM provider to set up the HSM device. Once the HSM device is set up, the HSM device can be used within Infisical. + + After setting up the HSM from your provider, you will have a set of files that you can use to access the HSM. These files need to be present on the machine where Infisical is running. + If you are using containers, you will need to mount the folder where these files are stored as a volume in the container. + + The setup process for an HSM device varies depending on the provider. We have created a guide for Thales Luna Cloud HSM, which you can find below. + + + + + + Are you using Docker or Kubernetes for your deployment? If you are using Docker or Kubernetes, please follow the instructions in the [Using HSM's in your Deployment](#using-hsms-in-your-deployment) section. + + + Configuring the HSM on Infisical requires setting a set of environment variables: + - `HSM_LIB_PATH`: The path to the PKCS#11 library provided by the HSM provider. This usually comes in the form of a `.so` for Linux and MacOS, or a `.dll` file for Windows. For Docker, you need to mount the library path as a volume. Further instructions can be found below. If you are using Docker, make sure to set the HSM_LIB_PATH environment variable to the path where the library is mounted in the container. + - `HSM_PIN`: The PKCS#11 PIN to use for authentication with the HSM device. + - `HSM_SLOT`: The slot number to use for the HSM device. This is typically between `0` and `5` for most HSM devices. + - `HSM_KEY_LABEL`: The label of the key to use for encryption. **Please note that if no key is found with the provided label, the HSM will create a new key with the provided label.** + + You can read more about the [default instance configurations](/self-hosting/configuration/envars) here. + + + After setting up the HSM, you need to restart the Infisical instance for the changes to take effect. + + + ![Server Admin Console](/images/platform/kms/hsm/server-admin-console.png) + + + ![Set Encryption Strategy](/images/platform/kms/hsm/encryption-strategy.png) + + Once you press the 'Save' button, your Infisical instance will immediately switch to the HSM encryption strategy. This will re-encrypt your KMS key with keys from the HSM device. + + + To verify that the HSM was correctly configured, you can try creating a new secret in one of your projects. If the secret is created successfully, the HSM is now being used for encryption. + + + + +## Using HSMs In Your Deployment + + + + When using Docker, you need to mount the path containing the HSM client files. This section covers how to configure your Infisical instance to use an HSM with Docker. + + + + + + When using Docker, you are able to set your HSM library path to any location on your machine. In this example, we are going to be using `/etc/luna-docker`. + + ```bash + mkdir /etc/luna-docker + ``` + + After [setting up your Luna Cloud HSM client](https://thalesdocs.com/gphsm/luna/7/docs/network/Content/install/client_install/add_dpod.htm), you should have a set of files, referred to as the HSM client. You don't need all the files, but for simplicity we recommend copying all the files from the client. + + A folder structure of a client folder will often look like this: + ``` + partition-ca-certificate.pem + partition-certificate.pem + server-certificate.pem + Chrystoki.conf + /plugins + libcloud.plugin + /lock + /libs + /64 + libCryptoki2.so + /jsp + LunaProvider.jar + /64 + libLunaAPI.so + /etc + openssl.cnf + /bin + /64 + ckdemo + lunacm + multitoken + vtl + ``` + + The most important parts of the client folder is the `Chrystoki.conf` file, and the `libs`, `plugins`, and `jsp` folders. You need to copy these files to the folder you created in the first step. + + ```bash + cp -r / /etc/luna-docker + ``` + + + + + The `Chrystoki.conf` file is used to configure the HSM client. You need to update the `Chrystoki.conf` file to point to the correct file paths. + + In this example, we will be mounting the `/etc/luna-docker` folder to the Docker container under a different path. The path we will use in this example is `/usr/safenet/lunaclient`. This means `/etc/luna-docker` will be mounted to `/usr/safenet/lunaclient` in the Docker container. + + An example config file will look like this: + + ```Chrystoki.conf + Chrystoki2 = { + # This path points to the mounted path, /usr/safenet/lunaclient + LibUNIX64 = /usr/safenet/lunaclient/libs/64/libCryptoki2.so; + } + + Luna = { + DefaultTimeOut = 500000; + PEDTimeout1 = 100000; + PEDTimeout2 = 200000; + PEDTimeout3 = 20000; + KeypairGenTimeOut = 2700000; + CloningCommandTimeOut = 300000; + CommandTimeOutPedSet = 720000; + } + + CardReader = { + LunaG5Slots = 0; + RemoteCommand = 1; + } + + Misc = { + # Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step. + PluginModuleDir = /usr/safenet/lunaclient/plugins; + MutexFolder = /usr/safenet/lunaclient/lock; + PE1746Enabled = 1; + ToolsDir = /usr/bin; + + } + + Presentation = { + ShowEmptySlots = no; + } + + LunaSA Client = { + ReceiveTimeout = 20000; + # Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step. + SSLConfigFile = /usr/safenet/lunaclient/etc/openssl.cnf; + ClientPrivKeyFile = ./etc/ClientNameKey.pem; + ClientCertFile = ./etc/ClientNameCert.pem; + ServerCAFile = ./etc/CAFile.pem; + NetClient = 1; + TCPKeepAlive = 1; + } + + + REST = { + AppLogLevel = error + ServerName = ; + ServerPort = 443; + AuthTokenConfigURI = ; + AuthTokenClientId = ; + AuthTokenClientSecret = ; + RestClient = 1; + ClientTimeoutSec = 120; + ClientPoolSize = 32; + ClientEofRetryCount = 15; + ClientConnectRetryCount = 900; + ClientConnectIntervalMs = 1000; + } + XTC = { + Enabled = 1; + TimeoutSec = 600; + } + ``` + + Save the file after updating the paths. + + + + Running Docker with HSM encryption requires setting the HSM-related environment variables as mentioned previously in the [HSM setup instructions](#setup-instructions). You can set these environment variables in your Docker run command. + + We are setting the environment variables for Docker via the command line in this example, but you can also pass in a `.env` file to set these environment variables. + + + If no key is found with the provided key label, the HSM will create a new key with the provided label. + Infisical depends on an AES and HMAC key to be present in the HSM. If these keys are not present, Infisical will create them. The AES key label will be the value of the `HSM_KEY_LABEL` environment variable, and the HMAC key label will be the value of the `HSM_KEY_LABEL` environment variable with the suffix `_HMAC`. + + + ```bash + docker run -p 80:8080 \ + -v /etc/luna-docker:/usr/safenet/lunaclient \ + -e HSM_LIB_PATH="/usr/safenet/lunaclient/libs/64/libCryptoki2.so" \ + -e HSM_PIN="" \ + -e HSM_SLOT= \ + -e HSM_KEY_LABEL="" \ + + # The rest are unrelated to HSM setup... + -e ENCRYPTION_KEY="<>" \ + -e AUTH_SECRET="<>" \ + -e DB_CONNECTION_URI="<>" \ + -e REDIS_URL="<>" \ + -e SITE_URL="<>" \ + infisical/infisical-fips: # Replace with the version you want to use + ``` + + We recommend reading further about [using Infisical with Docker](/self-hosting/deployment-options/standalone-infisical). + + + + After following these steps, your Docker setup will be ready to use HSM encryption. + + + + + When you are deploying Infisical with the [Kubernetes self-hosting option](/self-hosting/deployment-options/kubernetes-helm), you can still use HSM encryption, but you need to ensure that the HSM client files are present in the container. + + + + + This is only supported on helm chart version `1.4.1` and above. Please see the [Helm Chart Changelog](https://github.com/Infisical/infisical/blob/main/helm-charts/infisical-standalone-postgres/CHANGELOG.md#141-march-19-2025) for more information. + + + + + When using Kubernetes, you need to mount the path containing the HSM client files. This section covers how to configure your Infisical instance to use an HSM with Kubernetes. + + + ```bash + mkdir /etc/hsm-client + ``` + + After [setting up your Luna Cloud HSM client](https://thalesdocs.com/gphsm/luna/7/docs/network/Content/install/client_install/add_dpod.htm), you should have a set of files, referred to as the HSM client. You don't need all the files, but for simplicity we recommend copying all the files from the client. + + A folder structure of a client folder will often look like this: + ``` + partition-ca-certificate.pem + partition-certificate.pem + server-certificate.pem + Chrystoki.conf + /plugins + libcloud.plugin + /lock + /libs + /64 + libCryptoki2.so + /jsp + LunaProvider.jar + /64 + libLunaAPI.so + /etc + openssl.cnf + /bin + /64 + ckdemo + lunacm + multitoken + vtl + ``` + + The most important parts of the client folder is the `Chrystoki.conf` file, and the `libs`, `plugins`, and `jsp` folders. You need to copy these files to the folder you created in the first step. + + ```bash + cp -r / /etc/hsm-client + ``` + + + The `Chrystoki.conf` file is used to configure the HSM client. You need to update the `Chrystoki.conf` file to point to the correct file paths. + + In this example, we will be mounting the `/etc/hsm-client` folder from the host to containers in our deployment's pods at the path `/hsm-client`. This means the contents of `/etc/hsm-client` on the host will be accessible at `/hsm-client` within the containers. + + An example config file will look like this: + + ```Chrystoki.conf + Chrystoki2 = { + # This path points to the mounted path, /hsm-client + LibUNIX64 = /hsm-client/libs/64/libCryptoki2.so; + } + + Luna = { + DefaultTimeOut = 500000; + PEDTimeout1 = 100000; + PEDTimeout2 = 200000; + PEDTimeout3 = 20000; + KeypairGenTimeOut = 2700000; + CloningCommandTimeOut = 300000; + CommandTimeOutPedSet = 720000; + } + + CardReader = { + LunaG5Slots = 0; + RemoteCommand = 1; + } + + Misc = { + # Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step. + PluginModuleDir = /hsm-client/plugins; + MutexFolder = /hsm-client/lock; + PE1746Enabled = 1; + ToolsDir = /usr/bin; + + } + + Presentation = { + ShowEmptySlots = no; + } + + LunaSA Client = { + ReceiveTimeout = 20000; + # Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step. + SSLConfigFile = /hsm-client/etc/openssl.cnf; + ClientPrivKeyFile = ./etc/ClientNameKey.pem; + ClientCertFile = ./etc/ClientNameCert.pem; + ServerCAFile = ./etc/CAFile.pem; + NetClient = 1; + TCPKeepAlive = 1; + } + + + REST = { + AppLogLevel = error + ServerName = ; + ServerPort = 443; + AuthTokenConfigURI = ; + AuthTokenClientId = ; + AuthTokenClientSecret = ; + RestClient = 1; + ClientTimeoutSec = 120; + ClientPoolSize = 32; + ClientEofRetryCount = 15; + ClientConnectRetryCount = 900; + ClientConnectIntervalMs = 1000; + } + XTC = { + Enabled = 1; + TimeoutSec = 600; + } + ``` + + Save the file after updating the paths. + + + + You need to create a Persistent Volume Claim (PVC) to mount the HSM client files to the Infisical deployment. + + + ```bash + kubectl apply -f - < + + + Next we need to update the environment variables used for the deployment. If you followed the [setup instructions for Kubernetes deployments](/self-hosting/deployment-options/kubernetes-helm), you should have a Kubernetes secret called `infisical-secrets`. + We need to update the secret with the following environment variables: + + - `HSM_LIB_PATH` - The path to the HSM client library _(mapped to `/hsm-client/libs/64/libCryptoki2.so`)_ + - `HSM_PIN` - The PIN for the HSM device that you created when setting up your Luna Cloud HSM client + - `HSM_SLOT` - The slot number for the HSM device that you selected when setting up your Luna Cloud HSM client + - `HSM_KEY_LABEL` - The label for the HSM key. If no key is found with the provided key label, the HSM will create a new key with the provided label. + + The following is an example of the secret that you should update: + + ```yaml + apiVersion: v1 + kind: Secret + metadata: + name: infisical-secrets + type: Opaque + stringData: + # ... Other environment variables ... + HSM_LIB_PATH: "/hsm-client/libs/64/libCryptoki2.so" # If you followed this guide, this will be the path of the Luna Cloud HSM client + HSM_PIN: "" + HSM_SLOT: "" + HSM_KEY_LABEL: "" + ``` + + Save the file after updating the environment variables, and apply the secret changes + + ```bash + kubectl apply -f ./secret-file-name.yaml + ``` + + + + After we've successfully configured the PVC and updated our environment variables, we are ready to update the deployment configuration so that the pods it creates can access the HSM client files. + + We need to update the Docker image of the deployment to use `infisical/infisical-fips`. The `infisical/infisical-fips` image is a functionally identical image to the `infisical/infisical` image, but it is built with support for HSM encryption. + + ```yaml + # ... The rest of the values.yaml file ... + + image: + repository: infisical/infisical-fips # Very important: Must use "infisical/infisical-fips" + tag: "v0.117.1-postgres" + pullPolicy: IfNotPresent + + extraVolumeMounts: + - name: hsm-data + mountPath: /hsm-client # The path we will mount the HSM client files to + subPath: ./hsm-client + + extraVolumes: + - name: hsm-data + persistentVolumeClaim: + claimName: infisical-data-pvc # The PVC we created in the previous step + + # ... The rest of the values.yaml file ... + ``` + + + + + + After updating the values.yaml file, you need to upgrade the Helm chart in order for the changes to take effect. + + ```bash + helm upgrade --install infisical infisical-helm-charts/infisical-standalone --values /path/to/values.yaml + ``` + + + After upgrading the Helm chart, you need to restart the deployment in order for the changes to take effect. + + ```bash + kubectl rollout restart deployment/infisical-infisical + ``` + + + After following these steps, your Kubernetes setup will be ready to use HSM encryption. + + + + + + +## Disabling HSM Encryption + +To disable HSM encryption, navigate to Infisical's Server Admin Console and set the KMS encryption strategy to `Software-based Encryption`. This will revert the encryption strategy back to the default software-based encryption. + + + In order to disable HSM encryption, the Infisical instance must be able to access the HSM device. If the HSM device is no longer accessible, you will not be able to disable HSM encryption. + \ No newline at end of file diff --git a/docs/documentation/platform/kms/kmip.mdx b/docs/documentation/platform/kms/kmip.mdx new file mode 100644 index 000000000..1e025d51a --- /dev/null +++ b/docs/documentation/platform/kms/kmip.mdx @@ -0,0 +1,142 @@ +--- +title: "KMIP Integration" +description: "Learn more about integrating with Infisical KMS using KMIP (Key Management Interoperability Protocol)." +--- + + + KMIP integration is an Enterprise-only feature. Please reach out to + sales@infisical.com if you have any questions. + + +## Overview + +Infisical KMS provides **Key Management Interoperability Protocol (KMIP)** support, enabling seamless integration with KMIP-compatible clients. This allows for enhanced key management across various applications that support the **KMIP 1.4 protocol**. + +## Supported Operations + +The Infisical KMIP server supports the following operations for **symmetric keys**: + +- **Create** - Generate symmetric keys. +- **Register** - Register externally created keys. +- **Locate** - Find keys using attributes. +- **Get** - Retrieve keys securely. +- **Activate** - Enable keys for usage. +- **Revoke** - Revoke existing keys. +- **Destroy** - Permanently remove keys. +- **Get Attributes** - Retrieve metadata associated with keys. +- **Query** - Query server capabilities and supported operations. + +## Benefits of KMIP Integration + +Integrating Infisical KMS with KMIP-compatible clients provides the following benefits: + +- **Standardized Key Management**: Allows interoperability with security and cryptographic applications that support KMIP. +- **Enterprise-Grade Security**: Utilizes Infisical’s encryption mechanisms to securely store and manage keys. +- **Centralized Key Management**: Enables a unified approach for managing cryptographic keys across multiple environments. + +## Compatibility + +Infisical KMIP supports **KMIP versions 1.0 to 1.4**, ensuring compatibility with a wide range of clients and security tools. + +## Secure Communication & Authorization + +KMIP client-server communication is secured using **mutual TLS (mTLS)**, ensuring strong identity verification and encrypted data exchange via **PKI certificates**. Each KMIP entity must possess valid certificates signed by a trusted Root CA to establish trust. +For strong isolation, each Infisical organization has its own KMIP PKI (Public Key Infrastructure), ensuring that cryptographic operations and certificate authorities remain separate across organizations. + +Infisical KMS enforces a **two-layer authorization model** for KMIP operations: + +1. **KMIP Server Authorization** – The KMIP server, acting as a proxy, must have the `proxy KMIP` permission to forward client requests to Infisical KMS. This is done using a **machine identity** attached to the KMIP server. +2. **KMIP Client Authorization** – Clients must have the necessary KMIP-level permissions to perform specific key management operations. + +By combining **mTLS for secure communication** and **machine identity-based proxying**, Infisical KMS ensures **strong authentication, controlled access, and centralized key management** for KMIP operations. + +## Setup Instructions + +### Setup KMIP for your organization + + + + From there, press Setup KMIP. + ![KMIP org navigate](/images/platform/kms/kmip/kmip-org-setup-navigation.png) + + + In the modal, select the desired key algorithm to use for the KMIP PKI of your organization. Press continue. + ![KMIP org PKI setup](/images/platform/kms/kmip/kmip-org-setup-modal.png) + + This generates the KMIP PKI for your organization. After this, you can proceed to setting up your KMIP server. + + + + +### Deploying and Configuring the KMIP Server + +Follow these steps to configure and deploy a KMIP server. + + + + Configure a [machine identity](https://infisical.com/docs/documentation/platform/identities/machine-identities#machine-identities) for the KMIP server to use. + ![KMIP create machine identity](/images/platform/kms/kmip/kmip-create-mi.png) + + Create a custom organization role and give it the **Proxy KMIP** permission. + ![KMIP create custom role](/images/platform/kms/kmip/kmip-create-custom-role.png) + ![KMIP assign proxy to role](/images/platform/kms/kmip/kmip-assign-custom-role-proxy.png) + + Assign the machine identity to the custom organization role. This allows the machine identity to serve KMIP client requests and forward them from your KMIP server to Infisical. + ![KMIP assign role to machine identity](/images/platform/kms/kmip/kmip-assign-mi-to-role.png) + + + + + To deploy the KMIP server, use the Infisical CLI’s `kmip start` command. + Before proceeding, make sure you have the [Infisical CLI installed](https://infisical.com/docs/cli/overview). + + Once installed, launch the KMIP server with the following command: + + ```bash + infisical kmip start \ + --identity-client-id= \ # This can be set by defining the INFISICAL_UNIVERSAL_AUTH_CLIENT_ID ENV variable + --identity-client-secret= \ # This can be set by defining the INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET ENV variable + --domain=https://app.infisical.com \ + --hostnames-or-ips="my-kmip-server.com" + ``` + + The following flags are available for the `infisical kmip start` command:: + - **listen-address** (default: localhost:5696): The address the KMIP server listens on. + - **identity-auth-method** (default: universal-auth): The authentication method for the machine identity. + - **identity-client-id**: The client ID of the machine identity. This can be set by defining the `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` ENV variable. + - **identity-client-secret**: The client secret of the machine identity. This can be set by defining the `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` ENV variable. + - **server-name** (default: "kmip-server"): The name of the KMIP server. + - **certificate-ttl** (default: "1y"): The duration for which the server certificate is valid. + - **hostnames-or-ips:** A comma-separated list of hostnames or IPs the KMIP server will use (required). + + + + +### Add and Configure KMIP Clients + + + + From there, press Add KMIP Client + ![KMIP client overview](/images/platform/kms/kmip/kmip-client-overview.png) + + + In the modal, provide the details of your client. The selected permissions determine what KMIP operations can be performed in your KMS project. + ![KMIP client modal](/images/platform/kms/kmip/kmip-client-modal.png) + + + Once the KMIP client is created, you will have to generate a client certificate. + Press Generate Certificate. + ![KMIP generate client cert](/images/platform/kms/kmip/kmip-client-generate-cert.png) + + Provide the desired TTL and key algorithm to use and press Generate Client Certificate. + ![KMIP client cert config](/images/platform/kms/kmip/kmip-client-cert-config-modal.png) + + Configure your KMIP clients to use the generated client certificate, certificate chain and private key. + ![KMIP client cert modal](/images/platform/kms/kmip/kmip-client-certificate-modal.png) + + + + +## Additional Resources + +- [KMIP 1.4 Specification](http://docs.oasis-open.org/kmip/spec/v1.4/os/kmip-spec-v1.4-os.html) diff --git a/docs/documentation/platform/kms/overview.mdx b/docs/documentation/platform/kms/overview.mdx index a9563a635..577373ab8 100644 --- a/docs/documentation/platform/kms/overview.mdx +++ b/docs/documentation/platform/kms/overview.mdx @@ -8,6 +8,10 @@ description: "Learn how to manage and use cryptographic keys with Infisical." Infisical can be used as a Key Management System (KMS), referred to as Infisical KMS, to centralize management of keys to be used for cryptographic operations like encryption/decryption. +By default your Infisical data such as projects and the data within them are encrypted at rest using Infisical's own KMS. This ensures that your data is secure and protected from unauthorized access. + +If you are on-premise, your KMS root key will be created at random with the `ROOT_ENCRYPTION_KEY` environment variable. You can also use a Hardware Security Module (HSM), to create the root key. Read more about [HSM](/docs/documentation/platform/kms/encryption-strategies). + Keys managed in KMS are not extractable from the platform. Additionally, data is never stored when performing cryptographic operations. @@ -26,7 +30,9 @@ The typical workflow for using Infisical KMS consists of the following steps: as via API. -## Guide to Encrypting Data +## Encryption + +### Guide to Encrypting Data In the following steps, we explore how to generate a key and use it to encrypt data. @@ -40,7 +46,8 @@ In the following steps, we explore how to generate a key and use it to encrypt d Specify your key details. Here's some guidance on each field: - Name: A slug-friendly name for the key. - - Type: The encryption algorithm associated with the key (e.g. `AES-GCM-256`). + - Key Usage: The type of key to create (e.g `Encrypt/Decrypt` for encryption, and `Sign/Verify` for signing). + - Algorithm: The encryption algorithm associated with the key (e.g. `AES-GCM-256`). - Description: An optional description of what the intended usage is for the key. ![kms add key modal](/images/platform/kms/infisical-kms/kms-add-key-modal.png) @@ -133,7 +140,7 @@ In the following steps, we explore how to generate a key and use it to encrypt d -## Guide to Decrypting Data +### Guide to Decrypting Data In the following steps, we explore how to use decrypt data using an existing key in Infisical KMS. @@ -189,6 +196,164 @@ In the following steps, we explore how to use decrypt data using an existing key +## Signing + +### Guide to Signing Data + +In the following steps, we explore how to generate a key and use it to sign data. + + + + + + Navigate to Project > Key Management and tap on the **Add Key** button. + ![kms add key button](/images/platform/kms/infisical-kms/kms-add-key.png) + + Specify your key details. Here's some guidance on each field: + + - Name: A slug-friendly name for the key. + - Key Usage: The type of key to create (e.g `Encrypt/Decrypt` for encryption, and `Sign/Verify` for signing). + - Algorithm: The signing algorithm associated with the key (e.g. `RSA_4096`). + - Description: An optional description of what the intended usage is for the key. + + ![kms add key modal](/images/platform/kms/infisical-kms/signing/add-new-rsa-key.png) + + + + Once your key is generated, open the options menu for the newly created key and select sign data. + ![kms key options](/images/platform/kms/infisical-kms/signing/sign-options.png) + + Populate the text area with your data and tap on the Sign button. + ![kms sign data](/images/platform/kms/infisical-kms/signing/sign-data-modal.png) + + Make sure to select the appropriate signing algorithm that will be used to sign the data. + Supported signing algorithms are: + + **For RSA keys:** + - `RSASSA PSS SHA 512`: Not deterministic, and includes random salt. + - `RSASSA PSS SHA 384`: Not deterministic, and includes random salt. + - `RSASSA PSS SHA 256`: Not deterministic, and includes random salt. + - `RSASSA PKCS1 V1.5 SHA 512`: Deterministic, and does not include randomness. + - `RSASSA PKCS1 V1.5 SHA 384`: Deterministic, and does not include randomness. + - `RSASSA PKCS1 V1.5 SHA 256`: Deterministic, and does not include randomness. + + **For ECC keys:** + - `ECDSA SHA 512`: Not deterministic, and includes randomness. + - `ECDSA SHA 384`: Not deterministic, and includes randomness. + - `ECDSA SHA 256`: Not deterministic, and includes randomness. + + In this example, we'll use the `RSASSA PSS SHA 512` signing algorithm. + + + If your data is already Base64 encoded make sure to toggle the respective switch on to avoid + redundant encoding. + + + Copy and store the signature of your data. + ![kms signed data](/images/platform/kms/infisical-kms/signing/copy-signature.png) + + + + + + + To sign data, make an API request to the [Sign + Data](/api-reference/endpoints/kms/signing/sign) API endpoint, + specifying the key to use. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/kms/keys//sign \ + --header 'Content-Type: application/json' \ + --data '{ + "data": "SGVsbG8sIFdvcmxkIQ==", // base64 encoded data + "signingAlgorithm": "RSASSA_PKCS1_V1_5_SHA_512", + }' + ``` + + ### Sample response + + ```bash Response + { + "signature": "JYuiBt1Ta9pbqFIW9Ou6qzBsFhjYbMJp9k4dP87ILrO+F2MPnp85g3nOlXK1ttZmRoGWsWnLNDRn9W3rf5VtkeaixPqUW/KvY/fM3CxdMyIV3BuxlGgDksjL8X34Eqkrz4CCPo9hjB5uT+rBCOxCgZqRbOdATPipAneUapI9npseNquEeh3jPklwviBix83PJHV9PW2t03AGGUXuMY55ZaFEIMv+IrI1WYdnPVIXDyIitYsS3y+/6KRfhVeTcPNJ5Rw+FE9y1eZzDEZtTNpxOfUT3QIoXmpZlYL4HbhRuJBZ+Yx54C7uPiUIN9U69XbyXt+Kkynykw2HPaagwuCZxiqCU5sFfLnrVbc3dmZxQcX2yRrs2gmFamzBx+uVbi648H4mb7WuE5UPTBjjA11jRsBjCY0YS2T4Vgfe1RlzlPQkZgjP/bnCCGDqXa3/VZAlZX1nTI51X995bPHBQI0rq2sNDlIXenwiAy1wJSITbSI8DbUx09Cr83xCEaYAE6R6PUfog/tbIUXi0VbrYsCVkAGCK446Wb1vW6q7HR8jrjXNwmXlqN9eLbSVWqdWj7N7fieeTYSrECtUaAjxtUYTIVsH2bfT6FOEM9gMWKffOpFowVzzr3B9bNQLIhnEEwebxBw947i4OcxyVIcEUuumWxoKvcbSPxzJ8v1M3SoBBh4=", // base64 encoded signature + "keyId": "62b2c14e-58af-4199-9842-02995c63edf9", + "signingAlgorithm": "RSASSA_PKCS1_V1_5_SHA_512", + } + ``` + + + To sign predigested data, you can pass `"isDigest": true` in the request body. This requires the data to be a base64 encoded digest of the data you wish to sign. + It's important that the digest is created using the same hashing algorithm as the signing algorithm. As an example, you would create the digest with `SHA512` if you are using the `RSASSA_PKCS1_V1_5_SHA_512` signing algorithm. + + + + + + +### Guide to Verifying Data + +In the following steps, we explore how to verify data using an existing key in Infisical KMS. + + + + + + Navigate to Project > Key Management and open the options menu for the key used to sign the data + you want to verify. + ![kms key options](/images/platform/kms/infisical-kms/signing/sign-options.png) + + + + Paste your signature and data into the text areas and tap on the Verify button. + ![kms verify data](/images/platform/kms/infisical-kms/signing/verify-data-modal.png) + + Your verification result will be displayed and can be copied for use. + ![kms verified data](/images/platform/kms/infisical-kms/signing/signature-verified.png) + + If the signature is invalid, you'll see an error message indicating that the signature is invalid, and the "Signature Status" field will be `Invalid`. + + + + + + + To verify data, make an API request to the [Verify + Data](/api-reference/endpoints/kms/signing/verify) API endpoint, + specifying the key to use. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/kms/keys//verify \ + --header 'Content-Type: application/json' \ + --data '{ + "data": "SGVsbG8sIFdvcmxkIQ==", // base64 encoded data + "signature": "JYuiBt1Ta9pbqFIW9Ou6qzBsFhjYbMJp9k4dP87ILrO+F2MPnp85g3nOlXK1ttZmRoGWsWnLNDRn9W3rf5VtkeaixPqUW/KvY/fM3CxdMyIV3BuxlGgDksjL8X34Eqkrz4CCPo9hjB5uT+rBCOxCgZqRbOdATPipAneUapI9npseNquEeh3jPklwviBix83PJHV9PW2t03AGGUXuMY55ZaFEIMv+IrI1WYdnPVIXDyIitYsS3y+/6KRfhVeTcPNJ5Rw+FE9y1eZzDEZtTNpxOfUT3QIoXmpZlYL4HbhRuJBZ+Yx54C7uPiUIN9U69XbyXt+Kkynykw2HPaagwuCZxiqCU5sFfLnrVbc3dmZxQcX2yRrs2gmFamzBx+uVbi648H4mb7WuE5UPTBjjA11jRsBjCY0YS2T4Vgfe1RlzlPQkZgjP/bnCCGDqXa3/VZAlZX1nTI51X995bPHBQI0rq2sNDlIXenwiAy1wJSITbSI8DbUx09Cr83xCEaYAE6R6PUfog/tbIUXi0VbrYsCVkAGCK446Wb1vW6q7HR8jrjXNwmXlqN9eLbSVWqdWj7N7fieeTYSrECtUaAjxtUYTIVsH2bfT6FOEM9gMWKffOpFowVzzr3B9bNQLIhnEEwebxBw947i4OcxyVIcEUuumWxoKvcbSPxzJ8v1M3SoBBh4=", // base64 encoded signature + "signingAlgorithm": "RSASSA_PKCS1_V1_5_SHA_512" + }' + ``` + + ### Sample response + + ```bash Response + { + "signatureValid": true, + "keyId": "62b2c14e-58af-4199-9842-02995c63edf9", + "signingAlgorithm": "RSASSA_PKCS1_V1_5_SHA_512" + } + ``` + + To verify predigested data, you can pass `"isDigest": true` in the request body. This requires the data to be a base64 encoded digest of the data you wish to verify. + It's important that the digest is created using the same hashing algorithm as the signing algorithm. As an example, you would create the digest with `SHA512` if you are using the `RSASSA_PKCS1_V1_5_SHA_512` signing algorithm. + + + + + + ## FAQ @@ -201,8 +366,76 @@ In the following steps, we explore how to use decrypt data using an existing key external sources.
- Currently, Infisical only supports `AES-128-GCM` and `AES-256-GCM` for - encryption operations. We anticipate supporting more algorithms and - cryptographic operations in the coming months. + Currently Infisical supports 4 different key algorithms with different purposes: + + - `RSA_4096`: For signing and verifying data. + - `ECC_NIST_P256`: For signing and verifying data. + + - `AES-256-GCM`: For encryption and decryption operations. + - `AES-128-GCM`: For encryption and decryption operations. + + We anticipate to further expand our supported algorithms and support cryptographic operations in the future. + + + To sign and verify a digest using the Infisical KMS, you can use the `Sign` and `Verify` endpoints respectively. + You will need to pass `"isDigest": true` in the request body to indicate that you are signing or verifying a digest. + The data you are signing or verifying will need to be a base64 encoded digest of the data you wish to sign or verify. + It's important that the digest is created using the same hashing algorithm as the signing algorithm. As an example, you would create the digest with `SHA512` if you are using the `RSASSA_PKCS1_V1_5_SHA_512` signing algorithm. + + To create a SHA512 digest of your data, you can use the following command with OpenSSL: + ```bash + echo -n "Hello, World" | openssl dgst -sha512 -binary | openssl base64 + ``` + + ### Sample request for signing a digest + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/kms/keys//sign \ + --header 'Content-Type: application/json' \ + --data '{ + "data": , + "signingAlgorithm": "RSASSA_PKCS1_V1_5_SHA_512", + "isDigest": true + }' + ``` + + ### Sample response for signing a digest + + ```bash Response + { + "signature": , + "keyId": , + "signingAlgorithm": "RSASSA_PKCS1_V1_5_SHA_512" + } + ``` + + ### Sample request for verifying a digest + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/kms/keys//verify \ + --header 'Content-Type: application/json' \ + --data '{ + "data": , + "signature": , + "signingAlgorithm": "RSASSA_PKCS1_V1_5_SHA_512", + "isDigest": true + }' + ``` + + ### Sample response for verifying a digest + + ```bash Response + { + "signatureValid": true, + "keyId": , + "signingAlgorithm": "RSASSA_PKCS1_V1_5_SHA_512" + } + ``` + + + Please note that `RSA PSS` signing algorithms are not supported for digest signing and verification. Please use `RSA PKCS1 V1.5` signing algorithms for digest signing and verification, or `ECDSA` if you're using an ECC key. +
diff --git a/docs/documentation/platform/mfa.mdx b/docs/documentation/platform/mfa.mdx index 69a84fdd4..1f61fae5c 100644 --- a/docs/documentation/platform/mfa.mdx +++ b/docs/documentation/platform/mfa.mdx @@ -4,19 +4,18 @@ sidebarTitle: "MFA" description: "Learn how to secure your Infisical account with MFA." --- -MFA requires users to provide multiple forms of identification to access their account. Currently, this means logging in with your password and a 6-digit code sent to your email. +MFA requires users to provide multiple forms of identification to access their account. ## Email 2FA -Check the box in Personal Settings > Two-factor Authentication to enable email-based 2FA. +If 2-factor authentication is enabled in the Personal settings page, email will be used for MFA by default. -![Email-based MFA](../../images/mfa-email.png) +![Email-based MFA](/images/mfa-email.png) - - Infisical currently supports email-based 2FA. We're actively working on - building support for other forms of identification via SMS and Authenticator - App. - +## Mobile Authenticator 2FA + +You can use any mobile authenticator app (Authy, Google Authenticator, Duo, etc.) to secure your account. After registration with an authenticator, select **Mobile Authenticator** as your 2FA method. +![Authenticator-based MFA](/images/mfa-authenticator.png) ## Entra ID / Azure AD MFA @@ -25,32 +24,39 @@ Check the box in Personal Settings > Two-factor Authentication to enable email-b We also encourage you to have your team download and setup the [Microsoft Authenticator App](https://www.microsoft.com/en-us/security/mobile-authenticator-app) prior to enabling MFA. + - - ![Entra Infisical app](../../images/platform/mfa/entra/mfa_entra_infisical_app.png) - - - ![conditional access](../../images/platform/mfa/entra/mfa_entra_conditional_access.png) - - - ![create policy](../../images/platform/mfa/entra/mfa_entra_create_policy.png) - - - ![require MFA and review policy](../../images/platform/mfa/entra/mfa_entra_review_policy.png) - - By default all users except the configuring admin will be setup to require MFA. - Microsoft encourages keeping at least one admin excluded from MFA to prevent accidental lockout. - - - - ![enable policy and confirm](../../images/platform/mfa/entra/mfa_entra_confirm_policy.png) - - - ![mfa login](../../images/platform/mfa/entra/mfa_entra_login.png) - - If users have not setup MFA for Entra / Azure they will be prompted to do so at this time. - - - \ No newline at end of file + + ![Entra Infisical + app](/images/platform/mfa/entra/mfa_entra_infisical_app.png) + + + ![conditional + access](/images/platform/mfa/entra/mfa_entra_conditional_access.png) + + + ![create policy](/images/platform/mfa/entra/mfa_entra_create_policy.png) + + + ![require MFA and review + policy](/images/platform/mfa/entra/mfa_entra_review_policy.png) + + By default all users except the configuring admin will be setup to require + MFA. Microsoft encourages keeping at least one admin excluded from MFA to + prevent accidental lockout. + + + + ![enable policy and + confirm](/images/platform/mfa/entra/mfa_entra_confirm_policy.png) + + + ![mfa login](/images/platform/mfa/entra/mfa_entra_login.png) + + If users have not setup MFA for Entra / Azure they will be prompted to do + so at this time. + + + diff --git a/docs/documentation/platform/pki/est.mdx b/docs/documentation/platform/pki/est.mdx index a31a5672e..ecbd98dd9 100644 --- a/docs/documentation/platform/pki/est.mdx +++ b/docs/documentation/platform/pki/est.mdx @@ -35,6 +35,7 @@ These endpoints are exposed on port 8443 under the .well-known/est path e.g. ![est enrollment modal create](/images/platform/pki/est/template-enrollment-modal.png) + - **Disable Bootstrap Certificate Validation** - Enable this if your devices are not configured with a bootstrap certificate. - **Certificate Authority Chain** - This is the certificate chain used to validate your devices' manufacturing/pre-installed certificates. This will be used to authenticate your devices with Infisical's EST server. - **Passphrase** - This is also used to authenticate your devices with Infisical's EST server. When configuring the clients, use the value defined here as the EST password. diff --git a/docs/documentation/platform/pr-workflows.mdx b/docs/documentation/platform/pr-workflows.mdx index d582c838b..187bae5d4 100644 --- a/docs/documentation/platform/pr-workflows.mdx +++ b/docs/documentation/platform/pr-workflows.mdx @@ -3,6 +3,13 @@ title: "Approval Workflows" description: "Learn how to enable a set of policies to manage changes to sensitive secrets and environments." --- + + Approval Workflows is a paid feature. + + If you're using Infisical Cloud, then it is available under the **Pro Tier** and **Enterprise Tire**. + If you're self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it. + + ## Problem at hand Updating secrets in high-stakes environments (e.g., production) can have a number of problematic issues: @@ -40,4 +47,4 @@ When a user submits a change to an enviropnment that is under a particular polic Approvers are notified by email and/or Slack as soon as the request is initiated. In the Infisical Dashboard, they will be able to `approve` and `merge` (or `deny`) a request for a change in a particular environment. After that, depending on the workflows setup, the change will be automatically propagated to the right applications (e.g., using [Infisical Kubernetes Operator](https://infisical.com/docs/integrations/platforms/kubernetes)). -![secrets update pull request](../../images/platform/pr-workflows/secret-update-pr.png) \ No newline at end of file +![secrets update pull request](../../images/platform/pr-workflows/secret-update-pr.png) diff --git a/docs/documentation/platform/project.mdx b/docs/documentation/platform/project.mdx index bd80d8ae5..dcf447169 100644 --- a/docs/documentation/platform/project.mdx +++ b/docs/documentation/platform/project.mdx @@ -3,19 +3,21 @@ title: "Projects" description: "Learn more and understand the concept of Infisical projects." --- -A project in Infisical belongs to an [organization](./organization) and contains a number of environments, folders, and secrets. -Only users and machine identities who belong to a project can access resources inside of it according to predefined permissions. +A project in Infisical belongs to an [organization](./organization) and contains a number of environments, folders, and secrets. +Only users and machine identities who belong to a project can access resources inside of it according to predefined permissions. + +Infisical also allows users to request project access. Refer to the [project access request section](./access-controls/project-access-requests) ## Project environments -For both visual and organizational structure, Infisical allows splitting up secrets into environments (e.g., development, staging, production). In project settings, such environments can be -customized depending on the intended use case. +For both visual and organizational structure, Infisical allows splitting up secrets into environments (e.g., development, staging, production). In project settings, such environments can be +customized depending on the intended use case. ![project secrets overview](../../images/platform/project/project-environments.png) ## Secrets Overview -The **Secrets Overview** page captures a birds-eye-view of secrets and [folders](./folder) across environments. +The **Secrets Overview** page captures a birds-eye-view of secrets and [folders](./folder) across environments. This is useful for comparing secrets, identifying if anything is missing, and making quick changes. ![project secrets overview](../../images/platform/project/project-secrets-overview-open.png) @@ -98,7 +100,7 @@ Then: - If users B and C fetch the secret D back, they both get the value E. - Please keep in mind that secret reminders won't work with personal overrides. + Please keep in mind that secret reminders won't work with personal overrides. ![project override secret](../../images/platform/project/project-secrets-override.png) @@ -112,4 +114,3 @@ To view the full details of each secret, you can hover over it and press on the This opens up a side-drawer: ![project secrets drawer](../../images/platform/project/project-secrets-drawer.png) - diff --git a/docs/documentation/platform/scim/azure.mdx b/docs/documentation/platform/scim/azure.mdx index 74a6c2030..0e86f6149 100644 --- a/docs/documentation/platform/scim/azure.mdx +++ b/docs/documentation/platform/scim/azure.mdx @@ -15,7 +15,7 @@ Prerequisites: - In Infisical, head to your Organization Settings > Authentication > SCIM Configuration and + In Infisical, head to your Organization Settings > Security > SCIM Configuration and press the **Enable SCIM provisioning** toggle to allow Azure to provision/deprovision users for your organization. ![SCIM enable provisioning](/images/platform/scim/scim-enable-provisioning.png) diff --git a/docs/documentation/platform/scim/jumpcloud.mdx b/docs/documentation/platform/scim/jumpcloud.mdx index ce4542035..42d33247a 100644 --- a/docs/documentation/platform/scim/jumpcloud.mdx +++ b/docs/documentation/platform/scim/jumpcloud.mdx @@ -15,7 +15,7 @@ Prerequisites: - In Infisical, head to your Organization Settings > Authentication > SCIM Configuration and + In Infisical, head to your Organization Settings > Security > SCIM Configuration and press the **Enable SCIM provisioning** toggle to allow JumpCloud to provision/deprovision users and user groups for your organization. ![SCIM enable provisioning](/images/platform/scim/scim-enable-provisioning.png) diff --git a/docs/documentation/platform/scim/okta.mdx b/docs/documentation/platform/scim/okta.mdx index 6b0bf6ccf..d33bd242d 100644 --- a/docs/documentation/platform/scim/okta.mdx +++ b/docs/documentation/platform/scim/okta.mdx @@ -15,7 +15,7 @@ Prerequisites: - In Infisical, head to your Organization Settings > Authentication > SCIM Configuration and + In Infisical, head to your Organization Settings > Security > SCIM Configuration and press the **Enable SCIM provisioning** toggle to allow Okta to provision/deprovision users and user groups for your organization. ![SCIM enable provisioning](/images/platform/scim/scim-enable-provisioning.png) diff --git a/docs/documentation/platform/scim/overview.mdx b/docs/documentation/platform/scim/overview.mdx index 232df95a1..5b8507961 100644 --- a/docs/documentation/platform/scim/overview.mdx +++ b/docs/documentation/platform/scim/overview.mdx @@ -3,11 +3,15 @@ title: "SCIM Overview" description: "Learn how to provision users for Infisical via SCIM." --- + + SCIM provisioning can only be enabled when either SAML or OIDC is setup for + the organization. + - SCIM provisioning is a paid feature. - - If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical, - then you should contact sales@infisical.com to purchase an enterprise license to use it. + SCIM provisioning is a paid feature. If you're using Infisical Cloud, then it + is available under the **Enterprise Tier**. If you're self-hosting Infisical, + then you should contact sales@infisical.com to purchase an enterprise license + to use it. You can configure your organization in Infisical to have users and user groups be provisioned/deprovisioned using [SCIM](https://scim.cloud/#Implementations2) via providers like Okta, Azure, JumpCloud, etc. @@ -20,13 +24,3 @@ SCIM providers: - [Okta SCIM](/documentation/platform/scim/okta) - [Azure SCIM](/documentation/platform/scim/azure) - [JumpCloud SCIM](/documentation/platform/scim/jumpcloud) - -**FAQ** - - - - Infisical's SCIM implementation accounts for retaining the end-to-end encrypted architecture of Infisical because we decouple the **authentication** and **decryption** steps in the platform. - - For this reason, SCIM-provisioned users are initialized but must finish setting up their account when logging in the first time by creating a master encryption/decryption key. With this implementation, IdPs and SCIM providers cannot and will not have access to the decryption key needed to decrypt your secrets. - - \ No newline at end of file diff --git a/docs/documentation/platform/secret-reference.mdx b/docs/documentation/platform/secret-reference.mdx index 119a8cefa..545ed6b3b 100644 --- a/docs/documentation/platform/secret-reference.mdx +++ b/docs/documentation/platform/secret-reference.mdx @@ -11,10 +11,11 @@ This means that updating the value of a base secret propagates directly to other ![secret referencing](../../images/platform/secret-references-imports/secret-reference.png) -Since secret referencing works by reconstructing values back on the client side, the client, be it a user, service token, or a machine identity, fetching back secrets -must be permissioned access to all base and dependent secrets. +Since secret referencing reconstructs values on the client side, any client (user, service token, or machine identity) fetching secrets must have proper permissions to access all base and dependent secrets. Without sufficient permissions, secret references will not resolve to their appropriate values. -For example, to access some secret `A` whose values depend on secrets `B` and `C` from different scopes, a client must have `read` access to the scopes of secrets `A`, `B`, and `C`. +For example, if secret A references values from secrets B and C located in different scopes, the client must have read access to all three scopes containing secrets A, B, and C. If permission to any referenced secret is missing, the reference will remain unresolved, potentially causing application errors or unexpected behavior. + +This is an important security consideration when planning your secret access strategy, especially when working with cross-environment or cross-folder references. ### Syntax @@ -28,11 +29,11 @@ Then consider the following scenarios: Here are a few more helpful examples for how to reference secrets in different contexts: -| Reference syntax | Environment | Folder | Secret Key | -| --------------------- | ----------- | ------------ | ---------- | -| `${KEY1}` | same env | same folder | KEY1 | -| `${dev.KEY2}` | `dev` | `/` (root of dev environment) | KEY2 | -| `${prod.frontend.KEY2}` | `prod` | `/frontend` | KEY2 | +| Reference syntax | Environment | Folder | Secret Key | +| ----------------------- | ----------- | ----------------------------- | ---------- | +| `${KEY1}` | same env | same folder | KEY1 | +| `${dev.KEY2}` | `dev` | `/` (root of dev environment) | KEY2 | +| `${prod.frontend.KEY2}` | `prod` | `/frontend` | KEY2 | ## Secret Imports @@ -59,4 +60,12 @@ To reorder a secret import, hover over it and drag the arrows handle to the posi ![reorder secret import](../../images/platform/secret-references-imports/secret-import-reorder.png) - + diff --git a/docs/documentation/platform/secret-rotation/auth0-client-secret.mdx b/docs/documentation/platform/secret-rotation/auth0-client-secret.mdx new file mode 100644 index 000000000..3845a3879 --- /dev/null +++ b/docs/documentation/platform/secret-rotation/auth0-client-secret.mdx @@ -0,0 +1,154 @@ +--- +title: "Auth0 Client Secret" +description: "Learn how to automatically rotate Auth0 Client Secrets." +--- + + + Due to how Auth0 client secrets are rotated, retired credentials will not be able to + authenticate with Auth0 during their [inactive period](./overview#how-rotation-works). + + This is a limitation of the Auth0 platform and cannot be + rectified by Infisical. + + +## Prerequisites + +- Create an [Auth0 Connection](/integrations/app-connections/auth0) with the required **Secret Rotation** audience and permissions + +## Create an Auth0 Client Secret Rotation in Infisical + + + + 1. Navigate to your Secret Manager Project's Dashboard and select **Add Secret Rotation** from the actions dropdown. + ![Secret Manager Dashboard](/images/secret-rotations-v2/generic/add-secret-rotation.png) + + 2. Select the **Auth0 Client Secret** option. + ![Select Auth0 Client Secret](/images/secret-rotations-v2/auth0-client-secret/select-auth0-client-secret-option.png) + + 3. Select the **Auth0 Connection** to use and configure the rotation behavior. Then click **Next**. + ![Rotation Configuration](/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-configuration.png) + + - **Auth0 Connection** - the connection that will perform the rotation of the specified application's Client Secret. + - **Rotation Interval** - the interval, in days, that once elapsed will trigger a rotation. + - **Rotate At** - the local time of day when rotation should occur once the interval has elapsed. + - **Auto-Rotation Enabled** - whether secrets should automatically be rotated once the rotation interval has elapsed. Disable this option to manually rotate secrets or pause secret rotation. + + Due to Auth0 Client Secret Rotations rotating a single credential set, auto-rotation may result in service interruptions. If you need to ensure service continuity, we recommend disabling this option. + + + + 4. Select the Auth0 application whose Client Secret you want to rotate. Then click **Next**. + ![Rotation Parameters](/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-parameters.png) + + 5. Specify the secret names that the client credentials should be mapped to. Then click **Next**. + ![Rotation Secrets Mapping](/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-secrets-mapping.png) + + - **Client ID** - the name of the secret that the application Client ID will be mapped to. + - **Client Secret** - the name of the secret that the rotated Client Secret will be mapped to. + + 6. Give your rotation a name and description (optional). Then click **Next**. + ![Rotation Details](/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-details.png) + + - **Name** - the name of the secret rotation configuration. Must be slug-friendly. + - **Description** (optional) - a description of this rotation configuration. + + 7. Review your configuration, then click **Create Secret Rotation**. + ![Rotation Review](/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-confirm.png) + + 8. Your **Auth0 Client Secret** credentials are now available for use via the mapped secrets. + ![Rotation Created](/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-created.png) + + + To create an Auth0 Client Secret Rotation, make an API request to the [Create Auth0 + Client Secret Rotation](/api-reference/endpoints/secret-rotations/auth0-client-secret/create) API endpoint. + + You will first need the **Client ID** of the Auth0 application you want to rotate the secret for. This can be obtained from the Applications dashboard. + ![Auth0 Client ID](/images/secret-rotations-v2/auth0-client-secret/auth0-app-client-id.png) + + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://us.infisical.com/api/v2/secret-rotations/auth0-client-secret \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-auth0-rotation", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "my client secret rotation", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/", + "isAutoRotationEnabled": true, + "rotationInterval": 30, + "rotateAtUtc": { + "hours": 0, + "minutes": 0 + }, + "parameters": { + "clientId": "...", + }, + "secretsMapping": { + "clientId": "AUTH0_CLIENT_ID", + "clientSecret": "AUTH0_CLIENT_SECRET" + } + }' + ``` + + + Due to Auth0 Client Secret Rotations rotating a single credential set, auto-rotation may result in service interruptions. If you need to ensure service continuity, we recommend disabling this option. + + + ### Sample response + + ```bash Response + { + "secretRotation": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-auth0-rotation", + "description": "my client secret rotation", + "secretsMapping": { + "clientId": "AUTH0_CLIENT_ID", + "clientSecret": "AUTH0_CLIENT_SECRET" + }, + "isAutoRotationEnabled": true, + "activeIndex": 0, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "rotationInterval": 30, + "rotationStatus": "success", + "lastRotationAttemptedAt": "2023-11-07T05:31:56Z", + "lastRotatedAt": "2023-11-07T05:31:56Z", + "lastRotationJobId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "nextRotationAt": "2023-11-07T05:31:56Z", + "connection": { + "app": "auth0", + "name": "my-auth0-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/" + }, + "rotateAtUtc": { + "hours": 0, + "minutes": 0 + }, + "lastRotationMessage": null, + "type": "auth0-client-secret", + "parameters": { + "clientId": "...", + } + } + } + ``` + + diff --git a/docs/documentation/platform/secret-rotation/mssql-credentials.mdx b/docs/documentation/platform/secret-rotation/mssql-credentials.mdx new file mode 100644 index 000000000..8789e4152 --- /dev/null +++ b/docs/documentation/platform/secret-rotation/mssql-credentials.mdx @@ -0,0 +1,163 @@ +--- +title: "Microsoft SQL Server Credentials" +description: "Learn how to automatically rotate Microsoft SQL Server credentials." +--- + +## Prerequisites + +1. Create a [Microsoft SQL Server Connection](/integrations/app-connections/mssql) with the required **Secret Rotation** permissions +2. Create two designated database users for Infisical to rotate the credentials for. Be sure to grant each user login permissions for the desired database with the necessary privileges their use case will require. + +An example creation statement might look like: + ```SQL + -- create server-level logins + CREATE LOGIN [infisical_user_1] WITH PASSWORD = 'my-password'; + CREATE LOGIN [infisical_user_2] WITH PASSWORD = 'my-password'; + GRANT CONNECT SQL TO [infisical_user_1]; + GRANT CONNECT SQL TO [infisical_user_2]; + + -- create database-level users with login from above + USE my_database; + CREATE USER [infisical_user_1] FOR LOGIN [infisical_user_1]; + CREATE USER [infisical_user_2] FOR LOGIN [infisical_user_2]; + + -- grant relevant permissions + GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::dbo TO [infisical_user_1]; + GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::dbo TO [infisical_user_2]; + ``` + + + To learn more about Microsoft SQL Server's permission system, please visit their [documentation](https://learn.microsoft.com/en-us/sql/t-sql/statements/grant-transact-sql?view=sql-server-ver16). + + + +## Create a Microsoft SQL Server Credentials Rotation in Infisical + + + + 1. Navigate to your Secret Manager Project's Dashboard and select **Add Secret Rotation** from the actions dropdown. + ![Secret Manager Dashboard](/images/secret-rotations-v2/generic/add-secret-rotation.png) + + 2. Select the **Microsoft SQL Server Credentials** option. + ![Select Microsoft SQL Server Credentials](/images/secret-rotations-v2/mssql-credentials/select-mssql-credentials-option.png) + + 3. Select the **Microsoft SQL Server Connection** to use and configure the rotation behavior. Then click **Next**. + ![Rotation Configuration](/images/secret-rotations-v2/mssql-credentials/mssql-credentials-configuration.png) + + - **Microsoft SQL Server Connection** - the connection that will perform the rotation of the configured database user credentials. + - **Rotation Interval** - the interval, in days, that once elapsed will trigger a rotation. + - **Rotate At** - the local time of day when rotation should occur once the interval has elapsed. + - **Auto-Rotation Enabled** - whether secrets should automatically be rotated once the rotation interval has elapsed. Disable this option to manually rotate secrets or pause secret rotation. + + 4. Input the usernames of the database users created above that will be used for rotation. Then click **Next**. + ![Rotation Parameters](/images/secret-rotations-v2/mssql-credentials/mssql-credentials-parameters.png) + + - **Database Username 1** - the username of the first user that will be used for rotation. + - **Database Username 2** - the username of the second user that will be used for rotation. + + 5. Specify the secret names that the active credentials should be mapped to. Then click **Next**. + ![Rotation Secrets Mapping](/images/secret-rotations-v2/mssql-credentials/mssql-credentials-secrets-mapping.png) + + - **Username** - the name of the secret that the active username will be mapped to. + - **Password** - the name of the secret that the active password will be mapped to. + + 6. Give your rotation a name and description (optional). Then click **Next**. + ![Rotation Details](/images/secret-rotations-v2/mssql-credentials/mssql-credentials-details.png) + + - **Name** - the name of the secret rotation configuration. Must be slug-friendly. + - **Description** (optional) - a description of this rotation configuration. + + 7. Review your configuration, then click **Create Secret Rotation**. + ![Rotation Review](/images/secret-rotations-v2/mssql-credentials/mssql-credentials-confirm.png) + + 8. Your **Microsoft SQL Server Credentials** are now available for use via the mapped secrets. + ![Rotation Created](/images/secret-rotations-v2/mssql-credentials/mssql-credentials-created.png) + + + To create a Microsoft SQL Server Credentials Rotation, make an API request to the [Create Microsoft SQL Server + Credentials Rotation](/api-reference/endpoints/secret-rotations/mssql-credentials/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://us.infisical.com/api/v2/secret-rotations/mssql-credentials \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-mssql-rotation", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "my database credentials rotation", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/", + "isAutoRotationEnabled": true, + "rotationInterval": 30, + "rotateAtUtc": { + "hours": 0, + "minutes": 0 + }, + "parameters": { + "username1": "infisical_user_1", + "username2": "infisical_user_2" + }, + "secretsMapping": { + "username": "MSSQL_DB_USERNAME", + "password": "MSSQL_DB_PASSWORD" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretRotation": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-mssql-rotation", + "description": "my database credentials rotation", + "secretsMapping": { + "username": "MSSQL_DB_USERNAME", + "password": "MSSQL_DB_PASSWORD" + }, + "isAutoRotationEnabled": true, + "activeIndex": 0, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "rotationInterval": 30, + "rotationStatus": "success", + "lastRotationAttemptedAt": "2023-11-07T05:31:56Z", + "lastRotatedAt": "2023-11-07T05:31:56Z", + "lastRotationJobId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "nextRotationAt": "2023-11-07T05:31:56Z", + "connection": { + "app": "mssql", + "name": "my-mssql-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/" + }, + "rotateAtUtc": { + "hours": 0, + "minutes": 0 + }, + "lastRotationMessage": null, + "type": "mssql-credentials", + "parameters": { + "username1": "infisical_user_1", + "username2": "infisical_user_2" + } + } + } + ``` + + diff --git a/docs/documentation/platform/secret-rotation/mssql.mdx b/docs/documentation/platform/secret-rotation/mssql.mdx deleted file mode 100644 index c34bb9034..000000000 --- a/docs/documentation/platform/secret-rotation/mssql.mdx +++ /dev/null @@ -1,139 +0,0 @@ ---- -title: "Microsoft SQL Server" -description: "Learn how to automatically rotate Microsoft SQL Server user passwords." ---- - -The Infisical SQL Server secret rotation allows you to automatically rotate your database users' passwords at a predefined interval. - -## Prerequisites - -1. Create two SQL Server logins and database users with the required permissions. We'll refer to them as `user-a` and `user-b`. -2. Create another SQL Server login with permissions to alter logins for `user-a` and `user-b`. We'll refer to this as the `admin` login. - -Here's how to set up the prerequisites: - -```sql --- Create the logins (at server level) -CREATE LOGIN [user-a] WITH PASSWORD = 'ComplexPassword1'; -CREATE LOGIN [user-b] WITH PASSWORD = 'ComplexPassword2'; - --- Create database users for the logins (in your specific database) -USE [YourDatabase]; -CREATE USER [user-a] FOR LOGIN [user-a]; -CREATE USER [user-b] FOR LOGIN [user-b]; - --- Grant necessary permissions to the users -GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::dbo TO [user-a]; -GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::dbo TO [user-b]; - --- Create admin login with permission to alter other logins -CREATE LOGIN [admin] WITH PASSWORD = 'AdminComplexPassword'; -CREATE USER [admin] FOR LOGIN [admin]; - --- Grant permission to alter any login -GRANT ALTER ANY LOGIN TO [admin]; -``` - -To learn more about SQL Server's permission system, please visit this [documentation](https://learn.microsoft.com/en-us/sql/relational-databases/security/authentication-access/getting-started-with-database-engine-permissions). - -## How it works - -1. Infisical connects to your database using the provided `admin` login credentials. -2. A random value is generated and the password for `user-a` is updated with the new value. -3. The new password is then tested by logging into the database. -4. If test is successful, it's saved to the output secret mappings so that rest of the system gets the newly rotated value(s). -5. The process is then repeated for `user-b` on the next rotation. -6. The cycle repeats until secret rotation is deleted/stopped. - -## Rotation Configuration - - - - Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar - - - - - SQL Server admin username - - - - SQL Server admin password - - - - SQL Server host url (e.g., your-server.database.windows.net) - - - - Database port number (default: 1433) - - - - Database name (default: master) - - - - The first login name to rotate - `user-a` - - - - The second login name to rotate - `user-b` - - - - Optional database certificate to connect with database - - - - - When a secret rotation is successful, the updated values needs to be saved to an existing key(s) in your project. - - - The environment where the rotated credentials should be mapped to. - - - - The secret path where the rotated credentials should be mapped to. - - - - What interval should the credentials be rotated in days. - - - - Select an existing secret key where the rotated database username value should be saved to. - - - - Select an existing select key where the rotated database password value should be saved to. - - - - - -## FAQ - - - - When a system has multiple nodes by horizontal scaling, redeployment doesn't happen instantly. - - This means that when the secrets are rotated, and the redeployment is triggered, the existing system will still be using the old credentials until the change rolls out. - - To avoid causing failure for them, the old credentials are not removed. Instead, in the next rotation, the previous user's credentials are updated. - - - - The admin account is used by Infisical to update the credentials for `user-a` and `user-b`. - - You don't need to grant all permissions for your admin account but rather just the permission to alter logins (ALTER ANY LOGIN). - - - - When using Azure SQL Database, you'll need to: - - 1. Use the full server name as your host (e.g., your-server.database.windows.net) - 2. Ensure your admin account is either the Azure SQL Server admin or an Azure AD account with appropriate permissions - 3. Configure your Azure SQL Server firewall rules to allow connections from Infisical's IP addresses - - diff --git a/docs/documentation/platform/secret-rotation/overview.mdx b/docs/documentation/platform/secret-rotation/overview.mdx index 57ad17e09..d11334440 100644 --- a/docs/documentation/platform/secret-rotation/overview.mdx +++ b/docs/documentation/platform/secret-rotation/overview.mdx @@ -6,44 +6,110 @@ description: "Learn how to set up automated secret rotation in Infisical." ## Introduction -Secret rotation is a process that involves updating secret credentials periodically to minimize the risk of their compromise. -Rotating secrets helps prevent unauthorized access to systems and sensitive data by ensuring that old credentials are replaced with new ones regularly. +Secret rotation is a security best practice that involves systematically updating credentials and access tokens at regular intervals to minimize the risk of compromise. By proactively replacing existing secrets with new ones, organizations reduce the potential impact of credential theft or leakage. -Rotated secrets may include, but are not limited to: +Examples of rotated secrets include: -1. API keys for external services; -2. Database credentials for various platforms. +- API keys and authentication tokens for cloud services and third-party integrations +- Database credentials across production, staging, and development environments -## Rotation Process +## How Rotation Works -The practice of rotating secrets is a systematic and interval-based operation, carried out in four fundamental phases. +Secret Rotation systematically replaces secrets at regular intervals while ensuring zero downtime for your applications. This overlapping lifecycle approach maintains continuous availability while enhancing your security posture. -### 1. Creation +### Visual Timeline -The system initiates the rotation process by either making an API call to an external service or generating a new secret value internally. -Upon successful creation, the system will temporarily have three versions of the secret: +```mermaid +gantt + title Credential Lifecycle (Interval = 30 days) + dateFormat YYYY-MM-DD + axisFormat %b %d -- **Current active secret**: The one currently in use. -- **Future active secret (pending)**: The newly created secret, awaiting validation. -- **Previous active secret**: The old secret, soon to be retired. + section Credentials 1 + Active :active, a1, 2023-01-01, 30d + Inactive :done, i1, after a1, 30d + Revoked :crit, r1, after i1, 30d -### 2. Testing + section Credentials 2 + Active :active, a2, 2023-01-31, 30d + Inactive :done, i2, after a2, 30d + Revoked :crit, r2, after i2, 30d -The newly generated secret is subjected to a verification process to ensure its validity and functionality. -This involves conducting checks or tests that simulate actual operations the secret would perform. -Only the current active and the future active (pending) secrets are considered operational at this stage, while the previous active secret remains in standby mode. + section Credentials 3 + Active :active, a3, 2023-03-02, 30d + Inactive :done, i3, after a3, 30d + Revoked :crit, r3, after i3, 30d +``` -### 3. Deletion +### Credential States -Post-verification, the system deactivates and deletes the previous active secret, leaving only the current and future active (pending) secrets in the system. +Each set of credentials transitions through three distinct states: -### 4. Activation +- **Active**: The primary credentials that will be used for new connections +- **Inactive**: These credentials are still valid but are no longer issued for new connections + + Some rotation providers utilize a single credential set due to technical constraints. As a result, inactive credentials for these providers will immediately become invalid once rotated. -Finally, the system promotes the future active (pending) secret to be the new current active secret. It then triggers necessary side effects, such as invoking webhooks and generating events, to notify other services of the change. + To avoid service interruptions, Infisical recommends manually rotating these credentials to prevent downtime. + +- **Revoked**: Permanently invalidated and deleted from the system + +### Rotation Cycle Example (30-Day Interval) + +Using a __30-Day__ rotation interval as an example, here's how the process unfolds: + +1. __Day 0__ + - `Credential set 1` is issued and set to **Active** + - Applications begin using this set for authentication + +2. __Day 30__ + - `Credential set 2` is issued and set to **Active** + - `Credential set 1` transitions to **Inactive** but remains valid + - New connections utilize set 2 while existing connections with set 1 continue to work + + + This overlapping validity period ensures that at any point during the active period of a credential set, you are guaranteed that retrieved credentials will be valid for the specified rotation period. + + +3. __Day 60__ + - `Credential set 3` is issued and set to **Active** + - `Credential set 2` transitions to **Inactive** but remains valid + - `Credential set 1` is **Revoked** and securely deleted + - By now, all applications should have transitioned to using set 2 or 3 + +4. __Day 90__ + - `Credential set 4` is issued and set to **Active** + - `Credential set 3` transitions to **Inactive** but remains valid + - `Credential set 2` is **Revoked** and securely deleted + - The cycle continues... + +### Benefits of This Approach + +- **Zero Downtime**: Applications always have valid credentials +- **Grace Period**: The inactive period gives applications time to update to new credentials +- **Reduced Risk**: Credentials are regularly cycled, limiting the impact of potential compromise +- **Predictable Schedule**: Makes credential management more systematic and easier to automate + +### Implementation Considerations + +- Choose a rotation interval appropriate for your security requirements and operational needs +- Ensure your applications can handle credential updates gracefully +- Monitor for applications still using credentials nearing revocation ## Infisical Secret Rotation Strategies -1. [SendGrid Integration](./sendgrid) -2. [PostgreSQL/CockroachDB Implementation](./postgres) -3. [MySQL/MariaDB Configuration](./mysql) -4. [AWS IAM User](./aws-iam) +- [PostgreSQL Credentials](./postgres) +- [Microsoft SQL Server Credentials](./mssql) + +## FAQ + + + + Some credential providers have limitations that affect rotation patterns: + + - The third-party provider's API only supports managing one active credential set at a time + - The specific use-case (such as personal login accounts) is inherently limited to a single active credential + + In either scenario, when service continuity is critical, Infisical recommends disabling auto-rotation and performing manual credential rotation during scheduled maintenance windows. + + diff --git a/docs/documentation/platform/secret-rotation/postgres-credentials.mdx b/docs/documentation/platform/secret-rotation/postgres-credentials.mdx new file mode 100644 index 000000000..e0606e6ab --- /dev/null +++ b/docs/documentation/platform/secret-rotation/postgres-credentials.mdx @@ -0,0 +1,160 @@ +--- +title: "PostgreSQL Credentials" +description: "Learn how to automatically rotate PostgreSQL credentials." +--- + +## Prerequisites + +1. Create a [PostgreSQL Connection](/integrations/app-connections/postgres) with the required **Secret Rotation** permissions +2. Create two designated database users for Infisical to rotate the credentials for. Be sure to grant each user login permissions for the desired database with the necessary privileges their use case will require. + + An example creation statement might look like: + ```SQL + -- create user roles + CREATE USER infisical_user_1 WITH ENCRYPTED PASSWORD 'temporary_password'; + CREATE USER infisical_user_2 WITH ENCRYPTED PASSWORD 'temporary_password'; + + -- grant database connection permissions + GRANT CONNECT ON DATABASE my_database TO infisical_user_1; + GRANT CONNECT ON DATABASE my_database TO infisical_user_2; + + -- grant relevant table permissions + GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO infisical_user_1; + GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO infisical_user_2; + ``` + + + To learn more about PostgreSQL's permission system, please visit their [documentation](https://www.postgresql.org/docs/current/sql-grant.html). + + + +## Create a PostgreSQL Credentials Rotation in Infisical + + + + 1. Navigate to your Secret Manager Project's Dashboard and select **Add Secret Rotation** from the actions dropdown. + ![Secret Manager Dashboard](/images/secret-rotations-v2/generic/add-secret-rotation.png) + + 2. Select the **PostgreSQL Credentials** option. + ![Select PostgreSQL Credentials](/images/secret-rotations-v2/postgres-credentials/select-postgres-credentials-option.png) + + 3. Select the **PostgreSQL Connection** to use and configure the rotation behavior. Then click **Next**. + ![Rotation Configuration](/images/secret-rotations-v2/postgres-credentials/postgres-credentials-configuration.png) + + - **PostgreSQL Connection** - the connection that will perform the rotation of the configured database user credentials. + - **Rotation Interval** - the interval, in days, that once elapsed will trigger a rotation. + - **Rotate At** - the local time of day when rotation should occur once the interval has elapsed. + - **Auto-Rotation Enabled** - whether secrets should automatically be rotated once the rotation interval has elapsed. Disable this option to manually rotate secrets or pause secret rotation. + + 4. Input the usernames of the database users created above that will be used for rotation. Then click **Next**. + ![Rotation Parameters](/images/secret-rotations-v2/postgres-credentials/postgres-credentials-parameters.png) + + - **Database Username 1** - the username of the first user that will be used for rotation. + - **Database Username 2** - the username of the second user that will be used for rotation. + + 5. Specify the secret names that the active credentials should be mapped to. Then click **Next**. + ![Rotation Secrets Mapping](/images/secret-rotations-v2/postgres-credentials/postgres-credentials-secrets-mapping.png) + + - **Username** - the name of the secret that the active username will be mapped to. + - **Password** - the name of the secret that the active password will be mapped to. + + 6. Give your rotation a name and description (optional). Then click **Next**. + ![Rotation Details](/images/secret-rotations-v2/postgres-credentials/postgres-credentials-details.png) + + - **Name** - the name of the secret rotation configuration. Must be slug-friendly. + - **Description** (optional) - a description of this rotation configuration. + + 7. Review your configuration, then click **Create Secret Rotation**. + ![Rotation Review](/images/secret-rotations-v2/postgres-credentials/postgres-credentials-confirm.png) + + 8. Your **PostgreSQL Credentials** are now available for use via the mapped secrets. + ![Rotation Created](/images/secret-rotations-v2/postgres-credentials/postgres-credentials-created.png) + + + To create a PostgreSQL Credentials Rotation, make an API request to the [Create PostgreSQL + Credentials Rotation](/api-reference/endpoints/secret-rotations/postgres-credentials/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://us.infisical.com/api/v2/secret-rotations/postgres-credentials \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-pg-rotation", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "my database credentials rotation", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/", + "isAutoRotationEnabled": true, + "rotationInterval": 30, + "rotateAtUtc": { + "hours": 0, + "minutes": 0 + }, + "parameters": { + "username1": "infisical_user_1", + "username2": "infisical_user_2" + }, + "secretsMapping": { + "username": "POSTGRES_DB_USERNAME", + "password": "POSTGRES_DB_PASSWORD" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretRotation": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-pg-rotation", + "description": "my database credentials rotation", + "secretsMapping": { + "username": "POSTGRES_DB_USERNAME", + "password": "POSTGRES_DB_PASSWORD" + }, + "isAutoRotationEnabled": true, + "activeIndex": 0, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "rotationInterval": 30, + "rotationStatus": "success", + "lastRotationAttemptedAt": "2023-11-07T05:31:56Z", + "lastRotatedAt": "2023-11-07T05:31:56Z", + "lastRotationJobId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "nextRotationAt": "2023-11-07T05:31:56Z", + "connection": { + "app": "postgres", + "name": "my-pg-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/" + }, + "rotateAtUtc": { + "hours": 0, + "minutes": 0 + }, + "lastRotationMessage": null, + "type": "postgres-credentials", + "parameters": { + "username1": "infisical_user_1", + "username2": "infisical_user_2" + } + } + } + ``` + + diff --git a/docs/documentation/platform/secret-rotation/postgres.mdx b/docs/documentation/platform/secret-rotation/postgres.mdx deleted file mode 100644 index 0a6339e4e..000000000 --- a/docs/documentation/platform/secret-rotation/postgres.mdx +++ /dev/null @@ -1,104 +0,0 @@ ---- -title: "PostgreSQL/CockroachDB" -description: "Learn how to automatically rotate PostgreSQL/CockroachDB user passwords." ---- - -The Infisical Postgres secret rotation allows you to automatically rotate your Postgres database user's password at a predefined interval. - - -## Prerequisite - -1. Create two users with the required permission in your PostgreSQL instance. We'll refer to them as `user-a` and `user-b`. -2. Create another PostgreSQL user with just the permission to update the passwords of `user-a` and `user-b`. We'll refer to this user as the `admin` user. - -To learn more about Postgres permission system, please visit this [documentation](https://www.postgresql.org/docs/9.1/sql-grant.html). - - -## How it works - -1. Infisical connects to your database using the provided `admin` user account. -2. A random value is generated and the password for `user-a` is updated with the new value. -3. The new password is then tested by logging into the database -4. If test is success, it's saved to the output secret mappings so that rest of the system gets the newly rotated value(s). -5. The process is then repeated for `user-b` on the next rotation. -6. The cycle repeats until secret rotation is deleted/stopped. - -## Rotation Configuration - - - - Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar - - - - - - Rotator admin username - - - - Rotator admin password - - - - Database host url - - - - Database port number - - - - The first username of two to rotate - `user-a` - - - - The second username of two to rotate - `user-b` - - - - Optional database certificate to connect with database - - - - - When a secret rotation is successful, the updated values needs to be saved to an existing key(s) in your project. - - - The environment where the rotated credentials should be mapped to. - - - - The secret path where the rotated credentials should be mapped to. - - - - What interval should the credentials be rotated in days. - - - - Select an existing secret key where the rotated database username value should be saved to. - - - - Select an existing select key where the rotated database password value should be saved to. - - - - -## FAQ - - - - When a system has multiple nodes by horizontal scaling, redeployment doesn't happen instantly. - - This means that when the secrets are rotated, and the redeployment is triggered, the existing system will still be using the old credentials until the change rolls out. - - To avoid causing failure for them, the old credentials are not removed. Instead, in the next rotation, the previous user's credentials are updated. - - - The admin account is used by Infisical to update the credentials for `user-a` and `user-b`. - - You don't need to grant all permission for your admin account but rather just the permissions to update both of the user's passwords. - - diff --git a/docs/documentation/platform/secret-scanning.mdx b/docs/documentation/platform/secret-scanning.mdx new file mode 100644 index 000000000..4f030e882 --- /dev/null +++ b/docs/documentation/platform/secret-scanning.mdx @@ -0,0 +1,68 @@ +--- +title: 'Secret Scanning' +description: "Scan and prevent secret leaks in your code repositories" +--- + +The Infisical Secret Scanner allows you to keep an overview and stay alert of exposed secrets across your entire GitHub organization and repositories. + +To further enhance security, we recommend you also use our [CLI Secret Scanner](/cli/scanning-overview#automatically-scan-changes-before-you-commit) to scan for exposed secrets prior to pushing your changes. + +## Code Scanning + +![Scanning Overview](/images/platform/secret-scanning/overview.png) + +Secret scans are built on event-driven architecture. This means that every time a push is made to one of your selected repositories, Infisical will scan the modified files for any exposed secrets. + +If one or more exposed secrets are detected, it will be displayed in your Infisical dashboard. An exposed secret is known as a **"Risk"**. Each risk has the following data associated with it: +- **Date**: When the risk was first detected. +- **Secret Type**: Which type of secret was detected. +- **Info**: Information about the secret, such as the repository, file name, and the committer who made the change. + +Once an exposed secret is detected, all organization admins will be sent an e-mail notification containing details about the exposed secret. + + + Each risk also contains a "View Exposed Secret" button, which will take you directly to the GitHub commit and to the line where the secret was exposed. + + + + +![Exposed Secret](/images/platform/secret-scanning/exposed-secret.png) + + +## Responding to Exposed Secrets + +After an exposed secret is detected, it will be marked as `Needs Attention`. When there are risks marked as needs attention, it's important to address them as soon as possible. + +You can mark the risk as `Resolved` by changing the status to one of the following states: +- **This Is a False Positive**: The secret was not exposed, but was detected by the scanner. +- **I Have Rotated The Secret**: The secret was exposed, but it has now been removed. +- **No Rotation Needed**: You are choosing to ignore this risk. You may choose to do this if the risk is non-sensitive or otherwise not a security risk. + +![Needs Attention](/images/platform/secret-scanning/needs-attention.png) + + + + +## Ignoring Known Secrets +If you're intentionally committing a test secret that the secret scanner might flag, you can instruct Infisical to overlook that secret with the methods listed below. + +### infisical-scan:ignore + +To ignore a secret contained in line of code, simply add `infisical-scan:ignore ` at the end of the line as comment in the given programming. + +```js example.js +function helloWorld() { + console.log("8dyfuiRyq=vVc3RRr_edRk-fK__JItpZ"); // infisical-scan:ignore +} +``` + +### .infisicalignore +An alternative method to exclude specific findings involves creating a .infisicalignore file at your repository's root. +You can then add the fingerprints of the findings you wish to exclude. The [Infisical scan](/cli/scanning-overview) report provides a unique Fingerprint for each secret found. +By incorporating these Fingerprints into the .infisicalignore file, Infisical will skip the corresponding secret findings in subsequent scans. + +```.ignore .infisicalignore +bea0ff6e05a4de73a5db625d4ae181a015b50855:frontend/components/utilities/attemptLogin.js:stripe-access-token:147 +bea0ff6e05a4de73a5db625d4ae181a015b50855:backend/src/json/integrations.json:generic-api-key:5 +1961b92340e5d2613acae528b886c842427ce5d0:frontend/components/utilities/attemptLogin.js:stripe-access-token:148 +``` diff --git a/docs/documentation/platform/ssh-old.mdx b/docs/documentation/platform/ssh-old.mdx new file mode 100644 index 000000000..9e9e8aac4 --- /dev/null +++ b/docs/documentation/platform/ssh-old.mdx @@ -0,0 +1,363 @@ +--- +title: "Infisical SSH" +sidebarTitle: "Infisical SSH" +description: "Learn how to generate SSH credentials to provide secure and centralized SSH access control for your infrastructure." +--- + +## Concept + +Infisical can be used to issue SSH credentials to clients to provide short-lived, secure SSH access to infrastructure; +this improves on many limitations of traditional SSH key-based authentication via mitigation of private key compromise, static key management, +unauthorized access, and SSH key sprawl. + +The following concepts are useful to know when working with Infisical SSH: + +- SSH Certificate Authority (CA): A trusted authority that issues SSH certificates. +- Certificate Template: A set of policies bound to an SSH CA for certificates issued under that template; a CA can possess multiple templates, each with different policies for a different purpose (e.g. for admin versus developer access). +- SSH Certificate: A short-lived, credential issued by the SSH CA granting time-bound access to infrastructure. + +
+ +```mermaid +graph TD + A[SSH CA] + A --> B[Certificate Template A] + A --> C[Certificate Template N] + B --> D[SSH Certificate A] + C --> E[SSH Certificate N] + +``` + +
+ +When using Infisical SSH to provision client access to a remote host, an operator must create an SSH CA in Infisical; a certificate template under it, +specifying policies such as allowed users that can be requested under that template by a client; and configure the host to trust certificates issued by the Infisical SSH CA. + +When a client needs access to a host, they authenticate with Infisical and request an SSH certificate (and optionally key pair) +to be used to access the host for a time-bound session as part of the SSH operation. + +## Client Workflow + +The following sequence diagram illustrates the client workflow for accessing a remote host using an SSH certificate (and optionally key pair) +supplied by Infisical. + +```mermaid +sequenceDiagram + participant Client as Client + participant Infisical as Infisical (SSH CA) + participant Host as Remote Host + + Note over Client,Client: Step 1: Client Authentication with Infisical + Client->>Infisical: Send credential(s) to authenticate with Infisical + + Infisical-->>Client: Return access token + + Note over Client,Infisical: Step 2: SSH Certificate Request + Client->>Infisical: Make authenticated request for SSH certificate via either /api/v1/ssh/issue or /api/v1/ssh/sign + + Infisical-->>Client: Return signed SSH certificate (and optionally key pair) + + Note over Client,Client: Step 3: SSH Operation + Client->>Host: SSH into Host using the SSH certificate + + Host-->>Client: Grant access to the host +``` + +At a high-level, Infisical issues a signed SSH certificate to a client that can be used to access a remote host. + +To be more specific: + +1. The client authenticates with Infisical; this can be done using a user or machine identity [authentication method](/documentation/platform/identities/machine-identities) or a user [authentication method](/documentation/platform/identities/user-identities). +2. The client makes an authenticated request for an SSH certificate via either the `/api/v1/ssh/issue` or `/api/v1/ssh/sign` endpoints. Note that if the client wishes to use an existing SSH key pair, it can use the `/api/v1/ssh/sign` endpoint; otherwise, it can use the `/api/v1/ssh/issue` endpoint to have Infisical issue a new SSH key pair along with the certificate. +3. The client uses the issued SSH certificate (and potentially SSH key pair) to temporarily access the host. + + + Note that the workflow above requires an operator to perform additional + configuration on the remote host to trust SSH certificates issued by + Infisical. + + +## Guide to Configuring Infisical SSH + +In the following steps, we explore how to configure Infisical SSH to start issuing SSH certificates to clients as well as a remote host to trust these certificates +as part of the SSH operation. + + + + 1.1. Start by creating an SSH project in the SSH tab of your organization. + + ![ssh project create](/images/platform/ssh/ssh-project.png) + + 1.2. Next, create an SSH CA in the **Certificate Authorities** tab of the + project; this CA will be used for client key signing. + + ![ssh create client ca](/images/platform/ssh/ssh-client-create-ca-1.png) + + ![ssh create client ca popup](/images/platform/ssh/ssh-client-create-ca-2.png) + + Here's some guidance on each field: + + - Friendly Name: A friendly name for the CA; this is only for display. + - Key Source: Whether the CA's key pair should be generated internally or supplied from an external source. Select **Internal**. + - Key Algorithm: The type of public key algorithm and size, in bits, of the key pair for the CA. Supported key algorithms are `RSA 2048`, `RSA 4096`, `ECDSA P-256`, and `ECDSA P-384` with the default being `RSA 2048`. + + + + + 2.1. Next, create a certificate template in the **Certificate Templates** section of the newly-created CA. + + A certificate template is a set of policies for certificates issued under that template; each template is bound to a specific CA. + + With certificate templates, you can specify, for example, that certificates issued under a template are only allowed for users with a specific username like `ec2-user` or perhaps that the max TTL requested cannot exceed 1 hour. + + ![ssh client create template](/images/platform/ssh/ssh-client-create-template-1.png) + + ![ssh client create template popup](/images/platform/ssh/ssh-client-create-template-2.png) + + Here's some guidance on each field: + + - SSH Template Name: A name for the certificate template; this must be a valid slug. + - Allowed Users: A comma-separated list of valid usernames (e.g. `ec2-user`) on the remote host for which a client can request a certificate for. If you wish to allow a client to request a certificate for any username, set this to `*`; alternatively, if left blank, the template will not allow issuance of certificates under any username. + - Allowed Hosts: A comma-separated list of valid hostnames/domains on the remote host for which a client can request a certificate for. Each item in the list can be either a wildcard hostname (e.g. `*.acme.com`), a specific hostname (e.g. `example.com`), an IPv4 address (e.g. `192.168.1.1`), or an IPv6 address. If left empty, the template will not allow any hostnames; if set to `*`, the template will allow any hostname. + - Default TTL: The default Time-to-Live (TTL) for certificates issued under this template when a client does not explicitly specify a TTL in the certificate request. We recommend setting a shorter **Default TTL** for client certificates such as `30m`. + - Max TTL: The maximum TTL for certificates issued under this template. + - Allow User Certificates: Whether or not to allow issuance of user certificates; this should be set to `true`. + - Allow Host Certificates: Whether or not to allow issuance of host certificates; this is not relevant for this step. + - Allow Custom Key IDs: Whether or not to allow clients to specify a custom key ID to be included on the certificate as part of the certificate request. + + 2.2. Finally, add the user(s) you wish to be able to request an SSH certificate to the SSH project through the **Access Control** tab. + + + + + 3.1. Begin by downloading the client CA's public key from the CA's details section. + + ![ssh ca public key](/images/platform/ssh/ssh-client-ca-public-key.png) + + + The CA's public key can also be retrieved programmatically via API by making a `GET` request to the endpoint [here](/api-reference/endpoints/ssh/ca/public-key). + + + 3.2. Next, create a file containing this public key in the SSH folder of the remote host; we'll call the file `ca.pub`. + + This would result in the file at the path `/etc/ssh/ca.pub`. + + 3.3. Next, add the following lines to the `/etc/ssh/sshd_config` file on the remote host. + + ```bash + TrustedUserCAKeys /etc/ssh/ca.pub + + PubkeyAcceptedKeyTypes=+ssh-rsa,ssh-rsa-cert-v01@openssh.com + ``` + + 3.4. Finally, reload the SSH daemon on the remote host to apply the changes. + + ```bash + sudo systemctl reload sshd + ``` + + At this point, the remote host is configured to trust SSH certificates issued by the Infisical SSH CA. + + + + +## Guide to Using Infisical SSH to Access a Host + +In the following steps, we show how to obtain an SSH certificate and use it for a client to access a host via CLI: + + + The subsequent guide assumes the following prerequisites: + +- SSH Agent is running: The `ssh-agent` must be actively running on the host machine. +- OpenSSH is installed: The system should have OpenSSH installed; this includes + both the `ssh` client and `ssh-agent`. +- `SSH_AUTH_SOCK` environment variable + is set; the `SSH_AUTH_SOCK` variable should point to the UNIX socket that + `ssh-agent` uses for communication. + + + + + + +```bash +infisical login +``` + + + + Run the `infisical ssh issue-credentials` command, specifying the `--addToAgent` flag to automatically load the SSH certificate into the SSH agent. + ```bash + infisical ssh issue-credentials --certificateTemplateId= --principals= --addToAgent + ``` + + Here's some guidance on each flag: + + - `certificateTemplateId`: The ID of the certificate template to use for issuing the SSH certificate. + - `principals`: The comma-delimited username(s) or hostname(s) to include in the SSH certificate. + + For fuller documentation on commands and flags supported by the Infisical CLI for SSH, refer to the docs [here](/cli/commands/ssh). + + + + Finally, SSH into the desired host; the SSH operation will be performed using the SSH certificate loaded into the SSH agent. + + ```bash + ssh username@hostname + ``` + + + + + + Note that the above workflow can be executed via API or other client methods + such as SDK. + + +## Guide to Configuring Host Key Signing + +In the following steps, we show how to configure host key signing for clients to verify the identity of a remote host before attempting the SSH operation; this is recommended to reduce the probability of a client accessing a malicious machine. + + +This guide expects that the remote host already has an existing SSH key pair (typically found in the `/etc/ssh/` folder at `/etc/ssh/ssh_host__key` and `.pub`). + +If the remote host does not have an existing SSH key pair, you can generate a new key pair using the `ssh-keygen` command: `ssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N ''`. This will generate: + +- A private key: `/etc/ssh/ssh_host_rsa_key`. +- A public key: `/etc/ssh/ssh_host_rsa_key.pub`. + + + + + + 1.1. In the same SSH project, create another SSH CA in the **Certificate Authorities** tab; this CA will be used for host key signing. + + ![ssh create host ca](/images/platform/ssh/ssh-host-create-ca-1.png) + + ![ssh create host ca popup](/images/platform/ssh/ssh-host-create-ca-2.png) + + Here's some guidance on each field: + + - Friendly Name: A friendly name for the CA; this is only for display. + - Key Source: Whether the CA's key pair should be generated internally or supplied from an external source. Select **External**. + - Public Key: The public key for the CA (i.e. the host's SSH public key). + - Private Key: The private key for the CA (i.e. the host's SSH private key). + + + + + 2.1. Next, create a certificate template in the **Certificate Templates** section of the newly-created CA. + + ![ssh host create template](/images/platform/ssh/ssh-host-create-template-1.png) + + ![ssh host create template popup](/images/platform/ssh/ssh-host-create-template-2.png) + + Here's some guidance on each field: + + - SSH Template Name: A name for the certificate template; this must be a valid slug. + - Allowed Users: A comma-separated list of valid usernames (e.g. `ec2-user`) on the remote host for which a client can request a certificate for. If you wish to allow a client to request a certificate for any username, set this to `*`; alternatively, if left blank, the template will not allow issuance of certificates under any username. + - Allowed Hosts: A comma-separated list of valid hostnames/domains on the remote host for which a client can request a certificate for. Each item in the list can be either a wildcard hostname (e.g. `*.acme.com`), a specific hostname (e.g. `example.com`), an IPv4 address (e.g. `192.168.1.1`), or an IPv6 address. If left empty, the template will not allow any hostnames; if set to `*`, the template will allow any hostname. + - Default TTL: The default Time-to-Live (TTL) for certificates issued under this template when a client does not explicitly specify a TTL in the certificate request. We recommend setting a longer **Default TTL** for host certificates such as `2y`. + - Max TTL: The maximum TTL for certificates issued under this template. + - Allow User Certificates: Whether or not to allow issuance of user certificates; this is not relevant for this step. + - Allow Host Certificates: Whether or not to allow issuance of host certificates; this should be set to `true`. + - Allow Custom Key IDs: Whether or not to allow clients to specify a custom key ID to be included on the certificate as part of the certificate request. + + + + + 3.1. Obtain an SSH certificate for the host by requesting one from the **Certificates** tab. + + ![ssh host issue certificate 1](/images/platform/ssh/ssh-host-issue-cert-1.png) + + ![ssh host issue certificate 2](/images/platform/ssh/ssh-host-issue-cert-2.png) + + + You should select **Sign SSH Key** under the **Operation** field. + + Then input your host's SSH public key under the **SSH Public Key** field and hostname under the **Principal(s)** field; the host's public key should be in the `/etc/ssh` folder of the host as used in step 1. + + + ![ssh host issue certificate 3](/images/platform/ssh/ssh-host-issue-cert-3.png) + + 3.2. Create a file containing the certificate in the SSH folder of the remote host; we'll call it `ssh_host_key-cert.pub`. + + 3.3. Set permissions on the certificate to be `0640`: + + ```bash + sudo chmod 0640 /etc/ssh/ssh_host_key-cert.pub + ``` + + 3.4. Next, add the following lines to the `/etc/ssh/sshd_config` file on the remote host. + + ```bash + HostKey /etc/ssh/ssh_host_rsa_key + HostCertificate /etc/ssh/ssh_host_key-cert.pub + ``` + + + You should adjust the `HostKey` directive to match the path to the host's SSH private key as used in step 1. + + + 3.5. Finally, reload the SSH daemon on the remote host to apply the changes. + + ```bash + sudo systemctl reload sshd + ``` + + + + 4.1. Begin by downloading the host CA's public key from the CA's details section. + + ![ssh host ca public key](/images/platform/ssh/ssh-host-ca-public-key.png) + + + The CA's public key can also be retrieved programmatically via API by making a `GET` request to the endpoint [here](/api-reference/endpoints/ssh/ca/public-key). + + + 4.2. Next, add the resulting public key to the `known_hosts` file on the client machine (e.g. at the path `~/.ssh/known_hosts`). + + ```bash + @cert-authority *.example.com ssh-rsa ... + ``` + + + + Finally, SSH into the desired host as usual; the SSH operation will now also include client-side host verification. + + ```bash + ssh username@hostname + ``` + + + + +## FAQ + + + + After configuring Infisical SSH, you can add the `-vvv` flag as part of the + SSH operation to see verbose output from the SSH client. + + ```bash + ssh -vvv username@hostname + ``` + + You should see output from the SSH client that includes the following if both client key signing and host key signing are working: + + Host certificate was verified and trusted: + + ```bash + debug1: Host 'example.com' is known and matches the ECDSA-CERT host certificate. + debug1: Found CA key in /Users/user/.ssh/known_hosts:1 + ``` + + You authenticated with your user certificate: + + ```bash + debug1: Offering public key: Added via Infisical CLI RSA-CERT SHA256:... + debug1: Server accepts key: Added via Infisical CLI RSA-CERT SHA256:... + ``` + + + diff --git a/docs/documentation/platform/ssh.mdx b/docs/documentation/platform/ssh.mdx new file mode 100644 index 000000000..ae1e43df5 --- /dev/null +++ b/docs/documentation/platform/ssh.mdx @@ -0,0 +1,179 @@ +--- +title: "Infisical SSH" +sidebarTitle: "Infisical SSH" +description: "Learn how to securely provision user SSH access to your infrastructure using SSH certificates." +--- + +## Concept + +Infisical SSH can be configured to provide users on your team short-lived, secure SSH access to infrastructure. Under the hood, it uses SSH certificates +and improves upon traditional SSH key-based authentication by mitigating private key compromise, static key management, +unauthorized access, and SSH key sprawl. + +The following entities and concepts are important to understand when using Infisical SSH: + +- Administrator: An individual on your team who is responsible for configuring Infisical SSH. +- Users: Other individuals on your team that need access to the remote host. +- Host: A remote machine (e.g. EC2 instance, GCP VM, Azure VM, on-prem Linux server, Raspberry Pi, VMware VM, etc.) that users need SSH access to that is registered with Infisical SSH. + +## Workflow + +The typical workflow for using Infisical SSH consists of the following steps: + +1. The administrator registers a remote host with Infisical using the Infisical CLI via the `infisical ssh add-host` command. +2. The administrator configures Infisical SSH to grant users access to the remote host. +3. User(s) access the remote host using the Infisical CLI via the `infisical ssh connect` command. + +## Admin Guide for Configuring Infisical SSH + +In the following steps, we explore how to configure Infisical SSH to control and streamline your team's SSH access to infrastructure. As part of this guide, +we will register a remote host with Infisical through a [machine identity](/documentation/platform/identities/machine-identities) and configure Infisical to grant user(s) access to the remote host. + + + + 1.1. Start by creating a new Infisical SSH project in Infisical. + + ![ssh project create](/images/platform/ssh/v2/ssh-create-project.png) + + 1.2. Create a custom role in the project under Access Control > Project Roles to grant the machine identity that we will create in step 2 the ability to **Create** and **Issue Host Certificates** on the **SSH Host** resource; this will enable the linked machine identity to bootstrap a remote host with Infisical + and establish the necessary configuration on it. + + ![ssh custom role bootstrap 1](/images/platform/ssh/v2/ssh-add-bootstrap-role-1.png) + + ![ssh custom role bootstrap 2](/images/platform/ssh/v2/ssh-add-bootstrap-role-2.png) + + + 2.1. Follow the instructions [here](/documentation/platform/identities/universal-auth) to configure a [machine identity](/documentation/platform/identities/machine-identities) in Infisical with Universal Auth. + + By the end of this step, you should have a **Client ID** and **Client Secret** on hand as part of the Universal Auth configuration for the identity to authenticate with Infisical + as part of registering a remote host in step 3. + + + You may use other authentication methods as suitable (e.g. [AWS Auth](/documentation/platform/identities/aws-auth), [Azure Auth](/documentation/platform/identities/azure-auth), [GCP Auth](/documentation/platform/identities/gcp-auth), etc.) as part of the machine identity configuration but, to keep this example simple, we will be using Universal Auth. + + + 2.2. Add the machine identity to the Infisical SSH project you created in the previous step and assign it the custom role you created in step 1.2. + + ![ssh add identity to project](/images/platform/ssh/v2/ssh-add-identity-to-project.png) + + + + 3.1. Follow the instructions [here](/cli/overview) to install the Infisical CLI onto the remote host. + + 3.2. Run the commands below to register the remote host with Infisical. + + Use the **Client ID** and **Client Secret** from the machine identity you created in step 2.1 as part of the `infisical login` command + to obtain an access token and save it as an environment variable. + + ```bash + export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id= --client-secret= --silent --plain) + ``` + + Next, use the `infisical ssh add-host` command to register the remote host with Infisical. As part of this command, input the ID of the Infisical SSH project you created in step 1 for the `--projectId` flag and the hostname of the remote host for the `--hostname` flag. + + ```bash + sudo infisical ssh add-host --projectId= --hostname= --token="$INFISICAL_TOKEN" --writeUserCaToFile --writeHostCertToFile --configureSshd + ``` + + + Note that if you're self-hosting Infisical, you can use the `--domain` flag on the `infisical login` command to specify the domain of your Infisical instance. + + For more information on the `infisical ssh add-host` command, please refer to the Infisical CLI [documentation](/cli/overview). + + + If successful, you should see output similar to the following: + + ```bash + ✅ Successfully registered host: + 📁 Wrote User CA public key to: /etc/ssh/infisical_user_ca.pub + 📁 Wrote host certificate to: /etc/ssh/ssh_host_ed25519_key-cert.pub + 📄 Updated sshd_config entries + ``` + + Finally, use the following command to reload the SSH daemon on the remote host to apply the changes: + + ```bash + sudo systemctl reload sshd + ``` + + + The command may differ depending on the host. For older versions of Ubuntu/Debian/CentOS, you may need to use `sudo service ssh reload` instead; + for Alpine or minimal systems, `/etc/init.d/sshd reload`. + + + Back in Infisical, you should now see the remote host you just registered in the Infisical SSH project you created in step 1 under the **Hosts** tab. + + ![ssh hosts](/images/platform/ssh/v2/ssh-added-hosts.png) + + + + 4.1. Add the user(s) you wish to grant access to the remote host to the Infisical SSH project under Access Control > Users. + + ![ssh hosts](/images/platform/ssh/v2/ssh-add-user.png) + + 4.2. On the registered host in the **Hosts** tab, click **Edit SSH Host** and add a login mapping for the user(s) you added in step 4.1. + + The login mapping dictates what user(s) will be allowed access to the remote host and under a specific login user; in the allowed principals, + you should select user(s) part of the Infisical SSH project that will be allowed to login to the remote host as the login user. + + For instance, if you add a mapping with the login user `ec2-user` to some users John and Alice in Infisical, then they will be allowed to login to the remote host as `ec2-user` which is a system user that + exists on the remote host. + + ![ssh host mappings](/images/platform/ssh/v2/ssh-host-login-mappings.png) + + + Note that you should configure authorized principals files for each login user you add to the remote host. + + + + + +## User Guide for SSHing to a Host + +Once Infisical SSH is configured by an administrator, users can SSH to the remote host using the Infisical CLI. + + + + Follow the instructions [here](/cli/overview) to install the Infisical CLI onto your local machine. + + + Run the `infisical login` command to authenticate with Infisical. + + ```bash + infisical login + ``` + + + Run the `infisical ssh connect` command to connect to a remote host. + + ```bash + infisical ssh connect + ``` + + You'll be prompted to select an SSH Host from a list of accessible hosts; this is based on project membership and login mappings configured on hosts by + the administrator. + + ```bash + Use the arrow keys to navigate: ↓ ↑ → ← + ? Select an SSH Host: + ▸ ec2-12-345-678-910.ap-northeast-1.compute.amazonaws.com + ``` + + After selecting a host, you'll be prompted to select a login user from a list of allowed login users: + + ```bash + ? Select Login User: + ▸ ec2-user + ``` + + If successful, you should be able to SSH to the remote host. + + ```bash + ✔ ec2-54-199-104-116.ap-northeast-1.compute.amazonaws.com + ✔ ec2-user + ✔ SSH credentials successfully added to agent + Connecting to ec2-user@ec2-12-345-678-910.ap-northeast-1.compute.amazonaws.com... + ``` + + + diff --git a/docs/documentation/platform/sso/auth0-oidc.mdx b/docs/documentation/platform/sso/auth0-oidc.mdx index 9419d0976..bde87f42f 100644 --- a/docs/documentation/platform/sso/auth0-oidc.mdx +++ b/docs/documentation/platform/sso/auth0-oidc.mdx @@ -42,7 +42,7 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO." 3.1. Back in Infisical, in the Organization settings > Security > OIDC, click **Connect**. ![OIDC auth0 manage org Infisical](../../../images/sso/auth0-oidc/org-oidc-overview.png) - 3.2. For configuration type, select **Discovery URL**. Then, set **Discovery Document URL**, **Client ID**, and **Client Secret** from step 2.1 and 2.2. + 3.2. For configuration type, select **Discovery URL**. Then, set **Discovery Document URL**, **JWT Signature Algorithm**, **Client ID**, and **Client Secret** from step 2.1 and 2.2. ![OIDC auth0 paste values into Infisical](../../../images/sso/auth0-oidc/org-update-oidc.png) Once you've done that, press **Update** to complete the required configuration. @@ -65,7 +65,9 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO." We recommend ensuring that your account is provisioned using the application in Auth0 prior to enforcing OIDC SSO to prevent any unintended issues. - + + In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. +
diff --git a/docs/documentation/platform/sso/auth0-saml.mdx b/docs/documentation/platform/sso/auth0-saml.mdx new file mode 100644 index 000000000..b426d1aae --- /dev/null +++ b/docs/documentation/platform/sso/auth0-saml.mdx @@ -0,0 +1,97 @@ +--- +title: "Auth0 SAML" +description: "Learn how to configure Auth0 SAML for Infisical SSO." +--- + + + Auth0 SAML SSO feature is a paid feature. If you're using Infisical Cloud, + then it is available under the **Pro Tier**. If you're self-hosting Infisical, + then you should contact sales@infisical.com to purchase an enterprise license + to use it. + + + + + In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select Auth0, then click **Connect** again. + + Next, note the **Application Callback URL** and **Audience** to use when configuring the Auth0 SAML application. + + ![Auth0 SAML initial configuration](../../../images/sso/auth0-saml/init-config.png) + + + + 2.1. In your Auth0 account, head to Applications and create an application. + + ![Auth0 SAML app creation](../../../images/sso/auth0-saml/create-application.png) + + Select **Regular Web Application** and press **Create**. + + ![Auth0 SAML app creation](../../../images/sso/auth0-saml/create-application-2.png) + + 2.2. In the Application head to Settings > Application URIs and add the **Application Callback URL** from step 1 into the **Allowed Callback URLs** field. + + ![Auth0 SAML allowed callback URLs](../../../images/sso/auth0-saml/auth0-config.png) + + 2.3. In the Application head to Addons > SAML2 Web App and copy the **Issuer**, **Identity Provider Login URL**, and **Identity Provider Certificate** from the **Usage** tab. + + ![Auth0 SAML config](../../../images/sso/auth0-saml/auth0-config-2.png) + + 2.4. Back in Infisical, set **Issuer**, **Identity Provider Login URL**, and **Certificate** to the corresponding items from step 2.3. + + ![Auth0 SAML Infisical config](../../../images/sso/auth0-saml/infisical-config.png) + + 2.5. Back in Auth0, in the **Settings** tab, set the **Application Callback URL** to the **Application Callback URL** from step 1 + and update the **Settings** field with the JSON under the picture below (replacing `` with the **Audience** from step 1). + + ![Auth0 SAML config](../../../images/sso/auth0-saml/auth0-config-3.png) + + ```json + { + "audience": "", + "mappings": { + "email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email", + "given_name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/firstName", + "family_name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/lastName" + }, + "signatureAlgorithm": "rsa-sha256", + "digestAlgorithm": "sha256", + "signResponse": true + } + ``` + + Click **Save**. + + + Enabling SAML SSO allows members in your organization to log into Infisical via Auth0. + + ![Auth0 SAML enable](../../../images/sso/auth0-saml/enable-saml.png) + + + Enforcing SAML SSO ensures that members in your organization can only access Infisical + by logging into the organization via Auth0. + + To enforce SAML SSO, you're required to test out the SAML connection by successfully authenticating at least one Auth0 user with Infisical; + Once you've completed this requirement, you can toggle the **Enforce SAML SSO** button to enforce SAML SSO. + + + In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + + + + + + + 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. + + + + If you're configuring SAML SSO on a self-hosted instance of Infisical, make + sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to + work: +
+ - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This + can be a random 32-byte base64 string generated with `openssl rand -base64 + 32`. +
+ - `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com) + \ No newline at end of file diff --git a/docs/documentation/platform/sso/azure.mdx b/docs/documentation/platform/sso/azure.mdx index 185d5fcfb..282cddae5 100644 --- a/docs/documentation/platform/sso/azure.mdx +++ b/docs/documentation/platform/sso/azure.mdx @@ -12,7 +12,7 @@ description: "Learn how to configure Microsoft Entra ID for Infisical SSO." - In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**. + In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select Azure / Entra, then click **Connect** again. Next, copy the **Reply URL (Assertion Consumer Service URL)** and **Identifier (Entity ID)** to use when configuring the Azure SAML application. @@ -48,7 +48,7 @@ description: "Learn how to configure Microsoft Entra ID for Infisical SSO." Back in the **Set up Single Sign-On with SAML** screen, select **Edit** in the **Attributes & Claims** section and configure the following map: - - `email -> user.userprinciplename` + - `email -> user.userprincipalname` - `firstName -> user.givenname` - `lastName -> user.surname` @@ -106,6 +106,9 @@ description: "Learn how to configure Microsoft Entra ID for Infisical SSO." We recommend ensuring that your account is provisioned the application in Azure prior to enforcing SAML SSO to prevent any unintended issues. + + In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + diff --git a/docs/documentation/platform/sso/general-oidc.mdx b/docs/documentation/platform/sso/general-oidc.mdx index 7e3a76ff0..11216b893 100644 --- a/docs/documentation/platform/sso/general-oidc.mdx +++ b/docs/documentation/platform/sso/general-oidc.mdx @@ -66,6 +66,9 @@ Prerequisites: We recommend ensuring that your account is provisioned using the identity provider prior to enforcing OIDC SSO to prevent any unintended issues. + + In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + diff --git a/docs/documentation/platform/sso/google-saml.mdx b/docs/documentation/platform/sso/google-saml.mdx index 4f31bffb1..87ffa8412 100644 --- a/docs/documentation/platform/sso/google-saml.mdx +++ b/docs/documentation/platform/sso/google-saml.mdx @@ -12,7 +12,7 @@ description: "Learn how to configure Google SAML for Infisical SSO." - In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**. + In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select Google, then click **Connect** again. Next, note the **ACS URL** and **SP Entity ID** to use when configuring the Google SAML application. @@ -81,6 +81,9 @@ description: "Learn how to configure Google SAML for Infisical SSO." We recommend ensuring that your account is provisioned the application in Google prior to enforcing SAML SSO to prevent any unintended issues. + + In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + diff --git a/docs/documentation/platform/sso/jumpcloud.mdx b/docs/documentation/platform/sso/jumpcloud.mdx index ce89b8e0d..6ca20c752 100644 --- a/docs/documentation/platform/sso/jumpcloud.mdx +++ b/docs/documentation/platform/sso/jumpcloud.mdx @@ -12,7 +12,7 @@ description: "Learn how to configure JumpCloud SAML for Infisical SSO." - In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**. + In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select JumpCloud, then click **Connect** again. Next, copy the **ACS URL** and **SP Entity ID** to use when configuring the JumpCloud SAML application. @@ -86,6 +86,9 @@ description: "Learn how to configure JumpCloud SAML for Infisical SSO." We recommend ensuring that your account is provisioned the application in JumpCloud prior to enforcing SAML SSO to prevent any unintended issues. + + In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + diff --git a/docs/documentation/platform/sso/keycloak-oidc.mdx b/docs/documentation/platform/sso/keycloak-oidc.mdx deleted file mode 100644 index cb774a014..000000000 --- a/docs/documentation/platform/sso/keycloak-oidc.mdx +++ /dev/null @@ -1,112 +0,0 @@ ---- -title: "Keycloak OIDC" -description: "Learn how to configure Keycloak OIDC for Infisical SSO." ---- - - - Keycloak OIDC SSO is a paid feature. If you're using Infisical Cloud, then it - is available under the **Pro Tier**. If you're self-hosting Infisical, then - you should contact sales@infisical.com to purchase an enterprise license to - use it. - - - - - 1.1. In your realm, navigate to the **Clients** tab and click **Create client** to create a new client application. - - ![OIDC keycloak list of clients](../../../images/sso/keycloak-oidc/clients-list.png) - - - You don’t typically need to make a realm dedicated to Infisical. We recommend adding Infisical as a client to your primary realm. - - - 1.2. In the General Settings step, set **Client type** to **OpenID Connect**, the **Client ID** field to an appropriate identifier, and the **Name** field to a friendly name like **Infisical**. - - ![OIDC keycloak create client general settings](../../../images/sso/keycloak-oidc/create-client-general-settings.png) - - 1.3. Next, in the Capability Config step, ensure that **Client Authentication** is set to On and that **Standard flow** is enabled in the Authentication flow section. - - ![OIDC keycloak create client capability config settings](../../../images/sso/keycloak-oidc/create-client-capability.png) - - 1.4. In the Login Settings step, set the following values: - - Root URL: `https://app.infisical.com`. - - Home URL: `https://app.infisical.com`. - - Valid Redirect URIs: `https://app.infisical.com/api/v1/sso/oidc/callback`. - - Web origins: `https://app.infisical.com`. - - ![OIDC keycloak create client login settings](../../../images/sso/keycloak-oidc/create-client-login-settings.png) - - If you’re self-hosting Infisical, then you will want to replace https://app.infisical.com (base URL) with your own domain. - - - 1.5. Next, navigate to the **Client scopes** tab and select the client's dedicated scope. - - ![OIDC keycloak client scopes list](../../../images/sso/keycloak-oidc/client-scope-list.png) - - 1.6. Next, click **Add predefined mapper**. - - ![OIDC keycloak client mappers empty](../../../images/sso/keycloak-oidc/client-scope-mapper-menu.png) - - 1.7. Select the **email**, **given name**, **family name** attributes and click **Add**. - - ![OIDC keycloak client mappers predefined 1](../../../images/sso/keycloak-oidc/scope-predefined-mapper-1.png) - ![OIDC keycloak client mappers predefined 2](../../../images/sso/keycloak-oidc/scope-predefined-mapper-2.png) - - Once you've completed the above steps, the list of mappers should look like the following: - ![OIDC keycloak client mappers completed](../../../images/sso/keycloak-oidc/client-scope-complete-overview.png) - - - - 2.1. Back in Keycloak, navigate to Configure > Realm settings > General tab > Endpoints > OpenID Endpoint Configuration and copy the opened URL. This is what is to referred to as the Discovery Document URL and it takes the form: `https://keycloak-mysite.com/realms/myrealm/.well-known/openid-configuration`. - ![OIDC keycloak realm OIDC metadata](../../../images/sso/keycloak-oidc/realm-setting-oidc-config.png) - - 2.2. From the Clients page, navigate to the Credential tab and copy the **Client Secret** to be used in the next steps. - ![OIDC keycloak realm OIDC secret](../../../images/sso/keycloak-oidc/client-secret.png) - - - - 3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect. - ![OIDC keycloak manage org Infisical](../../../images/sso/keycloak-oidc/manage-org-oidc.png) - - 3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **Client ID**, and **Client Secret**. - ![OIDC keycloak paste values into Infisical](../../../images/sso/keycloak-oidc/create-oidc.png) - - Once you've done that, press **Update** to complete the required configuration. - - - - Enabling OIDC SSO allows members in your organization to log into Infisical via Keycloak. - - ![OIDC keycloak enable OIDC](../../../images/sso/keycloak-oidc/enable-oidc.png) - - - - Enforcing OIDC SSO ensures that members in your organization can only access Infisical - by logging into the organization via Keycloak. - - To enforce OIDC SSO, you're required to test out the OpenID connection by successfully authenticating at least one Keycloak user with Infisical. - Once you've completed this requirement, you can toggle the **Enforce OIDC SSO** button to enforce OIDC SSO. - - - We recommend ensuring that your account is provisioned using the application in Keycloak - prior to enforcing OIDC SSO to prevent any unintended issues. - - - - - - - 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 OIDC login. - - - - If you're configuring OIDC SSO on a self-hosted instance of Infisical, make - sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to - work: -
- - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This - can be a random 32-byte base64 string generated with `openssl rand -base64 - 32`. -
- - `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com) - diff --git a/docs/documentation/platform/sso/keycloak-oidc/group-membership-mapping.mdx b/docs/documentation/platform/sso/keycloak-oidc/group-membership-mapping.mdx new file mode 100644 index 000000000..c423bac5a --- /dev/null +++ b/docs/documentation/platform/sso/keycloak-oidc/group-membership-mapping.mdx @@ -0,0 +1,62 @@ +--- +title: "Keycloak OIDC Group Membership Mapping" +sidebarTitle: "Group Membership Mapping" +description: "Learn how to sync Keycloak group members to matching groups in Infisical." +--- + +You can have Infisical automatically sync group +memberships between Keycloak and Infisical by configuring a group membership mapper in Keycloak. +When a user logs in via OIDC, they will be added to Infisical groups that match their Keycloak groups names, and removed from any +Infisical groups not present in their groups claim. + + + When enabled, manual + management of Infisical group memberships will be disabled. + + + + Group membership changes in the Keycloak only sync with Infisical when a + user logs in via OIDC. For example, if you remove a user from a group in Keycloak, this change will not be reflected in Infisical until their next OIDC login. To ensure this behavior, Infisical recommends enabling Enforce OIDC + SSO in the OIDC settings. + + + + + + 1.1. In your realm, navigate to the **Clients** tab and select your Infisical client. + + ![OIDC keycloak client](/images/sso/keycloak-oidc/group-membership-mapping/select-client.png) + + 1.2. Select the **Client Scopes** tab. + + ![OIDC keycloak client scopes](/images/sso/keycloak-oidc/group-membership-mapping/select-client-scopes.png) + + 1.3. Next, select the dedicated scope for your Infisical client. + + ![OIDC keycloak dedicated scope](/images/sso/keycloak-oidc/group-membership-mapping/select-dedicated-scope.png) + + 1.4. Click on the **Add mapper** button, and select the **By configuration** option. + + ![OIDC keycloak add mapper by configuration](/images/sso/keycloak-oidc/group-membership-mapping/create-mapper-by-configuration.png) + + 1.5. Select the **Group Membership** option. + + ![OIDC keycloak group membership option](/images/sso/keycloak-oidc/group-membership-mapping/select-group-membership-mapper.png) + + 1.6. Give your mapper a name and ensure the following properties are set to the following before saving: + - **Token Claim Name** is set to `groups` + - **Full group path** is disabled + + ![OIDC keycloak group membership mapper](/images/sso/keycloak-oidc/group-membership-mapping/create-group-membership-mapper.png) + + + 2.1. In Infisical, create any groups you would like to sync users to. Make sure the name of the Infisical group is an exact match of the Keycloak group name. + ![OIDC keycloak infisical group](/images/sso/keycloak-oidc/group-membership-mapping/create-infisical-group.png) + + 2.2. Next, enable **OIDC Group Membership Mapping** in Organization Settings > Security. + ![OIDC keycloak enable group membership mapping](/images/sso/keycloak-oidc/group-membership-mapping/enable-group-membership-mapping.png) + + 2.3. The next time a user logs in they will be synced to their matching Keycloak groups. + ![OIDC keycloak synced users](/images/sso/keycloak-oidc/group-membership-mapping/synced-users.png) + + \ No newline at end of file diff --git a/docs/documentation/platform/sso/keycloak-oidc/overview.mdx b/docs/documentation/platform/sso/keycloak-oidc/overview.mdx new file mode 100644 index 000000000..6d8f4e4c8 --- /dev/null +++ b/docs/documentation/platform/sso/keycloak-oidc/overview.mdx @@ -0,0 +1,115 @@ +--- +title: "Keycloak OIDC Overview" +sidebarTitle: "Overview" +description: "Learn how to configure Keycloak OIDC for Infisical SSO." +--- + + + Keycloak OIDC SSO is a paid feature. If you're using Infisical Cloud, then it + is available under the **Pro Tier**. If you're self-hosting Infisical, then + you should contact sales@infisical.com to purchase an enterprise license to + use it. + + + + + 1.1. In your realm, navigate to the **Clients** tab and click **Create client** to create a new client application. + + ![OIDC keycloak list of clients](/images/sso/keycloak-oidc/clients-list.png) + + + You don’t typically need to make a realm dedicated to Infisical. We recommend adding Infisical as a client to your primary realm. + + + 1.2. In the General Settings step, set **Client type** to **OpenID Connect**, the **Client ID** field to an appropriate identifier, and the **Name** field to a friendly name like **Infisical**. + + ![OIDC keycloak create client general settings](/images/sso/keycloak-oidc/create-client-general-settings.png) + + 1.3. Next, in the Capability Config step, ensure that **Client Authentication** is set to On and that **Standard flow** is enabled in the Authentication flow section. + + ![OIDC keycloak create client capability config settings](/images/sso/keycloak-oidc/create-client-capability.png) + + 1.4. In the Login Settings step, set the following values: + - Root URL: `https://app.infisical.com`. + - Home URL: `https://app.infisical.com`. + - Valid Redirect URIs: `https://app.infisical.com/api/v1/sso/oidc/callback`. + - Web origins: `https://app.infisical.com`. + + ![OIDC keycloak create client login settings](/images/sso/keycloak-oidc/create-client-login-settings.png) + + If you’re self-hosting Infisical, then you will want to replace https://app.infisical.com (base URL) with your own domain. + + + 1.5. Next, navigate to the **Client scopes** tab and select the client's dedicated scope. + + ![OIDC keycloak client scopes list](/images/sso/keycloak-oidc/client-scope-list.png) + + 1.6. Next, click **Add predefined mapper**. + + ![OIDC keycloak client mappers empty](/images/sso/keycloak-oidc/client-scope-mapper-menu.png) + + 1.7. Select the **email**, **given name**, **family name** attributes and click **Add**. + + ![OIDC keycloak client mappers predefined 1](/images/sso/keycloak-oidc/scope-predefined-mapper-1.png) + ![OIDC keycloak client mappers predefined 2](/images/sso/keycloak-oidc/scope-predefined-mapper-2.png) + + Once you've completed the above steps, the list of mappers should look like the following: + ![OIDC keycloak client mappers completed](/images/sso/keycloak-oidc/client-scope-complete-overview.png) + + + + 2.1. Back in Keycloak, navigate to Configure > Realm settings > General tab > Endpoints > OpenID Endpoint Configuration and copy the opened URL. This is what is to referred to as the Discovery Document URL and it takes the form: `https://keycloak-mysite.com/realms/myrealm/.well-known/openid-configuration`. + ![OIDC keycloak realm OIDC metadata](/images/sso/keycloak-oidc/realm-setting-oidc-config.png) + + 2.2. From the Clients page, navigate to the Credential tab and copy the **Client Secret** to be used in the next steps. + ![OIDC keycloak realm OIDC secret](/images/sso/keycloak-oidc/client-secret.png) + + + + 3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect. + ![OIDC keycloak manage org Infisical](/images/sso/keycloak-oidc/manage-org-oidc.png) + + 3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **JWT Signature Algorithm**, **Client ID**, and **Client Secret**. + ![OIDC keycloak paste values into Infisical](/images/sso/keycloak-oidc/create-oidc.png) + + Once you've done that, press **Update** to complete the required configuration. + + + + Enabling OIDC SSO allows members in your organization to log into Infisical via Keycloak. + + ![OIDC keycloak enable OIDC](/images/sso/keycloak-oidc/enable-oidc.png) + + + + Enforcing OIDC SSO ensures that members in your organization can only access Infisical + by logging into the organization via Keycloak. + + To enforce OIDC SSO, you're required to test out the OpenID connection by successfully authenticating at least one Keycloak user with Infisical. + Once you've completed this requirement, you can toggle the **Enforce OIDC SSO** button to enforce OIDC SSO. + + + We recommend ensuring that your account is provisioned using the application in Keycloak + prior to enforcing OIDC SSO to prevent any unintended issues. + + + In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + + + + + + 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 OIDC login. + + + + If you're configuring OIDC SSO on a self-hosted instance of Infisical, make + sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to + work: +
+ - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This + can be a random 32-byte base64 string generated with `openssl rand -base64 + 32`. +
+ - `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com) + diff --git a/docs/documentation/platform/sso/keycloak-saml.mdx b/docs/documentation/platform/sso/keycloak-saml.mdx index 53f47f1ae..7e4004122 100644 --- a/docs/documentation/platform/sso/keycloak-saml.mdx +++ b/docs/documentation/platform/sso/keycloak-saml.mdx @@ -12,7 +12,7 @@ description: "Learn how to configure Keycloak SAML for Infisical SSO." - In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Manage**. + In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select Keycloak, then click **Connect** again. ![Keycloak SAML organization security section](../../../images/sso/keycloak/org-security-section.png) @@ -127,6 +127,9 @@ description: "Learn how to configure Keycloak SAML for Infisical SSO." We recommend ensuring that your account is provisioned the application in Keycloak prior to enforcing SAML SSO to prevent any unintended issues. + + In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + diff --git a/docs/documentation/platform/sso/okta.mdx b/docs/documentation/platform/sso/okta.mdx index 9a1d4aa2f..1abd03d6f 100644 --- a/docs/documentation/platform/sso/okta.mdx +++ b/docs/documentation/platform/sso/okta.mdx @@ -12,7 +12,7 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO." - In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**. + In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select Okta, then click **Connect** again. Next, copy the **Single sign-on URL** and **Audience URI (SP Entity ID)** to use when configuring the Okta SAML 2.0 application. ![Okta SAML initial configuration](../../../images/sso/okta/init-config.png) @@ -94,6 +94,9 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO." We recommend ensuring that your account is provisioned the application in Okta prior to enforcing SAML SSO to prevent any unintended issues. + + In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + diff --git a/docs/documentation/platform/sso/overview.mdx b/docs/documentation/platform/sso/overview.mdx index 227a7502f..0d4b8da89 100644 --- a/docs/documentation/platform/sso/overview.mdx +++ b/docs/documentation/platform/sso/overview.mdx @@ -28,6 +28,7 @@ Infisical supports these and many other identity providers: - [JumpCloud SAML](/documentation/platform/sso/jumpcloud) - [Keycloak SAML](/documentation/platform/sso/keycloak-saml) - [Google SAML](/documentation/platform/sso/google-saml) +- [Auth0 SAML](/documentation/platform/sso/auth0-saml) - [Keycloak OIDC](/documentation/platform/sso/keycloak-oidc) - [Auth0 OIDC](/documentation/platform/sso/auth0-oidc) - [General OIDC](/documentation/platform/sso/general-oidc) diff --git a/docs/documentation/platform/webhooks.mdx b/docs/documentation/platform/webhooks.mdx index dc3a71b27..92d3ff8b8 100644 --- a/docs/documentation/platform/webhooks.mdx +++ b/docs/documentation/platform/webhooks.mdx @@ -36,3 +36,18 @@ If the signature in the header matches the signature that you generated, then yo "timestamp": "" } ``` + +```json +{ + "event": "secrets.reminder-expired", + "project": { + "workspaceId": "the workspace id", + "environment": "project environment", + "secretPath": "project folder path", + "secretName": "name of the secret", + "secretId": "id of the secret", + "reminderNote": "reminder note of the secret" + }, + "timestamp": "" +} +``` diff --git a/docs/documentation/setup/networking.mdx b/docs/documentation/setup/networking.mdx new file mode 100644 index 000000000..4a666b73c --- /dev/null +++ b/docs/documentation/setup/networking.mdx @@ -0,0 +1,36 @@ +--- +title: "Networking" +sidebarTitle: "Networking" +description: "Network configuration details for Infisical Cloud" +--- + +## Overview + +When integrating your infrastructure with Infisical Cloud, you may need to configure network access controls. This page provides the IP addresses that Infisical uses to communicate with your services. + +## Egress IP Addresses + +Infisical Cloud operates from two regions: US and EU. If your infrastructure has strict network policies, you may need to allow traffic from Infisical by adding the following IP addresses to your ingress rules. These are the egress IPs Infisical uses when making outbound requests to your services. + +### US Region + +To allow connections from Infisical US, add these IP addresses to your ingress rules: + +- `3.213.63.16` +- `54.164.68.7` + +### EU Region + +To allow connections from Infisical EU, add these IP addresses to your ingress rules: + +- `3.77.89.19` +- `3.125.209.189` + +## Common Use Cases + +You may need to allow Infisical’s egress IPs if your services require inbound connections for: + +- Secret rotation - When Infisical needs to send requests to your systems to automatically rotate credentials +- Dynamic secrets - When Infisical generates and manages temporary credentials for your cloud services +- Secret integrations - When syncing secrets with third-party services like Azure Key Vault +- Native authentication with machine identities - When using methods like Kubernetes authentication diff --git a/docs/images/app-connections/auth0/auth0-audience.png b/docs/images/app-connections/auth0/auth0-audience.png new file mode 100644 index 000000000..1c5226aac Binary files /dev/null and b/docs/images/app-connections/auth0/auth0-audience.png differ diff --git a/docs/images/app-connections/auth0/auth0-client-credentials.png b/docs/images/app-connections/auth0/auth0-client-credentials.png new file mode 100644 index 000000000..3fea1096f Binary files /dev/null and b/docs/images/app-connections/auth0/auth0-client-credentials.png differ diff --git a/docs/images/app-connections/auth0/auth0-dashboard-applications.png b/docs/images/app-connections/auth0/auth0-dashboard-applications.png new file mode 100644 index 000000000..225113a04 Binary files /dev/null and b/docs/images/app-connections/auth0/auth0-dashboard-applications.png differ diff --git a/docs/images/app-connections/auth0/auth0-secret-rotation-api-selection.png b/docs/images/app-connections/auth0/auth0-secret-rotation-api-selection.png new file mode 100644 index 000000000..42f3ee1e4 Binary files /dev/null and b/docs/images/app-connections/auth0/auth0-secret-rotation-api-selection.png differ diff --git a/docs/images/app-connections/auth0/auth0-select-m2m.png b/docs/images/app-connections/auth0/auth0-select-m2m.png new file mode 100644 index 000000000..2d83c6de9 Binary files /dev/null and b/docs/images/app-connections/auth0/auth0-select-m2m.png differ diff --git a/docs/images/app-connections/auth0/client-credentials-create.png b/docs/images/app-connections/auth0/client-credentials-create.png new file mode 100644 index 000000000..2c4466cd2 Binary files /dev/null and b/docs/images/app-connections/auth0/client-credentials-create.png differ diff --git a/docs/images/app-connections/auth0/client_credentials_connection.png b/docs/images/app-connections/auth0/client_credentials_connection.png new file mode 100644 index 000000000..a4b115523 Binary files /dev/null and b/docs/images/app-connections/auth0/client_credentials_connection.png differ diff --git a/docs/images/app-connections/auth0/select-auth0-connection.png b/docs/images/app-connections/auth0/select-auth0-connection.png new file mode 100644 index 000000000..47d72d2a5 Binary files /dev/null and b/docs/images/app-connections/auth0/select-auth0-connection.png differ diff --git a/docs/images/app-connections/aws/access-key-connection.png b/docs/images/app-connections/aws/access-key-connection.png new file mode 100644 index 000000000..9c70da623 Binary files /dev/null and b/docs/images/app-connections/aws/access-key-connection.png differ diff --git a/docs/images/app-connections/aws/access-key-create-policy.png b/docs/images/app-connections/aws/access-key-create-policy.png new file mode 100644 index 000000000..adfc2eaad Binary files /dev/null and b/docs/images/app-connections/aws/access-key-create-policy.png differ diff --git a/docs/images/app-connections/aws/assume-role-connection.png b/docs/images/app-connections/aws/assume-role-connection.png new file mode 100644 index 000000000..c01f2e016 Binary files /dev/null and b/docs/images/app-connections/aws/assume-role-connection.png differ diff --git a/docs/images/app-connections/aws/assume-role-create-policy.png b/docs/images/app-connections/aws/assume-role-create-policy.png new file mode 100644 index 000000000..fdcb15bb4 Binary files /dev/null and b/docs/images/app-connections/aws/assume-role-create-policy.png differ diff --git a/docs/images/app-connections/aws/create-access-key-method.png b/docs/images/app-connections/aws/create-access-key-method.png new file mode 100644 index 000000000..a82cca038 Binary files /dev/null and b/docs/images/app-connections/aws/create-access-key-method.png differ diff --git a/docs/images/app-connections/aws/create-assume-role-method.png b/docs/images/app-connections/aws/create-assume-role-method.png new file mode 100644 index 000000000..4b422222d Binary files /dev/null and b/docs/images/app-connections/aws/create-assume-role-method.png differ diff --git a/docs/images/app-connections/aws/kms-key-user.png b/docs/images/app-connections/aws/kms-key-user.png new file mode 100644 index 000000000..c94edea67 Binary files /dev/null and b/docs/images/app-connections/aws/kms-key-user.png differ diff --git a/docs/images/app-connections/aws/parameter-store-permissions.png b/docs/images/app-connections/aws/parameter-store-permissions.png new file mode 100644 index 000000000..0c5191e37 Binary files /dev/null and b/docs/images/app-connections/aws/parameter-store-permissions.png differ diff --git a/docs/images/app-connections/aws/secrets-manager-permissions.png b/docs/images/app-connections/aws/secrets-manager-permissions.png new file mode 100644 index 000000000..6c60d9b83 Binary files /dev/null and b/docs/images/app-connections/aws/secrets-manager-permissions.png differ diff --git a/docs/images/app-connections/aws/select-aws-connection.png b/docs/images/app-connections/aws/select-aws-connection.png new file mode 100644 index 000000000..0cd51bb7f Binary files /dev/null and b/docs/images/app-connections/aws/select-aws-connection.png differ diff --git a/docs/images/app-connections/azure/app-configuration/create-oauth-method.png b/docs/images/app-connections/azure/app-configuration/create-oauth-method.png new file mode 100644 index 000000000..1c8ed186b Binary files /dev/null and b/docs/images/app-connections/azure/app-configuration/create-oauth-method.png differ diff --git a/docs/images/app-connections/azure/app-configuration/oauth-connection.png b/docs/images/app-connections/azure/app-configuration/oauth-connection.png new file mode 100644 index 000000000..906b30899 Binary files /dev/null and b/docs/images/app-connections/azure/app-configuration/oauth-connection.png differ diff --git a/docs/images/app-connections/azure/app-configuration/select-connection.png b/docs/images/app-connections/azure/app-configuration/select-connection.png new file mode 100644 index 000000000..ff452fb8b Binary files /dev/null and b/docs/images/app-connections/azure/app-configuration/select-connection.png differ diff --git a/docs/images/app-connections/azure/grant-access.png b/docs/images/app-connections/azure/grant-access.png new file mode 100644 index 000000000..c0545a343 Binary files /dev/null and b/docs/images/app-connections/azure/grant-access.png differ diff --git a/docs/images/app-connections/azure/key-vault/create-oauth-method.png b/docs/images/app-connections/azure/key-vault/create-oauth-method.png new file mode 100644 index 000000000..6d7de342a Binary files /dev/null and b/docs/images/app-connections/azure/key-vault/create-oauth-method.png differ diff --git a/docs/images/app-connections/azure/key-vault/oauth-connection.png b/docs/images/app-connections/azure/key-vault/oauth-connection.png new file mode 100644 index 000000000..5c0239cdc Binary files /dev/null and b/docs/images/app-connections/azure/key-vault/oauth-connection.png differ diff --git a/docs/images/app-connections/azure/key-vault/select-connection.png b/docs/images/app-connections/azure/key-vault/select-connection.png new file mode 100644 index 000000000..e55028588 Binary files /dev/null and b/docs/images/app-connections/azure/key-vault/select-connection.png differ diff --git a/docs/images/app-connections/azure/keyvault-azure-permissions.png b/docs/images/app-connections/azure/keyvault-azure-permissions.png new file mode 100644 index 000000000..0f009cdc1 Binary files /dev/null and b/docs/images/app-connections/azure/keyvault-azure-permissions.png differ diff --git a/docs/images/app-connections/azure/register-callback.png b/docs/images/app-connections/azure/register-callback.png new file mode 100644 index 000000000..b06b5ce6f Binary files /dev/null and b/docs/images/app-connections/azure/register-callback.png differ diff --git a/docs/images/app-connections/camunda/camunda-app-connection-created.png b/docs/images/app-connections/camunda/camunda-app-connection-created.png new file mode 100644 index 000000000..7af738e89 Binary files /dev/null and b/docs/images/app-connections/camunda/camunda-app-connection-created.png differ diff --git a/docs/images/app-connections/camunda/camunda-app-connection-form.png b/docs/images/app-connections/camunda/camunda-app-connection-form.png new file mode 100644 index 000000000..d8b26f0b5 Binary files /dev/null and b/docs/images/app-connections/camunda/camunda-app-connection-form.png differ diff --git a/docs/images/app-connections/camunda/camunda-app-connection-select.png b/docs/images/app-connections/camunda/camunda-app-connection-select.png new file mode 100644 index 000000000..33c45bd84 Binary files /dev/null and b/docs/images/app-connections/camunda/camunda-app-connection-select.png differ diff --git a/docs/images/app-connections/camunda/camunda-client-credentials.png b/docs/images/app-connections/camunda/camunda-client-credentials.png new file mode 100644 index 000000000..522f7f74c Binary files /dev/null and b/docs/images/app-connections/camunda/camunda-client-credentials.png differ diff --git a/docs/images/app-connections/camunda/camunda-console.png b/docs/images/app-connections/camunda/camunda-console.png new file mode 100644 index 000000000..ca96adbd2 Binary files /dev/null and b/docs/images/app-connections/camunda/camunda-console.png differ diff --git a/docs/images/app-connections/camunda/camunda-create-client-1.png b/docs/images/app-connections/camunda/camunda-create-client-1.png new file mode 100644 index 000000000..5ef589806 Binary files /dev/null and b/docs/images/app-connections/camunda/camunda-create-client-1.png differ diff --git a/docs/images/app-connections/camunda/camunda-create-client-2.png b/docs/images/app-connections/camunda/camunda-create-client-2.png new file mode 100644 index 000000000..9b8575c76 Binary files /dev/null and b/docs/images/app-connections/camunda/camunda-create-client-2.png differ diff --git a/docs/images/app-connections/camunda/camunda-organization-page.png b/docs/images/app-connections/camunda/camunda-organization-page.png new file mode 100644 index 000000000..fdc71a378 Binary files /dev/null and b/docs/images/app-connections/camunda/camunda-organization-page.png differ diff --git a/docs/images/app-connections/databricks/add-service-principal.png b/docs/images/app-connections/databricks/add-service-principal.png new file mode 100644 index 000000000..0d695582d Binary files /dev/null and b/docs/images/app-connections/databricks/add-service-principal.png differ diff --git a/docs/images/app-connections/databricks/create-databricks-service-principal-method.png b/docs/images/app-connections/databricks/create-databricks-service-principal-method.png new file mode 100644 index 000000000..b8dd38bf1 Binary files /dev/null and b/docs/images/app-connections/databricks/create-databricks-service-principal-method.png differ diff --git a/docs/images/app-connections/databricks/create-service-principal.png b/docs/images/app-connections/databricks/create-service-principal.png new file mode 100644 index 000000000..145931442 Binary files /dev/null and b/docs/images/app-connections/databricks/create-service-principal.png differ diff --git a/docs/images/app-connections/databricks/databricks-service-principal-connection.png b/docs/images/app-connections/databricks/databricks-service-principal-connection.png new file mode 100644 index 000000000..6b632553c Binary files /dev/null and b/docs/images/app-connections/databricks/databricks-service-principal-connection.png differ diff --git a/docs/images/app-connections/databricks/manage-service-principals.png b/docs/images/app-connections/databricks/manage-service-principals.png new file mode 100644 index 000000000..f76d2400d Binary files /dev/null and b/docs/images/app-connections/databricks/manage-service-principals.png differ diff --git a/docs/images/app-connections/databricks/select-databricks-connection.png b/docs/images/app-connections/databricks/select-databricks-connection.png new file mode 100644 index 000000000..41a21e6f4 Binary files /dev/null and b/docs/images/app-connections/databricks/select-databricks-connection.png differ diff --git a/docs/images/app-connections/databricks/service-principal-ids.png b/docs/images/app-connections/databricks/service-principal-ids.png new file mode 100644 index 000000000..2748c13e8 Binary files /dev/null and b/docs/images/app-connections/databricks/service-principal-ids.png differ diff --git a/docs/images/app-connections/databricks/service-principal-secrets.png b/docs/images/app-connections/databricks/service-principal-secrets.png new file mode 100644 index 000000000..e1e0b053e Binary files /dev/null and b/docs/images/app-connections/databricks/service-principal-secrets.png differ diff --git a/docs/images/app-connections/databricks/workspace-settings.png b/docs/images/app-connections/databricks/workspace-settings.png new file mode 100644 index 000000000..5c3ec54a6 Binary files /dev/null and b/docs/images/app-connections/databricks/workspace-settings.png differ diff --git a/docs/images/app-connections/gcp/create-gcp-impersonation-method.png b/docs/images/app-connections/gcp/create-gcp-impersonation-method.png new file mode 100644 index 000000000..9d2467689 Binary files /dev/null and b/docs/images/app-connections/gcp/create-gcp-impersonation-method.png differ diff --git a/docs/images/app-connections/gcp/create-instance-service-account.png b/docs/images/app-connections/gcp/create-instance-service-account.png new file mode 100644 index 000000000..4a18e2ad2 Binary files /dev/null and b/docs/images/app-connections/gcp/create-instance-service-account.png differ diff --git a/docs/images/app-connections/gcp/create-service-account-credential.png b/docs/images/app-connections/gcp/create-service-account-credential.png new file mode 100644 index 000000000..acab54c31 Binary files /dev/null and b/docs/images/app-connections/gcp/create-service-account-credential.png differ diff --git a/docs/images/app-connections/gcp/create-service-account.png b/docs/images/app-connections/gcp/create-service-account.png new file mode 100644 index 000000000..c0d86b681 Binary files /dev/null and b/docs/images/app-connections/gcp/create-service-account.png differ diff --git a/docs/images/app-connections/gcp/gcp-app-impersonation-connection.png b/docs/images/app-connections/gcp/gcp-app-impersonation-connection.png new file mode 100644 index 000000000..8f67478b9 Binary files /dev/null and b/docs/images/app-connections/gcp/gcp-app-impersonation-connection.png differ diff --git a/docs/images/app-connections/gcp/select-gcp-connection.png b/docs/images/app-connections/gcp/select-gcp-connection.png new file mode 100644 index 000000000..3b28869f9 Binary files /dev/null and b/docs/images/app-connections/gcp/select-gcp-connection.png differ diff --git a/docs/images/app-connections/gcp/service-account-credentials-api.png b/docs/images/app-connections/gcp/service-account-credentials-api.png new file mode 100644 index 000000000..50a0be814 Binary files /dev/null and b/docs/images/app-connections/gcp/service-account-credentials-api.png differ diff --git a/docs/images/app-connections/gcp/service-account-grant-access.png b/docs/images/app-connections/gcp/service-account-grant-access.png new file mode 100644 index 000000000..d0e0df52b Binary files /dev/null and b/docs/images/app-connections/gcp/service-account-grant-access.png differ diff --git a/docs/images/app-connections/gcp/service-account-overview.png b/docs/images/app-connections/gcp/service-account-overview.png new file mode 100644 index 000000000..4e94d31f1 Binary files /dev/null and b/docs/images/app-connections/gcp/service-account-overview.png differ diff --git a/docs/images/app-connections/gcp/service-account-permission-overview.png b/docs/images/app-connections/gcp/service-account-permission-overview.png new file mode 100644 index 000000000..789085cfe Binary files /dev/null and b/docs/images/app-connections/gcp/service-account-permission-overview.png differ diff --git a/docs/images/app-connections/gcp/service-account-secret-sync-permission.png b/docs/images/app-connections/gcp/service-account-secret-sync-permission.png new file mode 100644 index 000000000..f3bf0d28c Binary files /dev/null and b/docs/images/app-connections/gcp/service-account-secret-sync-permission.png differ diff --git a/docs/images/app-connections/general/add-connection.png b/docs/images/app-connections/general/add-connection.png new file mode 100644 index 000000000..97718065a Binary files /dev/null and b/docs/images/app-connections/general/add-connection.png differ diff --git a/docs/images/app-connections/github/create-github-app-method.png b/docs/images/app-connections/github/create-github-app-method.png new file mode 100644 index 000000000..640fb0213 Binary files /dev/null and b/docs/images/app-connections/github/create-github-app-method.png differ diff --git a/docs/images/app-connections/github/create-oauth-method.png b/docs/images/app-connections/github/create-oauth-method.png new file mode 100644 index 000000000..4898a0de0 Binary files /dev/null and b/docs/images/app-connections/github/create-oauth-method.png differ diff --git a/docs/images/app-connections/github/github-app-connection.png b/docs/images/app-connections/github/github-app-connection.png new file mode 100644 index 000000000..3d81bc182 Binary files /dev/null and b/docs/images/app-connections/github/github-app-connection.png differ diff --git a/docs/images/app-connections/github/install-github-app.png b/docs/images/app-connections/github/install-github-app.png new file mode 100644 index 000000000..3b09ed485 Binary files /dev/null and b/docs/images/app-connections/github/install-github-app.png differ diff --git a/docs/images/app-connections/github/oauth-connection.png b/docs/images/app-connections/github/oauth-connection.png new file mode 100644 index 000000000..bf907256c Binary files /dev/null and b/docs/images/app-connections/github/oauth-connection.png differ diff --git a/docs/images/app-connections/github/select-github-connection.png b/docs/images/app-connections/github/select-github-connection.png new file mode 100644 index 000000000..2856d0a39 Binary files /dev/null and b/docs/images/app-connections/github/select-github-connection.png differ diff --git a/docs/images/app-connections/humanitec/add-humanitec-connection.png b/docs/images/app-connections/humanitec/add-humanitec-connection.png new file mode 100644 index 000000000..9ae9a7bf1 Binary files /dev/null and b/docs/images/app-connections/humanitec/add-humanitec-connection.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-add-api-token.png b/docs/images/app-connections/humanitec/humanitec-add-api-token.png new file mode 100644 index 000000000..6eaf796c4 Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-add-api-token.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-add-user-options.png b/docs/images/app-connections/humanitec/humanitec-add-user-options.png new file mode 100644 index 000000000..ad6d28343 Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-add-user-options.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-add-user-role.png b/docs/images/app-connections/humanitec/humanitec-add-user-role.png new file mode 100644 index 000000000..71f7647e6 Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-add-user-role.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-add-user.png b/docs/images/app-connections/humanitec/humanitec-add-user.png new file mode 100644 index 000000000..d675c8336 Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-add-user.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-app-connection-created.png b/docs/images/app-connections/humanitec/humanitec-app-connection-created.png new file mode 100644 index 000000000..0120adfb5 Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-app-connection-created.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-app-connection-modal.png b/docs/images/app-connections/humanitec/humanitec-app-connection-modal.png new file mode 100644 index 000000000..9a96d6cee Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-app-connection-modal.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-app-connection-option.png b/docs/images/app-connections/humanitec/humanitec-app-connection-option.png new file mode 100644 index 000000000..1f1f724b2 Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-app-connection-option.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-applications-tab.png b/docs/images/app-connections/humanitec/humanitec-applications-tab.png new file mode 100644 index 000000000..c97bb96d9 Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-applications-tab.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-connection.png b/docs/images/app-connections/humanitec/humanitec-connection.png new file mode 100644 index 000000000..b18ac8aac Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-connection.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-copy-api-token.png b/docs/images/app-connections/humanitec/humanitec-copy-api-token.png new file mode 100644 index 000000000..29f67954b Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-copy-api-token.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-create-api-token.png b/docs/images/app-connections/humanitec/humanitec-create-api-token.png new file mode 100644 index 000000000..1cb198129 Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-create-api-token.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-create-new-user.png b/docs/images/app-connections/humanitec/humanitec-create-new-user.png new file mode 100644 index 000000000..f1ac7d029 Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-create-new-user.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-service-account-filled.png b/docs/images/app-connections/humanitec/humanitec-service-account-filled.png new file mode 100644 index 000000000..023253b88 Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-service-account-filled.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-service-users.png b/docs/images/app-connections/humanitec/humanitec-service-users.png new file mode 100644 index 000000000..3b95403ff Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-service-users.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-user-added.png b/docs/images/app-connections/humanitec/humanitec-user-added.png new file mode 100644 index 000000000..fe918a7bd Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-user-added.png differ diff --git a/docs/images/app-connections/humanitec/select-humanitec-connection.png b/docs/images/app-connections/humanitec/select-humanitec-connection.png new file mode 100644 index 000000000..70d430cfc Binary files /dev/null and b/docs/images/app-connections/humanitec/select-humanitec-connection.png differ diff --git a/docs/images/app-connections/mssql/create-username-and-password-method.png b/docs/images/app-connections/mssql/create-username-and-password-method.png new file mode 100644 index 000000000..c30423857 Binary files /dev/null and b/docs/images/app-connections/mssql/create-username-and-password-method.png differ diff --git a/docs/images/app-connections/mssql/select-mssql-connection.png b/docs/images/app-connections/mssql/select-mssql-connection.png new file mode 100644 index 000000000..cc5f14ce4 Binary files /dev/null and b/docs/images/app-connections/mssql/select-mssql-connection.png differ diff --git a/docs/images/app-connections/mssql/username-and-password-connection.png b/docs/images/app-connections/mssql/username-and-password-connection.png new file mode 100644 index 000000000..b188f1f45 Binary files /dev/null and b/docs/images/app-connections/mssql/username-and-password-connection.png differ diff --git a/docs/images/app-connections/postgres/create-username-and-password-method.png b/docs/images/app-connections/postgres/create-username-and-password-method.png new file mode 100644 index 000000000..deecd87b6 Binary files /dev/null and b/docs/images/app-connections/postgres/create-username-and-password-method.png differ diff --git a/docs/images/app-connections/postgres/select-postgres-connection.png b/docs/images/app-connections/postgres/select-postgres-connection.png new file mode 100644 index 000000000..e6e7053b0 Binary files /dev/null and b/docs/images/app-connections/postgres/select-postgres-connection.png differ diff --git a/docs/images/app-connections/postgres/username-and-password-connection.png b/docs/images/app-connections/postgres/username-and-password-connection.png new file mode 100644 index 000000000..c31cd4758 Binary files /dev/null and b/docs/images/app-connections/postgres/username-and-password-connection.png differ diff --git a/docs/images/app-connections/terraform-cloud/terraform-cloud-account-settings.png b/docs/images/app-connections/terraform-cloud/terraform-cloud-account-settings.png new file mode 100644 index 000000000..f80df4229 Binary files /dev/null and b/docs/images/app-connections/terraform-cloud/terraform-cloud-account-settings.png differ diff --git a/docs/images/app-connections/terraform-cloud/terraform-cloud-app-connection-created.png b/docs/images/app-connections/terraform-cloud/terraform-cloud-app-connection-created.png new file mode 100644 index 000000000..f8904957a Binary files /dev/null and b/docs/images/app-connections/terraform-cloud/terraform-cloud-app-connection-created.png differ diff --git a/docs/images/app-connections/terraform-cloud/terraform-cloud-app-connection-modal.png b/docs/images/app-connections/terraform-cloud/terraform-cloud-app-connection-modal.png new file mode 100644 index 000000000..e8f0b9524 Binary files /dev/null and b/docs/images/app-connections/terraform-cloud/terraform-cloud-app-connection-modal.png differ diff --git a/docs/images/app-connections/terraform-cloud/terraform-cloud-app-connection-option.png b/docs/images/app-connections/terraform-cloud/terraform-cloud-app-connection-option.png new file mode 100644 index 000000000..369067a31 Binary files /dev/null and b/docs/images/app-connections/terraform-cloud/terraform-cloud-app-connection-option.png differ diff --git a/docs/images/app-connections/terraform-cloud/terraform-cloud-copy-api-token.png b/docs/images/app-connections/terraform-cloud/terraform-cloud-copy-api-token.png new file mode 100644 index 000000000..348a4fc53 Binary files /dev/null and b/docs/images/app-connections/terraform-cloud/terraform-cloud-copy-api-token.png differ diff --git a/docs/images/app-connections/terraform-cloud/terraform-cloud-create-api-token.png b/docs/images/app-connections/terraform-cloud/terraform-cloud-create-api-token.png new file mode 100644 index 000000000..3637145b6 Binary files /dev/null and b/docs/images/app-connections/terraform-cloud/terraform-cloud-create-api-token.png differ diff --git a/docs/images/app-connections/terraform-cloud/terraform-cloud-tokens-tab.png b/docs/images/app-connections/terraform-cloud/terraform-cloud-tokens-tab.png new file mode 100644 index 000000000..3293ccdac Binary files /dev/null and b/docs/images/app-connections/terraform-cloud/terraform-cloud-tokens-tab.png differ diff --git a/docs/images/app-connections/vercel/vercel-app-connection-created.png b/docs/images/app-connections/vercel/vercel-app-connection-created.png new file mode 100644 index 000000000..8fb371440 Binary files /dev/null and b/docs/images/app-connections/vercel/vercel-app-connection-created.png differ diff --git a/docs/images/app-connections/vercel/vercel-app-connection-modal.png b/docs/images/app-connections/vercel/vercel-app-connection-modal.png new file mode 100644 index 000000000..6b789713d Binary files /dev/null and b/docs/images/app-connections/vercel/vercel-app-connection-modal.png differ diff --git a/docs/images/app-connections/vercel/vercel-app-connection-option.png b/docs/images/app-connections/vercel/vercel-app-connection-option.png new file mode 100644 index 000000000..b4308a1a2 Binary files /dev/null and b/docs/images/app-connections/vercel/vercel-app-connection-option.png differ diff --git a/docs/images/app-connections/vercel/vercel-copy-token.png b/docs/images/app-connections/vercel/vercel-copy-token.png new file mode 100644 index 000000000..d6491c02e Binary files /dev/null and b/docs/images/app-connections/vercel/vercel-copy-token.png differ diff --git a/docs/images/app-connections/vercel/vercel-create-token.png b/docs/images/app-connections/vercel/vercel-create-token.png new file mode 100644 index 000000000..c507f0d14 Binary files /dev/null and b/docs/images/app-connections/vercel/vercel-create-token.png differ diff --git a/docs/images/app-connections/vercel/vercel-main-page.png b/docs/images/app-connections/vercel/vercel-main-page.png new file mode 100644 index 000000000..e1a28248e Binary files /dev/null and b/docs/images/app-connections/vercel/vercel-main-page.png differ diff --git a/docs/images/app-connections/vercel/vercel-settings-page.png b/docs/images/app-connections/vercel/vercel-settings-page.png new file mode 100644 index 000000000..86e67d6e1 Binary files /dev/null and b/docs/images/app-connections/vercel/vercel-settings-page.png differ diff --git a/docs/images/app-connections/vercel/vercel-token-created.png b/docs/images/app-connections/vercel/vercel-token-created.png new file mode 100644 index 000000000..5e57a4bd8 Binary files /dev/null and b/docs/images/app-connections/vercel/vercel-token-created.png differ diff --git a/docs/images/app-connections/windmill/create-windmill-access-token.png b/docs/images/app-connections/windmill/create-windmill-access-token.png new file mode 100644 index 000000000..0c4ae06f9 Binary files /dev/null and b/docs/images/app-connections/windmill/create-windmill-access-token.png differ diff --git a/docs/images/app-connections/windmill/select-windmill-connection.png b/docs/images/app-connections/windmill/select-windmill-connection.png new file mode 100644 index 000000000..dabead13e Binary files /dev/null and b/docs/images/app-connections/windmill/select-windmill-connection.png differ diff --git a/docs/images/app-connections/windmill/windmill-access-token-created.png b/docs/images/app-connections/windmill/windmill-access-token-created.png new file mode 100644 index 000000000..6035bf0ad Binary files /dev/null and b/docs/images/app-connections/windmill/windmill-access-token-created.png differ diff --git a/docs/images/app-connections/windmill/windmill-account-settings.png b/docs/images/app-connections/windmill/windmill-account-settings.png new file mode 100644 index 000000000..b2a63d997 Binary files /dev/null and b/docs/images/app-connections/windmill/windmill-account-settings.png differ diff --git a/docs/images/app-connections/windmill/windmill-copy-token.png b/docs/images/app-connections/windmill/windmill-copy-token.png new file mode 100644 index 000000000..4c5a5e8ca Binary files /dev/null and b/docs/images/app-connections/windmill/windmill-copy-token.png differ diff --git a/docs/images/app-connections/windmill/windmill-create-token.png b/docs/images/app-connections/windmill/windmill-create-token.png new file mode 100644 index 000000000..c5a4ac004 Binary files /dev/null and b/docs/images/app-connections/windmill/windmill-create-token.png differ diff --git a/docs/images/app-connections/windmill/windmill-new-token.png b/docs/images/app-connections/windmill/windmill-new-token.png new file mode 100644 index 000000000..9f103ad41 Binary files /dev/null and b/docs/images/app-connections/windmill/windmill-new-token.png differ diff --git a/docs/images/integrations/aws/integrations-aws-secret-manager-options.png b/docs/images/integrations/aws/integrations-aws-secret-manager-options.png index f8492cdfa..9450df8b7 100644 Binary files a/docs/images/integrations/aws/integrations-aws-secret-manager-options.png and b/docs/images/integrations/aws/integrations-aws-secret-manager-options.png differ diff --git a/docs/images/integrations/azure-app-configuration/create-integration-form.png b/docs/images/integrations/azure-app-configuration/create-integration-form.png index 58a935d8f..af2cbb062 100644 Binary files a/docs/images/integrations/azure-app-configuration/create-integration-form.png and b/docs/images/integrations/azure-app-configuration/create-integration-form.png differ diff --git a/docs/images/integrations/azure-key-vault/integrations-azure-key-vault-tenant-select.png b/docs/images/integrations/azure-key-vault/integrations-azure-key-vault-tenant-select.png new file mode 100644 index 000000000..6c63aaea1 Binary files /dev/null and b/docs/images/integrations/azure-key-vault/integrations-azure-key-vault-tenant-select.png differ diff --git a/docs/images/integrations/bitbucket/integrations-bitbucket-configuration.png b/docs/images/integrations/bitbucket/integrations-bitbucket-configuration.png new file mode 100644 index 000000000..658cefc7b Binary files /dev/null and b/docs/images/integrations/bitbucket/integrations-bitbucket-configuration.png differ diff --git a/docs/images/integrations/circleci/integrations-circleci-auth.png b/docs/images/integrations/circleci/integrations-circleci-auth.png index 055ebbf4a..73a5fd686 100644 Binary files a/docs/images/integrations/circleci/integrations-circleci-auth.png and b/docs/images/integrations/circleci/integrations-circleci-auth.png differ diff --git a/docs/images/integrations/circleci/integrations-circleci-create-context.png b/docs/images/integrations/circleci/integrations-circleci-create-context.png new file mode 100644 index 000000000..9d911953e Binary files /dev/null and b/docs/images/integrations/circleci/integrations-circleci-create-context.png differ diff --git a/docs/images/integrations/circleci/integrations-circleci-create-project.png b/docs/images/integrations/circleci/integrations-circleci-create-project.png new file mode 100644 index 000000000..73ab1e75a Binary files /dev/null and b/docs/images/integrations/circleci/integrations-circleci-create-project.png differ diff --git a/docs/images/integrations/circleci/integrations-circleci.png b/docs/images/integrations/circleci/integrations-circleci.png index 5ee9a9df0..cde74678d 100644 Binary files a/docs/images/integrations/circleci/integrations-circleci.png and b/docs/images/integrations/circleci/integrations-circleci.png differ diff --git a/docs/images/integrations/external/backstage/backstage-plugin-infisical.png b/docs/images/integrations/external/backstage/backstage-plugin-infisical.png new file mode 100644 index 000000000..5d7e6d350 Binary files /dev/null and b/docs/images/integrations/external/backstage/backstage-plugin-infisical.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-add-role.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-add-role.png new file mode 100644 index 000000000..6af836355 Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-add-role.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-add-to-team.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-add-to-team.png new file mode 100644 index 000000000..7c9a6e4ea Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-add-to-team.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-authorize.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-authorize.png new file mode 100644 index 000000000..455285047 Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-authorize.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-copy-api-key.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-copy-api-key.png new file mode 100644 index 000000000..adb372216 Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-copy-api-key.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create-api-key.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create-api-key.png new file mode 100644 index 000000000..f19c77e7e Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create-api-key.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create-service-account.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create-service-account.png new file mode 100644 index 000000000..3708f8517 Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create-service-account.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create-team.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create-team.png new file mode 100644 index 000000000..9cb703e12 Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create-team.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create.png new file mode 100644 index 000000000..e71fef6ee Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-generate-api-key.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-generate-api-key.png new file mode 100644 index 000000000..57c61e079 Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-generate-api-key.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-integrations.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-integrations.png new file mode 100644 index 000000000..35b18e0e3 Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-integrations.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-save-team.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-save-team.png new file mode 100644 index 000000000..34106b71f Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-save-team.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-sync.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-sync.png new file mode 100644 index 000000000..c3ceca1ba Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-sync.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-team-settings.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-team-settings.png new file mode 100644 index 000000000..a53229e25 Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-team-settings.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-user-settings.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-user-settings.png new file mode 100644 index 000000000..227b2770b Binary files /dev/null and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-user-settings.png differ diff --git a/docs/images/mfa-authenticator.png b/docs/images/mfa-authenticator.png new file mode 100644 index 000000000..2a72042ed Binary files /dev/null and b/docs/images/mfa-authenticator.png differ diff --git a/docs/images/mfa-email.png b/docs/images/mfa-email.png index f592ee239..eb6814c38 100644 Binary files a/docs/images/mfa-email.png and b/docs/images/mfa-email.png differ diff --git a/docs/images/platform/access-controls/abac-policies-by-auth.png b/docs/images/platform/access-controls/abac-policies-by-auth.png new file mode 100644 index 000000000..3c75aabbe Binary files /dev/null and b/docs/images/platform/access-controls/abac-policies-by-auth.png differ diff --git a/docs/images/platform/access-controls/abac-policy-oidc-format.png b/docs/images/platform/access-controls/abac-policy-oidc-format.png new file mode 100644 index 000000000..7fc76f50b Binary files /dev/null and b/docs/images/platform/access-controls/abac-policy-oidc-format.png differ diff --git a/docs/images/platform/admin-panels/access-org-admin-console.png b/docs/images/platform/admin-panels/access-org-admin-console.png index 057c82944..8d291d449 100644 Binary files a/docs/images/platform/admin-panels/access-org-admin-console.png and b/docs/images/platform/admin-panels/access-org-admin-console.png differ diff --git a/docs/images/platform/admin-panels/access-server-admin-panel.png b/docs/images/platform/admin-panels/access-server-admin-panel.png index a27735de0..706ba0814 100644 Binary files a/docs/images/platform/admin-panels/access-server-admin-panel.png and b/docs/images/platform/admin-panels/access-server-admin-panel.png differ diff --git a/docs/images/platform/admin-panels/admin-panel-general-1.png b/docs/images/platform/admin-panels/admin-panel-general-1.png new file mode 100644 index 000000000..45579586b Binary files /dev/null and b/docs/images/platform/admin-panels/admin-panel-general-1.png differ diff --git a/docs/images/platform/admin-panels/admin-panel-users.png b/docs/images/platform/admin-panels/admin-panel-users.png index 94add6d85..1f56f2f2d 100644 Binary files a/docs/images/platform/admin-panels/admin-panel-users.png and b/docs/images/platform/admin-panels/admin-panel-users.png differ diff --git a/docs/images/platform/admin-panels/auth-consent-usage.png b/docs/images/platform/admin-panels/auth-consent-usage.png new file mode 100644 index 000000000..6c6157b48 Binary files /dev/null and b/docs/images/platform/admin-panels/auth-consent-usage.png differ diff --git a/docs/images/platform/admin-panels/page-frame-usage.png b/docs/images/platform/admin-panels/page-frame-usage.png new file mode 100644 index 000000000..6dc5f7bcb Binary files /dev/null and b/docs/images/platform/admin-panels/page-frame-usage.png differ diff --git a/docs/images/platform/audit-logs/audit-logs-table.png b/docs/images/platform/audit-logs/audit-logs-table.png index a24b02bc0..ff8168aa5 100644 Binary files a/docs/images/platform/audit-logs/audit-logs-table.png and b/docs/images/platform/audit-logs/audit-logs-table.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-modal-oracle.png b/docs/images/platform/dynamic-secrets/dynamic-secret-modal-oracle.png deleted file mode 100644 index 053873a9c..000000000 Binary files a/docs/images/platform/dynamic-secrets/dynamic-secret-modal-oracle.png and /dev/null differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-modal-totp.png b/docs/images/platform/dynamic-secrets/dynamic-secret-modal-totp.png new file mode 100644 index 000000000..53326ebe2 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-modal-totp.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-mssql.png b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-mssql.png index 7f296a441..89994c55e 100644 Binary files a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-mssql.png and b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-mssql.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-oracle.png b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-oracle.png new file mode 100644 index 000000000..0e3f64e7c Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-oracle.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-postgresql.png b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-postgresql.png new file mode 100644 index 000000000..39fd4243b Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-postgresql.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-manual.png b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-manual.png new file mode 100644 index 000000000..788852d85 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-manual.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-url.png b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-url.png new file mode 100644 index 000000000..ede474a59 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-url.png differ diff --git a/docs/images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-modal.png b/docs/images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-modal.png new file mode 100644 index 000000000..2d48a93a3 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-modal.png differ diff --git a/docs/images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-setup-modal.png b/docs/images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-setup-modal.png new file mode 100644 index 000000000..ceb474770 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-setup-modal.png differ diff --git a/docs/images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-statements.png b/docs/images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-statements.png new file mode 100644 index 000000000..9ac56f456 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-statements.png differ diff --git a/docs/images/platform/dynamic-secrets/totp-lease-value.png b/docs/images/platform/dynamic-secrets/totp-lease-value.png new file mode 100644 index 000000000..af2ffe8e1 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/totp-lease-value.png differ diff --git a/docs/images/platform/gateways/assign-project.png b/docs/images/platform/gateways/assign-project.png new file mode 100644 index 000000000..a1ff61909 Binary files /dev/null and b/docs/images/platform/gateways/assign-project.png differ diff --git a/docs/images/platform/gateways/create-identity-for-gateway.png b/docs/images/platform/gateways/create-identity-for-gateway.png new file mode 100644 index 000000000..d7ef6b02a Binary files /dev/null and b/docs/images/platform/gateways/create-identity-for-gateway.png differ diff --git a/docs/images/platform/gateways/dynamic-secret.png b/docs/images/platform/gateways/dynamic-secret.png new file mode 100644 index 000000000..bf742413e Binary files /dev/null and b/docs/images/platform/gateways/dynamic-secret.png differ diff --git a/docs/images/platform/gateways/edit-gateway.png b/docs/images/platform/gateways/edit-gateway.png new file mode 100644 index 000000000..04ef2a7d2 Binary files /dev/null and b/docs/images/platform/gateways/edit-gateway.png differ diff --git a/docs/images/platform/gateways/gateway-list.png b/docs/images/platform/gateways/gateway-list.png new file mode 100644 index 000000000..11f8206fe Binary files /dev/null and b/docs/images/platform/gateways/gateway-list.png differ diff --git a/docs/images/platform/identities/identities-org-create-jwt-auth-method-jwks.png b/docs/images/platform/identities/identities-org-create-jwt-auth-method-jwks.png new file mode 100644 index 000000000..1f693b346 Binary files /dev/null and b/docs/images/platform/identities/identities-org-create-jwt-auth-method-jwks.png differ diff --git a/docs/images/platform/identities/identities-org-create-jwt-auth-method-static.png b/docs/images/platform/identities/identities-org-create-jwt-auth-method-static.png new file mode 100644 index 000000000..5d434a6e0 Binary files /dev/null and b/docs/images/platform/identities/identities-org-create-jwt-auth-method-static.png differ diff --git a/docs/images/platform/kms/aws/encryption-modal-provider-select.png b/docs/images/platform/kms/aws/encryption-modal-provider-select.png deleted file mode 100644 index 704043a74..000000000 Binary files a/docs/images/platform/kms/aws/encryption-modal-provider-select.png and /dev/null differ diff --git a/docs/images/platform/kms/encryption-modal-provider-select.png b/docs/images/platform/kms/encryption-modal-provider-select.png new file mode 100644 index 000000000..5bc696021 Binary files /dev/null and b/docs/images/platform/kms/encryption-modal-provider-select.png differ diff --git a/docs/images/platform/kms/aws/encryption-org-settings-add.png b/docs/images/platform/kms/encryption-org-settings-add.png similarity index 100% rename from docs/images/platform/kms/aws/encryption-org-settings-add.png rename to docs/images/platform/kms/encryption-org-settings-add.png diff --git a/docs/images/platform/kms/aws/encryption-org-settings.png b/docs/images/platform/kms/encryption-org-settings.png similarity index 100% rename from docs/images/platform/kms/aws/encryption-org-settings.png rename to docs/images/platform/kms/encryption-org-settings.png diff --git a/docs/images/platform/kms/gcp/gcp-add-modal-filled.png b/docs/images/platform/kms/gcp/gcp-add-modal-filled.png new file mode 100644 index 000000000..c6d4b0725 Binary files /dev/null and b/docs/images/platform/kms/gcp/gcp-add-modal-filled.png differ diff --git a/docs/images/platform/kms/gcp/keyring-create.png b/docs/images/platform/kms/gcp/keyring-create.png new file mode 100644 index 000000000..c03097cc1 Binary files /dev/null and b/docs/images/platform/kms/gcp/keyring-create.png differ diff --git a/docs/images/platform/kms/gcp/project-settings.png b/docs/images/platform/kms/gcp/project-settings.png new file mode 100644 index 000000000..915115204 Binary files /dev/null and b/docs/images/platform/kms/gcp/project-settings.png differ diff --git a/docs/images/platform/kms/gcp/select-gcp-kms-in-project.png b/docs/images/platform/kms/gcp/select-gcp-kms-in-project.png new file mode 100644 index 000000000..18f24d304 Binary files /dev/null and b/docs/images/platform/kms/gcp/select-gcp-kms-in-project.png differ diff --git a/docs/images/platform/kms/gcp/service-account-form.png b/docs/images/platform/kms/gcp/service-account-form.png new file mode 100644 index 000000000..eea0dc324 Binary files /dev/null and b/docs/images/platform/kms/gcp/service-account-form.png differ diff --git a/docs/images/platform/kms/gcp/service-account-permissions.png b/docs/images/platform/kms/gcp/service-account-permissions.png new file mode 100644 index 000000000..d528199dc Binary files /dev/null and b/docs/images/platform/kms/gcp/service-account-permissions.png differ diff --git a/docs/images/platform/kms/hsm/encryption-strategy.png b/docs/images/platform/kms/hsm/encryption-strategy.png new file mode 100644 index 000000000..18eab934d Binary files /dev/null and b/docs/images/platform/kms/hsm/encryption-strategy.png differ diff --git a/docs/images/platform/kms/hsm/hsm-illustration.png b/docs/images/platform/kms/hsm/hsm-illustration.png new file mode 100644 index 000000000..b35c3f10a Binary files /dev/null and b/docs/images/platform/kms/hsm/hsm-illustration.png differ diff --git a/docs/images/platform/kms/hsm/server-admin-console.png b/docs/images/platform/kms/hsm/server-admin-console.png new file mode 100644 index 000000000..661cfc2d2 Binary files /dev/null and b/docs/images/platform/kms/hsm/server-admin-console.png differ diff --git a/docs/images/platform/kms/infisical-kms/signing/add-new-rsa-key.png b/docs/images/platform/kms/infisical-kms/signing/add-new-rsa-key.png new file mode 100644 index 000000000..97d7ca246 Binary files /dev/null and b/docs/images/platform/kms/infisical-kms/signing/add-new-rsa-key.png differ diff --git a/docs/images/platform/kms/infisical-kms/signing/copy-signature.png b/docs/images/platform/kms/infisical-kms/signing/copy-signature.png new file mode 100644 index 000000000..2644b358b Binary files /dev/null and b/docs/images/platform/kms/infisical-kms/signing/copy-signature.png differ diff --git a/docs/images/platform/kms/infisical-kms/signing/sign-data-modal.png b/docs/images/platform/kms/infisical-kms/signing/sign-data-modal.png new file mode 100644 index 000000000..da8a01438 Binary files /dev/null and b/docs/images/platform/kms/infisical-kms/signing/sign-data-modal.png differ diff --git a/docs/images/platform/kms/infisical-kms/signing/sign-options.png b/docs/images/platform/kms/infisical-kms/signing/sign-options.png new file mode 100644 index 000000000..7129c1d5b Binary files /dev/null and b/docs/images/platform/kms/infisical-kms/signing/sign-options.png differ diff --git a/docs/images/platform/kms/infisical-kms/signing/signature-verified.png b/docs/images/platform/kms/infisical-kms/signing/signature-verified.png new file mode 100644 index 000000000..70b7856b6 Binary files /dev/null and b/docs/images/platform/kms/infisical-kms/signing/signature-verified.png differ diff --git a/docs/images/platform/kms/infisical-kms/signing/verify-data-modal.png b/docs/images/platform/kms/infisical-kms/signing/verify-data-modal.png new file mode 100644 index 000000000..6d3683c9f Binary files /dev/null and b/docs/images/platform/kms/infisical-kms/signing/verify-data-modal.png differ diff --git a/docs/images/platform/kms/kmip/kmip-assign-custom-role-proxy.png b/docs/images/platform/kms/kmip/kmip-assign-custom-role-proxy.png new file mode 100644 index 000000000..498fc2012 Binary files /dev/null and b/docs/images/platform/kms/kmip/kmip-assign-custom-role-proxy.png differ diff --git a/docs/images/platform/kms/kmip/kmip-assign-mi-to-role.png b/docs/images/platform/kms/kmip/kmip-assign-mi-to-role.png new file mode 100644 index 000000000..93d4c7f84 Binary files /dev/null and b/docs/images/platform/kms/kmip/kmip-assign-mi-to-role.png differ diff --git a/docs/images/platform/kms/kmip/kmip-client-cert-config-modal.png b/docs/images/platform/kms/kmip/kmip-client-cert-config-modal.png new file mode 100644 index 000000000..45eb8c830 Binary files /dev/null and b/docs/images/platform/kms/kmip/kmip-client-cert-config-modal.png differ diff --git a/docs/images/platform/kms/kmip/kmip-client-certificate-modal.png b/docs/images/platform/kms/kmip/kmip-client-certificate-modal.png new file mode 100644 index 000000000..d6f9fc2ad Binary files /dev/null and b/docs/images/platform/kms/kmip/kmip-client-certificate-modal.png differ diff --git a/docs/images/platform/kms/kmip/kmip-client-generate-cert.png b/docs/images/platform/kms/kmip/kmip-client-generate-cert.png new file mode 100644 index 000000000..40b49f570 Binary files /dev/null and b/docs/images/platform/kms/kmip/kmip-client-generate-cert.png differ diff --git a/docs/images/platform/kms/kmip/kmip-client-modal.png b/docs/images/platform/kms/kmip/kmip-client-modal.png new file mode 100644 index 000000000..5038c2a87 Binary files /dev/null and b/docs/images/platform/kms/kmip/kmip-client-modal.png differ diff --git a/docs/images/platform/kms/kmip/kmip-client-overview.png b/docs/images/platform/kms/kmip/kmip-client-overview.png new file mode 100644 index 000000000..794a91d42 Binary files /dev/null and b/docs/images/platform/kms/kmip/kmip-client-overview.png differ diff --git a/docs/images/platform/kms/kmip/kmip-create-custom-role.png b/docs/images/platform/kms/kmip/kmip-create-custom-role.png new file mode 100644 index 000000000..5e6a5145d Binary files /dev/null and b/docs/images/platform/kms/kmip/kmip-create-custom-role.png differ diff --git a/docs/images/platform/kms/kmip/kmip-create-mi.png b/docs/images/platform/kms/kmip/kmip-create-mi.png new file mode 100644 index 000000000..ed8a0988c Binary files /dev/null and b/docs/images/platform/kms/kmip/kmip-create-mi.png differ diff --git a/docs/images/platform/kms/kmip/kmip-org-setup-modal.png b/docs/images/platform/kms/kmip/kmip-org-setup-modal.png new file mode 100644 index 000000000..6fc6dd7ce Binary files /dev/null and b/docs/images/platform/kms/kmip/kmip-org-setup-modal.png differ diff --git a/docs/images/platform/kms/kmip/kmip-org-setup-navigation.png b/docs/images/platform/kms/kmip/kmip-org-setup-navigation.png new file mode 100644 index 000000000..5a77878e5 Binary files /dev/null and b/docs/images/platform/kms/kmip/kmip-org-setup-navigation.png differ diff --git a/docs/images/platform/pki/est/template-enrollment-modal.png b/docs/images/platform/pki/est/template-enrollment-modal.png index c7fed648e..60ed273d7 100644 Binary files a/docs/images/platform/pki/est/template-enrollment-modal.png and b/docs/images/platform/pki/est/template-enrollment-modal.png differ diff --git a/docs/images/platform/project-access-requests/access-comment.png b/docs/images/platform/project-access-requests/access-comment.png new file mode 100644 index 000000000..c85a07bf1 Binary files /dev/null and b/docs/images/platform/project-access-requests/access-comment.png differ diff --git a/docs/images/platform/project-access-requests/all-project-view.png b/docs/images/platform/project-access-requests/all-project-view.png new file mode 100644 index 000000000..13c6b42a4 Binary files /dev/null and b/docs/images/platform/project-access-requests/all-project-view.png differ diff --git a/docs/images/platform/project-access-requests/request-access.png b/docs/images/platform/project-access-requests/request-access.png new file mode 100644 index 000000000..53f492ed4 Binary files /dev/null and b/docs/images/platform/project-access-requests/request-access.png differ diff --git a/docs/images/platform/secret-scanning/exposed-secret.png b/docs/images/platform/secret-scanning/exposed-secret.png new file mode 100644 index 000000000..727765292 Binary files /dev/null and b/docs/images/platform/secret-scanning/exposed-secret.png differ diff --git a/docs/images/platform/secret-scanning/needs-attention.png b/docs/images/platform/secret-scanning/needs-attention.png new file mode 100644 index 000000000..6ac664ead Binary files /dev/null and b/docs/images/platform/secret-scanning/needs-attention.png differ diff --git a/docs/images/platform/secret-scanning/overview.png b/docs/images/platform/secret-scanning/overview.png new file mode 100644 index 000000000..19981fa11 Binary files /dev/null and b/docs/images/platform/secret-scanning/overview.png differ diff --git a/docs/images/platform/ssh/ssh-ca-public-key.png b/docs/images/platform/ssh/ssh-ca-public-key.png new file mode 100644 index 000000000..42653df4a Binary files /dev/null and b/docs/images/platform/ssh/ssh-ca-public-key.png differ diff --git a/docs/images/platform/ssh/ssh-client-ca-public-key.png b/docs/images/platform/ssh/ssh-client-ca-public-key.png new file mode 100644 index 000000000..058b844fb Binary files /dev/null and b/docs/images/platform/ssh/ssh-client-ca-public-key.png differ diff --git a/docs/images/platform/ssh/ssh-client-create-ca-1.png b/docs/images/platform/ssh/ssh-client-create-ca-1.png new file mode 100644 index 000000000..30658944f Binary files /dev/null and b/docs/images/platform/ssh/ssh-client-create-ca-1.png differ diff --git a/docs/images/platform/ssh/ssh-client-create-ca-2.png b/docs/images/platform/ssh/ssh-client-create-ca-2.png new file mode 100644 index 000000000..3a0bf155c Binary files /dev/null and b/docs/images/platform/ssh/ssh-client-create-ca-2.png differ diff --git a/docs/images/platform/ssh/ssh-client-create-template-1.png b/docs/images/platform/ssh/ssh-client-create-template-1.png new file mode 100644 index 000000000..0c0ba97c8 Binary files /dev/null and b/docs/images/platform/ssh/ssh-client-create-template-1.png differ diff --git a/docs/images/platform/ssh/ssh-client-create-template-2.png b/docs/images/platform/ssh/ssh-client-create-template-2.png new file mode 100644 index 000000000..8c64ec62b Binary files /dev/null and b/docs/images/platform/ssh/ssh-client-create-template-2.png differ diff --git a/docs/images/platform/ssh/ssh-create-ca-1.png b/docs/images/platform/ssh/ssh-create-ca-1.png new file mode 100644 index 000000000..e9a5b7f06 Binary files /dev/null and b/docs/images/platform/ssh/ssh-create-ca-1.png differ diff --git a/docs/images/platform/ssh/ssh-create-ca-2.png b/docs/images/platform/ssh/ssh-create-ca-2.png new file mode 100644 index 000000000..63025025f Binary files /dev/null and b/docs/images/platform/ssh/ssh-create-ca-2.png differ diff --git a/docs/images/platform/ssh/ssh-create-template-1.png b/docs/images/platform/ssh/ssh-create-template-1.png new file mode 100644 index 000000000..9d9420948 Binary files /dev/null and b/docs/images/platform/ssh/ssh-create-template-1.png differ diff --git a/docs/images/platform/ssh/ssh-create-template-2.png b/docs/images/platform/ssh/ssh-create-template-2.png new file mode 100644 index 000000000..7b93e6d80 Binary files /dev/null and b/docs/images/platform/ssh/ssh-create-template-2.png differ diff --git a/docs/images/platform/ssh/ssh-host-ca-public-key.png b/docs/images/platform/ssh/ssh-host-ca-public-key.png new file mode 100644 index 000000000..f77034896 Binary files /dev/null and b/docs/images/platform/ssh/ssh-host-ca-public-key.png differ diff --git a/docs/images/platform/ssh/ssh-host-create-ca-1.png b/docs/images/platform/ssh/ssh-host-create-ca-1.png new file mode 100644 index 000000000..f064dec35 Binary files /dev/null and b/docs/images/platform/ssh/ssh-host-create-ca-1.png differ diff --git a/docs/images/platform/ssh/ssh-host-create-ca-2.png b/docs/images/platform/ssh/ssh-host-create-ca-2.png new file mode 100644 index 000000000..75b0fe76c Binary files /dev/null and b/docs/images/platform/ssh/ssh-host-create-ca-2.png differ diff --git a/docs/images/platform/ssh/ssh-host-create-template-1.png b/docs/images/platform/ssh/ssh-host-create-template-1.png new file mode 100644 index 000000000..e8ec9ec20 Binary files /dev/null and b/docs/images/platform/ssh/ssh-host-create-template-1.png differ diff --git a/docs/images/platform/ssh/ssh-host-create-template-2.png b/docs/images/platform/ssh/ssh-host-create-template-2.png new file mode 100644 index 000000000..95b41be9b Binary files /dev/null and b/docs/images/platform/ssh/ssh-host-create-template-2.png differ diff --git a/docs/images/platform/ssh/ssh-host-issue-cert-1.png b/docs/images/platform/ssh/ssh-host-issue-cert-1.png new file mode 100644 index 000000000..c4483eb83 Binary files /dev/null and b/docs/images/platform/ssh/ssh-host-issue-cert-1.png differ diff --git a/docs/images/platform/ssh/ssh-host-issue-cert-2.png b/docs/images/platform/ssh/ssh-host-issue-cert-2.png new file mode 100644 index 000000000..ec5f677bc Binary files /dev/null and b/docs/images/platform/ssh/ssh-host-issue-cert-2.png differ diff --git a/docs/images/platform/ssh/ssh-host-issue-cert-3.png b/docs/images/platform/ssh/ssh-host-issue-cert-3.png new file mode 100644 index 000000000..41af0c9f3 Binary files /dev/null and b/docs/images/platform/ssh/ssh-host-issue-cert-3.png differ diff --git a/docs/images/platform/ssh/ssh-project.png b/docs/images/platform/ssh/ssh-project.png new file mode 100644 index 000000000..9b28f04ab Binary files /dev/null and b/docs/images/platform/ssh/ssh-project.png differ diff --git a/docs/images/platform/ssh/v2/ssh-add-bootstrap-role-1.png b/docs/images/platform/ssh/v2/ssh-add-bootstrap-role-1.png new file mode 100644 index 000000000..8acc1efe9 Binary files /dev/null and b/docs/images/platform/ssh/v2/ssh-add-bootstrap-role-1.png differ diff --git a/docs/images/platform/ssh/v2/ssh-add-bootstrap-role-2.png b/docs/images/platform/ssh/v2/ssh-add-bootstrap-role-2.png new file mode 100644 index 000000000..2ad9804d4 Binary files /dev/null and b/docs/images/platform/ssh/v2/ssh-add-bootstrap-role-2.png differ diff --git a/docs/images/platform/ssh/v2/ssh-add-identity-to-project.png b/docs/images/platform/ssh/v2/ssh-add-identity-to-project.png new file mode 100644 index 000000000..83bd3c984 Binary files /dev/null and b/docs/images/platform/ssh/v2/ssh-add-identity-to-project.png differ diff --git a/docs/images/platform/ssh/v2/ssh-add-user.png b/docs/images/platform/ssh/v2/ssh-add-user.png new file mode 100644 index 000000000..363a2a898 Binary files /dev/null and b/docs/images/platform/ssh/v2/ssh-add-user.png differ diff --git a/docs/images/platform/ssh/v2/ssh-added-hosts.png b/docs/images/platform/ssh/v2/ssh-added-hosts.png new file mode 100644 index 000000000..20c7f9861 Binary files /dev/null and b/docs/images/platform/ssh/v2/ssh-added-hosts.png differ diff --git a/docs/images/platform/ssh/v2/ssh-create-project.png b/docs/images/platform/ssh/v2/ssh-create-project.png new file mode 100644 index 000000000..792d49612 Binary files /dev/null and b/docs/images/platform/ssh/v2/ssh-create-project.png differ diff --git a/docs/images/platform/ssh/v2/ssh-host-login-mappings.png b/docs/images/platform/ssh/v2/ssh-host-login-mappings.png new file mode 100644 index 000000000..cdc192274 Binary files /dev/null and b/docs/images/platform/ssh/v2/ssh-host-login-mappings.png differ diff --git a/docs/images/secret-rotations-v2/auth0-client-secret/auth0-app-client-id.png b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-app-client-id.png new file mode 100644 index 000000000..5540b7312 Binary files /dev/null and b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-app-client-id.png differ diff --git a/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-configuration.png b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-configuration.png new file mode 100644 index 000000000..f254c244c Binary files /dev/null and b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-configuration.png differ diff --git a/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-confirm.png b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-confirm.png new file mode 100644 index 000000000..1dcd4715c Binary files /dev/null and b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-confirm.png differ diff --git a/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-created.png b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-created.png new file mode 100644 index 000000000..fd82360da Binary files /dev/null and b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-created.png differ diff --git a/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-details.png b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-details.png new file mode 100644 index 000000000..42c49f808 Binary files /dev/null and b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-details.png differ diff --git a/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-parameters.png b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-parameters.png new file mode 100644 index 000000000..1f2fc64f9 Binary files /dev/null and b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-parameters.png differ diff --git a/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-secrets-mapping.png b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-secrets-mapping.png new file mode 100644 index 000000000..c91e54559 Binary files /dev/null and b/docs/images/secret-rotations-v2/auth0-client-secret/auth0-client-secret-secrets-mapping.png differ diff --git a/docs/images/secret-rotations-v2/auth0-client-secret/select-auth0-client-secret-option.png b/docs/images/secret-rotations-v2/auth0-client-secret/select-auth0-client-secret-option.png new file mode 100644 index 000000000..d042f46fb Binary files /dev/null and b/docs/images/secret-rotations-v2/auth0-client-secret/select-auth0-client-secret-option.png differ diff --git a/docs/images/secret-rotations-v2/generic/add-secret-rotation.png b/docs/images/secret-rotations-v2/generic/add-secret-rotation.png new file mode 100644 index 000000000..86b84001b Binary files /dev/null and b/docs/images/secret-rotations-v2/generic/add-secret-rotation.png differ diff --git a/docs/images/secret-rotations-v2/mssql-credentials/mssql-credentials-configuration.png b/docs/images/secret-rotations-v2/mssql-credentials/mssql-credentials-configuration.png new file mode 100644 index 000000000..fe2cc7f58 Binary files /dev/null and b/docs/images/secret-rotations-v2/mssql-credentials/mssql-credentials-configuration.png differ diff --git a/docs/images/secret-rotations-v2/mssql-credentials/mssql-credentials-confirm.png b/docs/images/secret-rotations-v2/mssql-credentials/mssql-credentials-confirm.png new file mode 100644 index 000000000..3b0c1c3d0 Binary files /dev/null and b/docs/images/secret-rotations-v2/mssql-credentials/mssql-credentials-confirm.png differ diff --git a/docs/images/secret-rotations-v2/mssql-credentials/mssql-credentials-created.png b/docs/images/secret-rotations-v2/mssql-credentials/mssql-credentials-created.png new file mode 100644 index 000000000..22cfb6478 Binary files /dev/null and b/docs/images/secret-rotations-v2/mssql-credentials/mssql-credentials-created.png differ diff --git a/docs/images/secret-rotations-v2/mssql-credentials/mssql-credentials-details.png b/docs/images/secret-rotations-v2/mssql-credentials/mssql-credentials-details.png new file mode 100644 index 000000000..a19ea2aae Binary files /dev/null and b/docs/images/secret-rotations-v2/mssql-credentials/mssql-credentials-details.png differ diff --git a/docs/images/secret-rotations-v2/mssql-credentials/mssql-credentials-parameters.png b/docs/images/secret-rotations-v2/mssql-credentials/mssql-credentials-parameters.png new file mode 100644 index 000000000..2778bad8e Binary files /dev/null and b/docs/images/secret-rotations-v2/mssql-credentials/mssql-credentials-parameters.png differ diff --git a/docs/images/secret-rotations-v2/mssql-credentials/mssql-credentials-secrets-mapping.png b/docs/images/secret-rotations-v2/mssql-credentials/mssql-credentials-secrets-mapping.png new file mode 100644 index 000000000..4171dad75 Binary files /dev/null and b/docs/images/secret-rotations-v2/mssql-credentials/mssql-credentials-secrets-mapping.png differ diff --git a/docs/images/secret-rotations-v2/mssql-credentials/select-mssql-credentials-option.png b/docs/images/secret-rotations-v2/mssql-credentials/select-mssql-credentials-option.png new file mode 100644 index 000000000..7a212edaa Binary files /dev/null and b/docs/images/secret-rotations-v2/mssql-credentials/select-mssql-credentials-option.png differ diff --git a/docs/images/secret-rotations-v2/postgres-credentials/postgres-credentials-configuration.png b/docs/images/secret-rotations-v2/postgres-credentials/postgres-credentials-configuration.png new file mode 100644 index 000000000..c2d35686f Binary files /dev/null and b/docs/images/secret-rotations-v2/postgres-credentials/postgres-credentials-configuration.png differ diff --git a/docs/images/secret-rotations-v2/postgres-credentials/postgres-credentials-confirm.png b/docs/images/secret-rotations-v2/postgres-credentials/postgres-credentials-confirm.png new file mode 100644 index 000000000..35806b0ea Binary files /dev/null and b/docs/images/secret-rotations-v2/postgres-credentials/postgres-credentials-confirm.png differ diff --git a/docs/images/secret-rotations-v2/postgres-credentials/postgres-credentials-created.png b/docs/images/secret-rotations-v2/postgres-credentials/postgres-credentials-created.png new file mode 100644 index 000000000..efc733df8 Binary files /dev/null and b/docs/images/secret-rotations-v2/postgres-credentials/postgres-credentials-created.png differ diff --git a/docs/images/secret-rotations-v2/postgres-credentials/postgres-credentials-details.png b/docs/images/secret-rotations-v2/postgres-credentials/postgres-credentials-details.png new file mode 100644 index 000000000..35904a466 Binary files /dev/null and b/docs/images/secret-rotations-v2/postgres-credentials/postgres-credentials-details.png differ diff --git a/docs/images/secret-rotations-v2/postgres-credentials/postgres-credentials-parameters.png b/docs/images/secret-rotations-v2/postgres-credentials/postgres-credentials-parameters.png new file mode 100644 index 000000000..800d9d823 Binary files /dev/null and b/docs/images/secret-rotations-v2/postgres-credentials/postgres-credentials-parameters.png differ diff --git a/docs/images/secret-rotations-v2/postgres-credentials/postgres-credentials-secrets-mapping.png b/docs/images/secret-rotations-v2/postgres-credentials/postgres-credentials-secrets-mapping.png new file mode 100644 index 000000000..575e58abc Binary files /dev/null and b/docs/images/secret-rotations-v2/postgres-credentials/postgres-credentials-secrets-mapping.png differ diff --git a/docs/images/secret-rotations-v2/postgres-credentials/select-postgres-credentials-option.png b/docs/images/secret-rotations-v2/postgres-credentials/select-postgres-credentials-option.png new file mode 100644 index 000000000..64b3055e9 Binary files /dev/null and b/docs/images/secret-rotations-v2/postgres-credentials/select-postgres-credentials-option.png differ diff --git a/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-created.png b/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-created.png new file mode 100644 index 000000000..009331fe6 Binary files /dev/null and b/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-created.png differ diff --git a/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-destination.png b/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-destination.png new file mode 100644 index 000000000..d7136cd9a Binary files /dev/null and b/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-destination.png differ diff --git a/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-details.png b/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-details.png new file mode 100644 index 000000000..2d4b59a3f Binary files /dev/null and b/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-details.png differ diff --git a/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-options.png b/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-options.png new file mode 100644 index 000000000..6a4a68f2c Binary files /dev/null and b/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-options.png differ diff --git a/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-review.png b/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-review.png new file mode 100644 index 000000000..0db843ede Binary files /dev/null and b/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-review.png differ diff --git a/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-source.png b/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-source.png new file mode 100644 index 000000000..4a5ec9904 Binary files /dev/null and b/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-source.png differ diff --git a/docs/images/secret-syncs/aws-parameter-store/select-aws-parameter-store-option.png b/docs/images/secret-syncs/aws-parameter-store/select-aws-parameter-store-option.png new file mode 100644 index 000000000..d43a79715 Binary files /dev/null and b/docs/images/secret-syncs/aws-parameter-store/select-aws-parameter-store-option.png differ diff --git a/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-created.png b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-created.png new file mode 100644 index 000000000..d788d27f9 Binary files /dev/null and b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-created.png differ diff --git a/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-destination.png b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-destination.png new file mode 100644 index 000000000..07bbf6212 Binary files /dev/null and b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-destination.png differ diff --git a/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-details.png b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-details.png new file mode 100644 index 000000000..a958af9fd Binary files /dev/null and b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-details.png differ diff --git a/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-options.png b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-options.png new file mode 100644 index 000000000..89ec35e4d Binary files /dev/null and b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-options.png differ diff --git a/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-review.png b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-review.png new file mode 100644 index 000000000..ddffa9190 Binary files /dev/null and b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-review.png differ diff --git a/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-source.png b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-source.png new file mode 100644 index 000000000..8da1d5601 Binary files /dev/null and b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-source.png differ diff --git a/docs/images/secret-syncs/aws-secrets-manager/select-aws-secrets-manager-option.png b/docs/images/secret-syncs/aws-secrets-manager/select-aws-secrets-manager-option.png new file mode 100644 index 000000000..7d32012c1 Binary files /dev/null and b/docs/images/secret-syncs/aws-secrets-manager/select-aws-secrets-manager-option.png differ diff --git a/docs/images/secret-syncs/azure-app-configuration/app-config-destination.png b/docs/images/secret-syncs/azure-app-configuration/app-config-destination.png new file mode 100644 index 000000000..22610ae22 Binary files /dev/null and b/docs/images/secret-syncs/azure-app-configuration/app-config-destination.png differ diff --git a/docs/images/secret-syncs/azure-app-configuration/app-config-details.png b/docs/images/secret-syncs/azure-app-configuration/app-config-details.png new file mode 100644 index 000000000..e007bd7f5 Binary files /dev/null and b/docs/images/secret-syncs/azure-app-configuration/app-config-details.png differ diff --git a/docs/images/secret-syncs/azure-app-configuration/app-config-options.png b/docs/images/secret-syncs/azure-app-configuration/app-config-options.png new file mode 100644 index 000000000..94b70a547 Binary files /dev/null and b/docs/images/secret-syncs/azure-app-configuration/app-config-options.png differ diff --git a/docs/images/secret-syncs/azure-app-configuration/app-config-review.png b/docs/images/secret-syncs/azure-app-configuration/app-config-review.png new file mode 100644 index 000000000..db10d6d41 Binary files /dev/null and b/docs/images/secret-syncs/azure-app-configuration/app-config-review.png differ diff --git a/docs/images/secret-syncs/azure-app-configuration/app-config-source.png b/docs/images/secret-syncs/azure-app-configuration/app-config-source.png new file mode 100644 index 000000000..df2e8b22f Binary files /dev/null and b/docs/images/secret-syncs/azure-app-configuration/app-config-source.png differ diff --git a/docs/images/secret-syncs/azure-app-configuration/app-config-synced.png b/docs/images/secret-syncs/azure-app-configuration/app-config-synced.png new file mode 100644 index 000000000..1d229319f Binary files /dev/null and b/docs/images/secret-syncs/azure-app-configuration/app-config-synced.png differ diff --git a/docs/images/secret-syncs/azure-app-configuration/select-app-config.png b/docs/images/secret-syncs/azure-app-configuration/select-app-config.png new file mode 100644 index 000000000..42ad36997 Binary files /dev/null and b/docs/images/secret-syncs/azure-app-configuration/select-app-config.png differ diff --git a/docs/images/secret-syncs/azure-key-vault/select-key-vault-option.png b/docs/images/secret-syncs/azure-key-vault/select-key-vault-option.png new file mode 100644 index 000000000..6380b315b Binary files /dev/null and b/docs/images/secret-syncs/azure-key-vault/select-key-vault-option.png differ diff --git a/docs/images/secret-syncs/azure-key-vault/vault-destination.png b/docs/images/secret-syncs/azure-key-vault/vault-destination.png new file mode 100644 index 000000000..636c892f4 Binary files /dev/null and b/docs/images/secret-syncs/azure-key-vault/vault-destination.png differ diff --git a/docs/images/secret-syncs/azure-key-vault/vault-details.png b/docs/images/secret-syncs/azure-key-vault/vault-details.png new file mode 100644 index 000000000..fdaa51ec8 Binary files /dev/null and b/docs/images/secret-syncs/azure-key-vault/vault-details.png differ diff --git a/docs/images/secret-syncs/azure-key-vault/vault-options.png b/docs/images/secret-syncs/azure-key-vault/vault-options.png new file mode 100644 index 000000000..f35d1cc3b Binary files /dev/null and b/docs/images/secret-syncs/azure-key-vault/vault-options.png differ diff --git a/docs/images/secret-syncs/azure-key-vault/vault-review.png b/docs/images/secret-syncs/azure-key-vault/vault-review.png new file mode 100644 index 000000000..51572a4ca Binary files /dev/null and b/docs/images/secret-syncs/azure-key-vault/vault-review.png differ diff --git a/docs/images/secret-syncs/azure-key-vault/vault-source.png b/docs/images/secret-syncs/azure-key-vault/vault-source.png new file mode 100644 index 000000000..5e5e624f1 Binary files /dev/null and b/docs/images/secret-syncs/azure-key-vault/vault-source.png differ diff --git a/docs/images/secret-syncs/azure-key-vault/vault-synced.png b/docs/images/secret-syncs/azure-key-vault/vault-synced.png new file mode 100644 index 000000000..828aa42a8 Binary files /dev/null and b/docs/images/secret-syncs/azure-key-vault/vault-synced.png differ diff --git a/docs/images/secret-syncs/camunda/camunda-created.png b/docs/images/secret-syncs/camunda/camunda-created.png new file mode 100644 index 000000000..10778748d Binary files /dev/null and b/docs/images/secret-syncs/camunda/camunda-created.png differ diff --git a/docs/images/secret-syncs/camunda/camunda-destination.png b/docs/images/secret-syncs/camunda/camunda-destination.png new file mode 100644 index 000000000..4dffbe1e6 Binary files /dev/null and b/docs/images/secret-syncs/camunda/camunda-destination.png differ diff --git a/docs/images/secret-syncs/camunda/camunda-details.png b/docs/images/secret-syncs/camunda/camunda-details.png new file mode 100644 index 000000000..06402a67f Binary files /dev/null and b/docs/images/secret-syncs/camunda/camunda-details.png differ diff --git a/docs/images/secret-syncs/camunda/camunda-options.png b/docs/images/secret-syncs/camunda/camunda-options.png new file mode 100644 index 000000000..b38153833 Binary files /dev/null and b/docs/images/secret-syncs/camunda/camunda-options.png differ diff --git a/docs/images/secret-syncs/camunda/camunda-review.png b/docs/images/secret-syncs/camunda/camunda-review.png new file mode 100644 index 000000000..6283b38b0 Binary files /dev/null and b/docs/images/secret-syncs/camunda/camunda-review.png differ diff --git a/docs/images/secret-syncs/camunda/camunda-source.png b/docs/images/secret-syncs/camunda/camunda-source.png new file mode 100644 index 000000000..5b7332ebd Binary files /dev/null and b/docs/images/secret-syncs/camunda/camunda-source.png differ diff --git a/docs/images/secret-syncs/camunda/select-camunda-option.png b/docs/images/secret-syncs/camunda/select-camunda-option.png new file mode 100644 index 000000000..e9e138e90 Binary files /dev/null and b/docs/images/secret-syncs/camunda/select-camunda-option.png differ diff --git a/docs/images/secret-syncs/databricks/databricks-created.png b/docs/images/secret-syncs/databricks/databricks-created.png new file mode 100644 index 000000000..829c5196c Binary files /dev/null and b/docs/images/secret-syncs/databricks/databricks-created.png differ diff --git a/docs/images/secret-syncs/databricks/databricks-destination.png b/docs/images/secret-syncs/databricks/databricks-destination.png new file mode 100644 index 000000000..8b76f8f65 Binary files /dev/null and b/docs/images/secret-syncs/databricks/databricks-destination.png differ diff --git a/docs/images/secret-syncs/databricks/databricks-details.png b/docs/images/secret-syncs/databricks/databricks-details.png new file mode 100644 index 000000000..71630f158 Binary files /dev/null and b/docs/images/secret-syncs/databricks/databricks-details.png differ diff --git a/docs/images/secret-syncs/databricks/databricks-options.png b/docs/images/secret-syncs/databricks/databricks-options.png new file mode 100644 index 000000000..76890d183 Binary files /dev/null and b/docs/images/secret-syncs/databricks/databricks-options.png differ diff --git a/docs/images/secret-syncs/databricks/databricks-review.png b/docs/images/secret-syncs/databricks/databricks-review.png new file mode 100644 index 000000000..e7ff5f6c1 Binary files /dev/null and b/docs/images/secret-syncs/databricks/databricks-review.png differ diff --git a/docs/images/secret-syncs/databricks/databricks-source.png b/docs/images/secret-syncs/databricks/databricks-source.png new file mode 100644 index 000000000..74e9e3535 Binary files /dev/null and b/docs/images/secret-syncs/databricks/databricks-source.png differ diff --git a/docs/images/secret-syncs/databricks/select-databricks-option.png b/docs/images/secret-syncs/databricks/select-databricks-option.png new file mode 100644 index 000000000..26f2fd4bd Binary files /dev/null and b/docs/images/secret-syncs/databricks/select-databricks-option.png differ diff --git a/docs/images/secret-syncs/gcp-secret-manager/enable-resource-manager-api.png b/docs/images/secret-syncs/gcp-secret-manager/enable-resource-manager-api.png new file mode 100644 index 000000000..a3154e7ec Binary files /dev/null and b/docs/images/secret-syncs/gcp-secret-manager/enable-resource-manager-api.png differ diff --git a/docs/images/secret-syncs/gcp-secret-manager/enable-secret-manager-api.png b/docs/images/secret-syncs/gcp-secret-manager/enable-secret-manager-api.png new file mode 100644 index 000000000..50358699a Binary files /dev/null and b/docs/images/secret-syncs/gcp-secret-manager/enable-secret-manager-api.png differ diff --git a/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-created.png b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-created.png new file mode 100644 index 000000000..502365ff7 Binary files /dev/null and b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-created.png differ diff --git a/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-destination.png b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-destination.png new file mode 100644 index 000000000..c50b6232a Binary files /dev/null and b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-destination.png differ diff --git a/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-details.png b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-details.png new file mode 100644 index 000000000..dbb47371f Binary files /dev/null and b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-details.png differ diff --git a/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-options.png b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-options.png new file mode 100644 index 000000000..d3eddfab9 Binary files /dev/null and b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-options.png differ diff --git a/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-review.png b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-review.png new file mode 100644 index 000000000..4d710b3af Binary files /dev/null and b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-review.png differ diff --git a/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-source.png b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-source.png new file mode 100644 index 000000000..9074d45ac Binary files /dev/null and b/docs/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-source.png differ diff --git a/docs/images/secret-syncs/gcp-secret-manager/select-gcp-secret-manager-option.png b/docs/images/secret-syncs/gcp-secret-manager/select-gcp-secret-manager-option.png new file mode 100644 index 000000000..24e6ff95d Binary files /dev/null and b/docs/images/secret-syncs/gcp-secret-manager/select-gcp-secret-manager-option.png differ diff --git a/docs/images/secret-syncs/general/secret-sync-tab.png b/docs/images/secret-syncs/general/secret-sync-tab.png new file mode 100644 index 000000000..dad8c2426 Binary files /dev/null and b/docs/images/secret-syncs/general/secret-sync-tab.png differ diff --git a/docs/images/secret-syncs/github/github-created.png b/docs/images/secret-syncs/github/github-created.png new file mode 100644 index 000000000..f3ab7241b Binary files /dev/null and b/docs/images/secret-syncs/github/github-created.png differ diff --git a/docs/images/secret-syncs/github/github-destination.png b/docs/images/secret-syncs/github/github-destination.png new file mode 100644 index 000000000..3713932bb Binary files /dev/null and b/docs/images/secret-syncs/github/github-destination.png differ diff --git a/docs/images/secret-syncs/github/github-details.png b/docs/images/secret-syncs/github/github-details.png new file mode 100644 index 000000000..a8cbbbb94 Binary files /dev/null and b/docs/images/secret-syncs/github/github-details.png differ diff --git a/docs/images/secret-syncs/github/github-options.png b/docs/images/secret-syncs/github/github-options.png new file mode 100644 index 000000000..8f2e3a4ba Binary files /dev/null and b/docs/images/secret-syncs/github/github-options.png differ diff --git a/docs/images/secret-syncs/github/github-review.png b/docs/images/secret-syncs/github/github-review.png new file mode 100644 index 000000000..4cbed76b6 Binary files /dev/null and b/docs/images/secret-syncs/github/github-review.png differ diff --git a/docs/images/secret-syncs/github/github-source.png b/docs/images/secret-syncs/github/github-source.png new file mode 100644 index 000000000..dd4bc3ec8 Binary files /dev/null and b/docs/images/secret-syncs/github/github-source.png differ diff --git a/docs/images/secret-syncs/github/select-github-option.png b/docs/images/secret-syncs/github/select-github-option.png new file mode 100644 index 000000000..ebee947b4 Binary files /dev/null and b/docs/images/secret-syncs/github/select-github-option.png differ diff --git a/docs/images/secret-syncs/humanitec/humanitec-created.png b/docs/images/secret-syncs/humanitec/humanitec-created.png new file mode 100644 index 000000000..19e5d503a Binary files /dev/null and b/docs/images/secret-syncs/humanitec/humanitec-created.png differ diff --git a/docs/images/secret-syncs/humanitec/humanitec-destination.png b/docs/images/secret-syncs/humanitec/humanitec-destination.png new file mode 100644 index 000000000..81605e9e2 Binary files /dev/null and b/docs/images/secret-syncs/humanitec/humanitec-destination.png differ diff --git a/docs/images/secret-syncs/humanitec/humanitec-details.png b/docs/images/secret-syncs/humanitec/humanitec-details.png new file mode 100644 index 000000000..172fde3f7 Binary files /dev/null and b/docs/images/secret-syncs/humanitec/humanitec-details.png differ diff --git a/docs/images/secret-syncs/humanitec/humanitec-options.png b/docs/images/secret-syncs/humanitec/humanitec-options.png new file mode 100644 index 000000000..bc907bbfc Binary files /dev/null and b/docs/images/secret-syncs/humanitec/humanitec-options.png differ diff --git a/docs/images/secret-syncs/humanitec/humanitec-review.png b/docs/images/secret-syncs/humanitec/humanitec-review.png new file mode 100644 index 000000000..2ffda9240 Binary files /dev/null and b/docs/images/secret-syncs/humanitec/humanitec-review.png differ diff --git a/docs/images/secret-syncs/humanitec/humanitec-source.png b/docs/images/secret-syncs/humanitec/humanitec-source.png new file mode 100644 index 000000000..ff50c11d8 Binary files /dev/null and b/docs/images/secret-syncs/humanitec/humanitec-source.png differ diff --git a/docs/images/secret-syncs/humanitec/select-humanitec-option.png b/docs/images/secret-syncs/humanitec/select-humanitec-option.png new file mode 100644 index 000000000..bb0cb9aed Binary files /dev/null and b/docs/images/secret-syncs/humanitec/select-humanitec-option.png differ diff --git a/docs/images/secret-syncs/terraform-cloud/terraform-cloud-created.png b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-created.png new file mode 100644 index 000000000..d6a89609e Binary files /dev/null and b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-created.png differ diff --git a/docs/images/secret-syncs/terraform-cloud/terraform-cloud-destination.png b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-destination.png new file mode 100644 index 000000000..bb2e2f095 Binary files /dev/null and b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-destination.png differ diff --git a/docs/images/secret-syncs/terraform-cloud/terraform-cloud-details.png b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-details.png new file mode 100644 index 000000000..b87c51494 Binary files /dev/null and b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-details.png differ diff --git a/docs/images/secret-syncs/terraform-cloud/terraform-cloud-option.png b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-option.png new file mode 100644 index 000000000..7670bf2c1 Binary files /dev/null and b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-option.png differ diff --git a/docs/images/secret-syncs/terraform-cloud/terraform-cloud-options.png b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-options.png new file mode 100644 index 000000000..def9cf1c0 Binary files /dev/null and b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-options.png differ diff --git a/docs/images/secret-syncs/terraform-cloud/terraform-cloud-review.png b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-review.png new file mode 100644 index 000000000..f7b245771 Binary files /dev/null and b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-review.png differ diff --git a/docs/images/secret-syncs/terraform-cloud/terraform-cloud-source.png b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-source.png new file mode 100644 index 000000000..7a7250f88 Binary files /dev/null and b/docs/images/secret-syncs/terraform-cloud/terraform-cloud-source.png differ diff --git a/docs/images/secret-syncs/vercel/select-vercel-option.png b/docs/images/secret-syncs/vercel/select-vercel-option.png new file mode 100644 index 000000000..b63d33cc7 Binary files /dev/null and b/docs/images/secret-syncs/vercel/select-vercel-option.png differ diff --git a/docs/images/secret-syncs/vercel/vercel-created.png b/docs/images/secret-syncs/vercel/vercel-created.png new file mode 100644 index 000000000..fe955b00f Binary files /dev/null and b/docs/images/secret-syncs/vercel/vercel-created.png differ diff --git a/docs/images/secret-syncs/vercel/vercel-destination.png b/docs/images/secret-syncs/vercel/vercel-destination.png new file mode 100644 index 000000000..4d0f73d35 Binary files /dev/null and b/docs/images/secret-syncs/vercel/vercel-destination.png differ diff --git a/docs/images/secret-syncs/vercel/vercel-details.png b/docs/images/secret-syncs/vercel/vercel-details.png new file mode 100644 index 000000000..4421c426c Binary files /dev/null and b/docs/images/secret-syncs/vercel/vercel-details.png differ diff --git a/docs/images/secret-syncs/vercel/vercel-options.png b/docs/images/secret-syncs/vercel/vercel-options.png new file mode 100644 index 000000000..1d38e7ae6 Binary files /dev/null and b/docs/images/secret-syncs/vercel/vercel-options.png differ diff --git a/docs/images/secret-syncs/vercel/vercel-review.png b/docs/images/secret-syncs/vercel/vercel-review.png new file mode 100644 index 000000000..7a921f4ec Binary files /dev/null and b/docs/images/secret-syncs/vercel/vercel-review.png differ diff --git a/docs/images/secret-syncs/vercel/vercel-source.png b/docs/images/secret-syncs/vercel/vercel-source.png new file mode 100644 index 000000000..efb753aad Binary files /dev/null and b/docs/images/secret-syncs/vercel/vercel-source.png differ diff --git a/docs/images/secret-syncs/windmill/select-windmill-option.png b/docs/images/secret-syncs/windmill/select-windmill-option.png new file mode 100644 index 000000000..0108dd2da Binary files /dev/null and b/docs/images/secret-syncs/windmill/select-windmill-option.png differ diff --git a/docs/images/secret-syncs/windmill/windmill-sync-created.png b/docs/images/secret-syncs/windmill/windmill-sync-created.png new file mode 100644 index 000000000..37abbba49 Binary files /dev/null and b/docs/images/secret-syncs/windmill/windmill-sync-created.png differ diff --git a/docs/images/secret-syncs/windmill/windmill-sync-destination.png b/docs/images/secret-syncs/windmill/windmill-sync-destination.png new file mode 100644 index 000000000..48b54ad79 Binary files /dev/null and b/docs/images/secret-syncs/windmill/windmill-sync-destination.png differ diff --git a/docs/images/secret-syncs/windmill/windmill-sync-details.png b/docs/images/secret-syncs/windmill/windmill-sync-details.png new file mode 100644 index 000000000..9871cc57d Binary files /dev/null and b/docs/images/secret-syncs/windmill/windmill-sync-details.png differ diff --git a/docs/images/secret-syncs/windmill/windmill-sync-options.png b/docs/images/secret-syncs/windmill/windmill-sync-options.png new file mode 100644 index 000000000..37ca24cc4 Binary files /dev/null and b/docs/images/secret-syncs/windmill/windmill-sync-options.png differ diff --git a/docs/images/secret-syncs/windmill/windmill-sync-review.png b/docs/images/secret-syncs/windmill/windmill-sync-review.png new file mode 100644 index 000000000..2a8a35e9c Binary files /dev/null and b/docs/images/secret-syncs/windmill/windmill-sync-review.png differ diff --git a/docs/images/secret-syncs/windmill/windmill-sync-source.png b/docs/images/secret-syncs/windmill/windmill-sync-source.png new file mode 100644 index 000000000..b7d94c048 Binary files /dev/null and b/docs/images/secret-syncs/windmill/windmill-sync-source.png differ diff --git a/docs/images/self-hosting/guides/automated-bootstrapping/identity-instance-admin.png b/docs/images/self-hosting/guides/automated-bootstrapping/identity-instance-admin.png new file mode 100644 index 000000000..8d819e1fb Binary files /dev/null and b/docs/images/self-hosting/guides/automated-bootstrapping/identity-instance-admin.png differ diff --git a/docs/images/self-hosting/reference-architectures/google-cloud-run/cloud-run-container-image.png b/docs/images/self-hosting/reference-architectures/google-cloud-run/cloud-run-container-image.png new file mode 100644 index 000000000..b18fd7713 Binary files /dev/null and b/docs/images/self-hosting/reference-architectures/google-cloud-run/cloud-run-container-image.png differ diff --git a/docs/images/self-hosting/reference-architectures/google-cloud-run/container-env-vars.png b/docs/images/self-hosting/reference-architectures/google-cloud-run/container-env-vars.png new file mode 100644 index 000000000..ee5f9257b Binary files /dev/null and b/docs/images/self-hosting/reference-architectures/google-cloud-run/container-env-vars.png differ diff --git a/docs/images/self-hosting/reference-architectures/google-cloud-run/container-network-configuration.png b/docs/images/self-hosting/reference-architectures/google-cloud-run/container-network-configuration.png new file mode 100644 index 000000000..9a8ae13cb Binary files /dev/null and b/docs/images/self-hosting/reference-architectures/google-cloud-run/container-network-configuration.png differ diff --git a/docs/images/sso/auth0-oidc/org-update-oidc.png b/docs/images/sso/auth0-oidc/org-update-oidc.png index 0b9e96b5b..bd61584a5 100644 Binary files a/docs/images/sso/auth0-oidc/org-update-oidc.png and b/docs/images/sso/auth0-oidc/org-update-oidc.png differ diff --git a/docs/images/sso/auth0-saml/auth0-config-2.png b/docs/images/sso/auth0-saml/auth0-config-2.png new file mode 100644 index 000000000..1fbc68363 Binary files /dev/null and b/docs/images/sso/auth0-saml/auth0-config-2.png differ diff --git a/docs/images/sso/auth0-saml/auth0-config-3.png b/docs/images/sso/auth0-saml/auth0-config-3.png new file mode 100644 index 000000000..f4287e51a Binary files /dev/null and b/docs/images/sso/auth0-saml/auth0-config-3.png differ diff --git a/docs/images/sso/auth0-saml/auth0-config.png b/docs/images/sso/auth0-saml/auth0-config.png new file mode 100644 index 000000000..6fca6247d Binary files /dev/null and b/docs/images/sso/auth0-saml/auth0-config.png differ diff --git a/docs/images/sso/auth0-saml/create-application-2.png b/docs/images/sso/auth0-saml/create-application-2.png new file mode 100644 index 000000000..71990fbef Binary files /dev/null and b/docs/images/sso/auth0-saml/create-application-2.png differ diff --git a/docs/images/sso/auth0-saml/create-application.png b/docs/images/sso/auth0-saml/create-application.png new file mode 100644 index 000000000..c113d2dda Binary files /dev/null and b/docs/images/sso/auth0-saml/create-application.png differ diff --git a/docs/images/sso/auth0-saml/enable-saml.png b/docs/images/sso/auth0-saml/enable-saml.png new file mode 100644 index 000000000..bb084ebac Binary files /dev/null and b/docs/images/sso/auth0-saml/enable-saml.png differ diff --git a/docs/images/sso/auth0-saml/infisical-config.png b/docs/images/sso/auth0-saml/infisical-config.png new file mode 100644 index 000000000..07a7cdc74 Binary files /dev/null and b/docs/images/sso/auth0-saml/infisical-config.png differ diff --git a/docs/images/sso/auth0-saml/init-config.png b/docs/images/sso/auth0-saml/init-config.png new file mode 100644 index 000000000..5ab5a01e5 Binary files /dev/null and b/docs/images/sso/auth0-saml/init-config.png differ diff --git a/docs/images/sso/keycloak-oidc/create-oidc.png b/docs/images/sso/keycloak-oidc/create-oidc.png index 358af1330..bf8aceb05 100644 Binary files a/docs/images/sso/keycloak-oidc/create-oidc.png and b/docs/images/sso/keycloak-oidc/create-oidc.png differ diff --git a/docs/images/sso/keycloak-oidc/group-membership-mapping/create-group-membership-mapper.png b/docs/images/sso/keycloak-oidc/group-membership-mapping/create-group-membership-mapper.png new file mode 100644 index 000000000..33e283891 Binary files /dev/null and b/docs/images/sso/keycloak-oidc/group-membership-mapping/create-group-membership-mapper.png differ diff --git a/docs/images/sso/keycloak-oidc/group-membership-mapping/create-infisical-group.png b/docs/images/sso/keycloak-oidc/group-membership-mapping/create-infisical-group.png new file mode 100644 index 000000000..6315631e5 Binary files /dev/null and b/docs/images/sso/keycloak-oidc/group-membership-mapping/create-infisical-group.png differ diff --git a/docs/images/sso/keycloak-oidc/group-membership-mapping/create-mapper-by-configuration.png b/docs/images/sso/keycloak-oidc/group-membership-mapping/create-mapper-by-configuration.png new file mode 100644 index 000000000..f2d358be1 Binary files /dev/null and b/docs/images/sso/keycloak-oidc/group-membership-mapping/create-mapper-by-configuration.png differ diff --git a/docs/images/sso/keycloak-oidc/group-membership-mapping/enable-group-membership-mapping.png b/docs/images/sso/keycloak-oidc/group-membership-mapping/enable-group-membership-mapping.png new file mode 100644 index 000000000..199a7432a Binary files /dev/null and b/docs/images/sso/keycloak-oidc/group-membership-mapping/enable-group-membership-mapping.png differ diff --git a/docs/images/sso/keycloak-oidc/group-membership-mapping/select-client-scopes.png b/docs/images/sso/keycloak-oidc/group-membership-mapping/select-client-scopes.png new file mode 100644 index 000000000..ddd8b84be Binary files /dev/null and b/docs/images/sso/keycloak-oidc/group-membership-mapping/select-client-scopes.png differ diff --git a/docs/images/sso/keycloak-oidc/group-membership-mapping/select-client.png b/docs/images/sso/keycloak-oidc/group-membership-mapping/select-client.png new file mode 100644 index 000000000..eb910a876 Binary files /dev/null and b/docs/images/sso/keycloak-oidc/group-membership-mapping/select-client.png differ diff --git a/docs/images/sso/keycloak-oidc/group-membership-mapping/select-dedicated-scope.png b/docs/images/sso/keycloak-oidc/group-membership-mapping/select-dedicated-scope.png new file mode 100644 index 000000000..1580a9d61 Binary files /dev/null and b/docs/images/sso/keycloak-oidc/group-membership-mapping/select-dedicated-scope.png differ diff --git a/docs/images/sso/keycloak-oidc/group-membership-mapping/select-group-membership-mapper.png b/docs/images/sso/keycloak-oidc/group-membership-mapping/select-group-membership-mapper.png new file mode 100644 index 000000000..cafae7666 Binary files /dev/null and b/docs/images/sso/keycloak-oidc/group-membership-mapping/select-group-membership-mapper.png differ diff --git a/docs/images/sso/keycloak-oidc/group-membership-mapping/synced-users.png b/docs/images/sso/keycloak-oidc/group-membership-mapping/synced-users.png new file mode 100644 index 000000000..37193e2fe Binary files /dev/null and b/docs/images/sso/keycloak-oidc/group-membership-mapping/synced-users.png differ diff --git a/docs/integrations/app-connections/auth0.mdx b/docs/integrations/app-connections/auth0.mdx new file mode 100644 index 000000000..42e78cb66 --- /dev/null +++ b/docs/integrations/app-connections/auth0.mdx @@ -0,0 +1,101 @@ +--- +title: "Auth0 Connection" +description: "Learn how to configure an Auth0 Connection for Infisical." +--- + +Infisical supports the use of [Client Credentials](https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-credentials-flow) to connect with your Auth0 applications. + +## Configure a Machine-to-Machine Application in Auth0 + + + + Navigate to the **Applications** page in Auth0 via the sidebar and click **Create Application**. + ![Applications Page](/images/app-connections/auth0/auth0-dashboard-applications.png) + + + Give your application a name and select **Machine-to-Machine** for the application type. + + ![Create Machine-to-Machine Application](/images/app-connections/auth0/auth0-select-m2m.png) + + + Depending on your connection use case, authorize your application for the applicable API and grant the relevant permissions. Once done, click **Authorize**. + + + + Select the **Auth0 Management API** option from the dropdown and grant the `update:client_keys` and `read:clients` permission. + ![Secret Rotation Authorization](/images/app-connections/auth0/auth0-secret-rotation-api-selection.png) + + + + + On your application page, select the **Settings** tab and copy the **Domain**, **Client ID** and **Client Secret** for later. + + ![Client Credentials](/images/app-connections/auth0/auth0-client-credentials.png) + + + Next, select the **APIs** tab and copy the **API Identifier**. + ![Application Audience](/images/app-connections/auth0/auth0-audience.png) + + + +## Setup Auth0 Connection in Infisical + + + + 1. Navigate to the App Connections tab on the Organization Settings page. + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + 2. Select the **Auth0 Connection** option. + ![Select Auth0 Connection](/images/app-connections/auth0/select-auth0-connection.png) + + 3. Select the **Client Credentials** method option and provide the details obtained from the previous section and press **Connect to Auth0**. + ![Create Auth0 Connection](/images/app-connections/auth0/client-credentials-create.png) + + 4. Your **Auth0 Connection** is now available for use. + ![Assume Role Auth0 Connection](/images/app-connections/auth0/client_credentials_connection.png) + + + To create a Auth0 Connection, make an API request to the [Create Auth0 + Connection](/api-reference/endpoints/app-connections/auth0/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/auth0 \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-auth0-connection", + "method": "client-credentials", + "credentials": { + "domain": "xxx-xxxxxxxxx.us.auth0.com", + "clientId": "...", + "clientSecret": "...", + "audience": "https://xxx-xxxxxxxxx.us.auth0.com/api/v2/" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-auth0-connection", + "version": 1, + "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "app": "auth0", + "method": "client-credentials", + "credentials": { + "domain": "xxx-xxxxxxxxx.us.auth0.com", + "clientId": "...", + "audience": "https://xxx-xxxxxxxxx.us.auth0.com/api/v2/" + } + } + } + ``` + + diff --git a/docs/integrations/app-connections/aws.mdx b/docs/integrations/app-connections/aws.mdx new file mode 100644 index 000000000..4944b0c34 --- /dev/null +++ b/docs/integrations/app-connections/aws.mdx @@ -0,0 +1,366 @@ +--- +title: "AWS Connection" +description: "Learn how to configure an AWS Connection for Infisical." +--- + +Infisical supports two methods for connecting to AWS. + + + + Infisical will assume the provided role in your AWS account securely, without the need to share any credentials. + + + To connect your self-hosted Infisical instance with AWS, you need to set up an AWS IAM User account that can assume the configured AWS IAM Role. + + If your instance is deployed on AWS, the aws-sdk will automatically retrieve the credentials. Ensure that you assign the provided permission policy to your deployed instance, such as ECS or EC2. + + The following steps are for instances not deployed on AWS: + + + Navigate to [Create IAM User](https://console.aws.amazon.com/iamv2/home#/users/create) in your AWS Console. + + + Attach the following inline permission policy to the IAM User to allow it to assume any IAM Roles: + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowAssumeAnyRole", + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Resource": "arn:aws:iam::*:role/*" + } + ] + } + ``` + + + Obtain the AWS access key ID and secret access key for your IAM User by navigating to **IAM > Users > [Your User] > Security credentials > Access keys**. + + ![Access Key Step 1](/images/integrations/aws/integrations-aws-access-key-1.png) + ![Access Key Step 2](/images/integrations/aws/integrations-aws-access-key-2.png) + ![Access Key Step 3](/images/integrations/aws/integrations-aws-access-key-3.png) + + + 1. Set the access key as **INF_APP_CONNECTION_AWS_ACCESS_KEY_ID**. + 2. Set the secret key as **INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY**. + + + + + + + 1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console. + ![IAM Role Creation](/images/integrations/aws/integration-aws-iam-assume-role.png) + + 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. + 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). + + + When configuring an IAM Role that Infisical will assume, it’s highly recommended to enable the **"Require external ID"** option and specify your **Organization ID**. + + This precaution helps protect your AWS account against the [confused deputy problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html), a potential security vulnerability where Infisical could be tricked into performing actions on your behalf by an unauthorized actor. + + Always enable "Require external ID" and use your Organization ID when setting up the IAM Role. + + + + + Navigate to your IAM role permissions and click **Create Inline Policy**. + + ![IAM Role Create Policy](/images/app-connections/aws/assume-role-create-policy.png) + + Depending on your use case, add one or more of the following policies to your IAM Role: + + + + + + Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Secrets Manager: + + ![IAM Role Secrets Manager Permissions](/images/app-connections/aws/secrets-manager-permissions.png) + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowSecretsManagerAccess", + "Effect": "Allow", + "Action": [ + "secretsmanager:ListSecrets", + "secretsmanager:GetSecretValue", + "secretsmanager:BatchGetSecretValue", + "secretsmanager:CreateSecret", + "secretsmanager:UpdateSecret", + "secretsmanager:DeleteSecret", + "secretsmanager:DescribeSecret", + "secretsmanager:TagResource", + "secretsmanager:UntagResource", + "kms:ListAliases", // if you need to specify the KMS key + "kms:Encrypt", // if you need to specify the KMS key + "kms:Decrypt", // if you need to specify the KMS key + "kms:DescribeKey" // if you need to specify the KMS key + ], + "Resource": "*" + } + ] + } + ``` + If using a custom KMS key, be sure to add the IAM user or role as a key user. ![KMS Key IAM Role User](/images/app-connections/aws/kms-key-user.png) + + + Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Parameter Store: + + ![IAM Role Secrets Manager Permissions](/images/app-connections/aws/parameter-store-permissions.png) + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowSSMAccess", + "Effect": "Allow", + "Action": [ + "ssm:PutParameter", + "ssm:GetParameters", + "ssm:GetParametersByPath", + "ssm:DescribeParameters", + "ssm:DeleteParameters", + "ssm:ListTagsForResource", // if you need to add tags to secrets + "ssm:AddTagsToResource", // if you need to add tags to secrets + "ssm:RemoveTagsFromResource", // if you need to add tags to secrets + "kms:ListAliases", // if you need to specify the KMS key + "kms:Encrypt", // if you need to specify the KMS key + "kms:Decrypt", // if you need to specify the KMS key + "kms:DescribeKey" // if you need to specify the KMS key + ], + "Resource": "*" + } + ] + } + ``` + If using a custom KMS key, be sure to add the IAM user or role as a key user. ![KMS Key IAM Role User](/images/app-connections/aws/kms-key-user.png) + + + + + + + + ![Copy IAM Role ARN](/images/integrations/aws/integration-aws-iam-assume-arn.png) + + + + + + 1. Navigate to the App Connections tab on the Organization Settings page. + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + 2. Select the **AWS Connection** option. + ![Select AWS Connection](/images/app-connections/aws/select-aws-connection.png) + + 3. Select the **Assume Role** method option and provide the **AWS IAM Role ARN** obtained from the previous step and press **Connect to AWS**. + ![Create AWS Connection](/images/app-connections/aws/create-assume-role-method.png) + + 4. Your **AWS Connection** is now available for use. + ![Assume Role AWS Connection](/images/app-connections/aws/assume-role-connection.png) + + + To create an AWS Connection, make an API request to the [Create AWS + Connection](/api-reference/endpoints/app-connections/aws/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/aws \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-aws-connection", + "method": "assume-role", + "credentials": { + "roleArn": "...", + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-aws-connection", + "version": 123, + "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "app": "aws", + "method": "assume-role", + "credentials": {} + } + } + ``` + + + + + + + + Infisical will use the provided **Access Key ID** and **Secret Key** to connect to your AWS instance. + + + + Navigate to your IAM user permissions and click **Create Inline Policy**. + + ![User IAM Create Policy](/images/app-connections/aws/access-key-create-policy.png) + + Depending on your use case, add one or more of the following policies to your user: + + + + + + Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Secrets Manager: + + ![IAM Role Secrets Manager Permissions](/images/app-connections/aws/secrets-manager-permissions.png) + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowSecretsManagerAccess", + "Effect": "Allow", + "Action": [ + "secretsmanager:ListSecrets", + "secretsmanager:GetSecretValue", + "secretsmanager:BatchGetSecretValue", + "secretsmanager:CreateSecret", + "secretsmanager:UpdateSecret", + "secretsmanager:DeleteSecret", + "secretsmanager:DescribeSecret", + "secretsmanager:TagResource", + "secretsmanager:UntagResource", + "kms:ListAliases", // if you need to specify the KMS key + "kms:Encrypt", // if you need to specify the KMS key + "kms:Decrypt", // if you need to specify the KMS key + "kms:DescribeKey" // if you need to specify the KMS key + ], + "Resource": "*" + } + ] + } + ``` + If using a custom KMS key, be sure to add the IAM user or role as a key user. ![KMS Key IAM Role User](/images/app-connections/aws/kms-key-user.png) + + + Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Parameter Store: + + ![IAM Role Secrets Manager Permissions](/images/app-connections/aws/parameter-store-permissions.png) + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowSSMAccess", + "Effect": "Allow", + "Action": [ + "ssm:PutParameter", + "ssm:GetParameters", + "ssm:GetParametersByPath", + "ssm:DescribeParameters", + "ssm:DeleteParameters", + "ssm:ListTagsForResource", // if you need to add tags to secrets + "ssm:AddTagsToResource", // if you need to add tags to secrets + "ssm:RemoveTagsFromResource", // if you need to add tags to secrets + "kms:ListAliases", // if you need to specify the KMS key + "kms:Encrypt", // if you need to specify the KMS key + "kms:Decrypt", // if you need to specify the KMS key + "kms:DescribeKey" // if you need to specify the KMS key + ], + "Resource": "*" + } + ] + } + ``` + If using a custom KMS key, be sure to add the IAM user or role as a key user. ![KMS Key IAM Role User](/images/app-connections/aws/kms-key-user.png) + + + + + + + Retrieve an AWS **Access Key ID** and a **Secret Key** for your IAM user in **IAM > Users > User > Security credentials > Access keys**. + + ![access key 1](/images/integrations/aws/integrations-aws-access-key-1.png) + ![access key 2](/images/integrations/aws/integrations-aws-access-key-2.png) + ![access key 3](/images/integrations/aws/integrations-aws-access-key-3.png) + + + + + 1. Navigate to the App Connections tab on the Organization Settings page. + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + 2. Select the **AWS Connection** option. + ![Select AWS Connection](/images/app-connections/aws/select-aws-connection.png) + + 3. Select the **Access Key** method option and provide the **Access Key ID** and **Secret Key** obtained from the previous step and press **Connect to AWS**. + ![Create AWS Connection](/images/app-connections/aws/create-access-key-method.png) + + 4. Your **AWS Connection** is now available for use. + ![Assume Role AWS Connection](/images/app-connections/aws/access-key-connection.png) + + + To create an AWS Connection, make an API request to the [Create AWS + Connection](/api-reference/endpoints/app-connections/aws/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/aws \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-aws-connection", + "method": "access-key", + "credentials": { + "accessKeyId": "...", + "secretKey": "..." + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-aws-connection", + "version": 123, + "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "app": "aws", + "method": "access-key", + "credentials": { + "accessKeyId": "..." + } + } + } + ``` + + + + + + + + diff --git a/docs/integrations/app-connections/azure-app-configuration.mdx b/docs/integrations/app-connections/azure-app-configuration.mdx new file mode 100644 index 000000000..959a1812a --- /dev/null +++ b/docs/integrations/app-connections/azure-app-configuration.mdx @@ -0,0 +1,90 @@ +--- +title: "Azure App Configuration Connection" +description: "Learn how to configure a Azure App Configuration Connection for Infisical." +--- + +Infisical currently only supports one method for connecting to Azure, which is OAuth. + + + Using the Azure App Configuration connection on a self-hosted instance of Infisical requires configuring an application in Azure + and registering your instance with it. + + **Prerequisites:** + + - Set up Azure and have an existing App Configuration instance. + + + + Navigate to Azure Active Directory > App registrations to create a new application. + + + Azure Active Directory is now Microsoft Entra ID. + + ![Azure app config](/images/integrations/azure-app-configuration/config-aad.png) + ![Azure app config](/images/integrations/azure-app-configuration/config-new-app.png) + + Create the application. As part of the form, set the **Redirect URI** to `https://your-domain.com/organization/app-connections/azure/oauth/callback`. + + The domain you defined in the Redirect URI should be equivalent to the `SITE_URL` configured in your Infisical instance. + + + ![Azure app config](/images/app-connections/azure/register-callback.png) + + + + For the Azure Connection to work with App Configuration, you need to assign multiple permissions to the application. + + #### Azure App Configuration permissions + + Set the API permissions of the Azure application to include the following Azure App Configuration permissions: `KeyValue.Delete`, `KeyValue.Read`, and `KeyValue.Write`. + ![Azure app config](../../images/integrations/azure-app-configuration/app-api-permissions.png) + + + + + Obtain the **Application (Client) ID** in Overview and generate a **Client Secret** in Certificate & secrets for your Azure application. + + ![Azure app config](../../images/integrations/azure-app-configuration/config-credentials-1.png) + ![Azure app config](../../images/integrations/azure-app-configuration/config-credentials-2.png) + ![Azure app config](../../images/integrations/azure-app-configuration/config-credentials-3.png) + + Back in your Infisical instance, add two new environment variables for the credentials of your Azure application. + + - `INF_APP_CONNECTION_AZURE_CLIENT_ID`: The **Application (Client) ID** of your Azure application. + - `INF_APP_CONNECTION_AZURE_CLIENT_SECRET`: The **Client Secret** of your Azure application. + + Once added, restart your Infisical instance and use the Azure App Configuration connection. + + + + + +## Setup Azure Connection in Infisical + + + + Navigate to the **App Connections** tab on the **Organization Settings** page. ![App Connections + Tab](/images/app-connections/general/add-connection.png) + + + Select the **Azure Connection** option from the connection options modal. ![Select Azure Connection](/images/app-connections/azure/app-configuration/select-connection.png) + + + You can optionally authenticate against a specific tenant by providing the Azure Tenant or Directory ID. + + Now select the **OAuth** method and click **Connect to Azure**. + + ![Connect via Azure OAUth](/images/app-connections/azure/app-configuration/create-oauth-method.png) + + + + + + You will then be redirected to Azure to grant Infisical access to your Azure account. Once granted, + you will redirect you back to Infisical's App Connections page. ![Azure App Configuration + Authorization](/images/app-connections/azure/grant-access.png) + + + Your **Azure App Configuration Connection** is now available for use. ![Assume Role AWS Connection](/images/app-connections/azure/app-configuration/oauth-connection.png) + + diff --git a/docs/integrations/app-connections/azure-key-vault.mdx b/docs/integrations/app-connections/azure-key-vault.mdx new file mode 100644 index 000000000..f73dab834 --- /dev/null +++ b/docs/integrations/app-connections/azure-key-vault.mdx @@ -0,0 +1,89 @@ +--- +title: "Azure Key Vault Connection" +description: "Learn how to configure a Azure Key Vault Connection for Infisical." +--- + +Infisical currently only supports one method for connecting to Azure, which is OAuth. + + + Using the Azure Key Vault connection on a self-hosted instance of Infisical requires configuring an application in Azure + and registering your instance with it. + + **Prerequisites:** + + - Set up Azure and have an existing Key Vault instance. + + + + Navigate to Azure Active Directory > App registrations to create a new application. + + + Azure Active Directory is now Microsoft Entra ID. + + ![Azure key vault](/images/integrations/azure-app-configuration/config-aad.png) + ![Azure key vault](/images/integrations/azure-app-configuration/config-new-app.png) + + Create the application. As part of the form, set the **Redirect URI** to `https://your-domain.com/organization/app-connections/azure/oauth/callback`. + + The domain you defined in the Redirect URI should be equivalent to the `SITE_URL` configured in your Infisical instance. + + + ![Azure key vault](/images/app-connections/azure/register-callback.png) + + + + For the Azure Connection to work with Key Vault, you need to assign multiple permissions to the application. + + #### Azure Key Vault permissions + + Set the API permissions of the Azure application to include `user.impersonation` for the Key Vault API. + ![Azure key vault](/images/app-connections/azure/keyvault-azure-permissions.png) + + + + Obtain the **Application (Client) ID** in Overview and generate a **Client Secret** in Certificate & secrets for your Azure application. + + ![Azure key vault](../../images/integrations/azure-app-configuration/config-credentials-1.png) + ![Azure key vault](../../images/integrations/azure-app-configuration/config-credentials-2.png) + ![Azure key vault](../../images/integrations/azure-app-configuration/config-credentials-3.png) + + Back in your Infisical instance, add two new environment variables for the credentials of your Azure application. + + - `INF_APP_CONNECTION_AZURE_CLIENT_ID`: The **Application (Client) ID** of your Azure application. + - `INF_APP_CONNECTION_AZURE_CLIENT_SECRET`: The **Client Secret** of your Azure application. + + Once added, restart your Infisical instance and use the Azure Key Vault connection. + + + + + +## Setup Azure Connection in Infisical + + + + Navigate to the **App Connections** tab on the **Organization Settings** page. ![App Connections + Tab](/images/app-connections/general/add-connection.png) + + + Select the **Azure Connection** option from the connection options modal. ![Select Azure Connection](/images/app-connections/azure/key-vault/select-connection.png) + + + You can optionally authenticate against a specific tenant by providing the Azure Tenant or Directory ID. + + Now select the **OAuth** method and click **Connect to Azure**. + + ![Connect via Azure OAUth](/images/app-connections/azure/key-vault/create-oauth-method.png) + + + + + + You will then be redirected to Azure to grant Infisical access to your Azure account. Once granted, + you will redirect you back to Infisical's App Connections page. ![Azure Key Vault + Authorization](/images/app-connections/azure/grant-access.png) + + + Your **Azure Key Vault Connection** is now available for use. ![Assume Role AWS Connection](/images/app-connections/azure/key-vault/oauth-connection.png) + + diff --git a/docs/integrations/app-connections/camunda.mdx b/docs/integrations/app-connections/camunda.mdx new file mode 100644 index 000000000..68084cea3 --- /dev/null +++ b/docs/integrations/app-connections/camunda.mdx @@ -0,0 +1,77 @@ +--- +title: "Camunda Connection" +description: "Learn how to configure a Camunda Connection for Infisical." +--- + +Infisical supports connecting to Camunda APIs using [client credentials](https://docs.camunda.io/docs/apis-tools/administration-api/authentication/#client-credentials-and-scopes). + +## Configure Client Credentials for Infisical + + + + In your Camunda Cloud Console, navigate to the **Organization** tab in the top navigation menu. + ![Organization Management](/images/app-connections/camunda/camunda-console.png) + + + From the Organization Management tabs, click on **Administration API** to manage your API credentials and click the **Create client credentials** button. + ![Create Client Credentials](/images/app-connections/camunda/camunda-organization-page.png) + + + Enter a recognizable name for your client, such as "my-infisical-client". The name can contain letters, dashes, underscores, and digits. + + + In the "Create new client credentials" modal, select the following permissions required for secret syncs: + + - **Cluster**: Enable read access (Get) + - **Connector secrets**: Enable all operations (Get, Create, Update, Delete) + + These specific permissions are required for Infisical to properly sync and manage your Camunda secrets. + ![Set Permissions](/images/app-connections/camunda/camunda-create-client-1.png) + ![Set Permissions 2](/images/app-connections/camunda/camunda-create-client-2.png) + + + Click the **Create** button to generate your client credentials. + + + After creation, you'll be shown your client credentials. For the Infisical connection, you'll need: + + - **Client ID** (`CAMUNDA_CONSOLE_CLIENT_ID`) + - **Client Secret** (`CAMUNDA_CONSOLE_CLIENT_SECRET`) + + **IMPORTANT**: Make sure to securely save the Client Secret, as it will not be shown again after you close this dialog. + + You can download these credentials or copy them to use in the next section. + ![Client Credentials](/images/app-connections/camunda/camunda-client-credentials.png) + + + + +## Setup Camunda Connection in Infisical + + + + Navigate to the **App Connections** tab on the **Organization Settings** + page. ![App Connections + Tab](/images/app-connections/general/add-connection.png) + + + Select the **Camunda Connection** option from the connection options modal. + ![Select Camunda + Connection](/images/app-connections/camunda/camunda-app-connection-select.png) + + + Select the **Client Credentials** method and enter the Camunda client + credentials you created: + + - **Client ID**: Your `CAMUNDA_CONSOLE_CLIENT_ID` value + - **Client Secret**: Your `CAMUNDA_CONSOLE_CLIENT_SECRET` value + + Infisical will automatically configure the connection using these credentials to access the Camunda API. Click **Connect to Camunda** to establish the connection. ![Connect to Camunda](/images/app-connections/camunda/camunda-app-connection-form.png) + + + + Your **Camunda Connection** is now available for use in your Infisical + projects. ![Camunda Connection + Created](/images/app-connections/camunda/camunda-app-connection-created.png) + + diff --git a/docs/integrations/app-connections/databricks.mdx b/docs/integrations/app-connections/databricks.mdx new file mode 100644 index 000000000..38d125ea6 --- /dev/null +++ b/docs/integrations/app-connections/databricks.mdx @@ -0,0 +1,64 @@ +--- +title: "Databricks Connection" +description: "Learn how to configure a Databricks Connection for Infisical." +--- + +Infisical supports the use of [service principals](https://docs.databricks.com/en/admin/users-groups/service-principals.html) to connect with your Databricks workspaces. + +## Configure a Service Principal for Infisical + + + + Navigate to your Databricks Workspace **Settings** via the dropdown in the top right. + ![Workspace Settings Page](/images/app-connections/databricks/workspace-settings.png) + + + Under the **Identity & Access** tab, click the **Manage** button in the **Service Principals** section. + + ![Manage Service Principals](/images/app-connections/databricks/manage-service-principals.png) + + + Click the **Add Service Principal** button. + + ![Add Service Principal](/images/app-connections/databricks/add-service-principal.png) + + + Select the **Add New** option and create a service principal for Infisical. + + ![Create Service Principal](/images/app-connections/databricks/create-service-principal.png) + + + Click on your new service principal, select the **Secrets** tab and click the **Generate Secret** button. + + ![Generate Secret](/images/app-connections/databricks/service-principal-secrets.png) + + + Copy your service principal **Secret** and **Client ID** for use in the following steps. + + ![Generate Secret](/images/app-connections/databricks/service-principal-ids.png) + + + +## Setup Databricks Connection in Infisical + + + + Navigate to the **App Connections** tab on the **Organization Settings** + page. ![App Connections + Tab](/images/app-connections/general/add-connection.png) + + + Select the **Databricks Connection** option from the connection options modal. + ![Select Databricks + Connection](/images/app-connections/databricks/select-databricks-connection.png) + + + Select the **Service Principal** method, add your **workspace URL** and **service principal credentials**, then click **Connect to + Databricks**. ![Connect via Databricks + service principal](/images/app-connections/databricks/create-databricks-service-principal-method.png) + + + Your **Databricks Connection** is now available for use. ![Databricks Service Principal + Connection](/images/app-connections/databricks/databricks-service-principal-connection.png) + + diff --git a/docs/integrations/app-connections/gcp.mdx b/docs/integrations/app-connections/gcp.mdx new file mode 100644 index 000000000..129c26c2a --- /dev/null +++ b/docs/integrations/app-connections/gcp.mdx @@ -0,0 +1,103 @@ +--- +title: "GCP Connection" +description: "Learn how to configure a GCP Connection for Infisical." +--- + +Infisical supports [service account impersonation](https://cloud.google.com/iam/docs/service-account-impersonation) to connect with your GCP projects. + + + Using the GCP integration on a self-hosted instance of Infisical requires configuring a service account on GCP and + configuring your instance to use it. + + + + ![Service Account API](/images/app-connections/gcp/service-account-credentials-api.png) + + + ![Service Account IAM Page](/images/app-connections/gcp/service-account-overview.png) + + + Create a new service account that will be used to impersonate other GCP service accounts for your app connections. + ![Create Service Account Page](/images/app-connections/gcp/create-instance-service-account.png) + + Press "DONE" after creating the service account. + + + Download the JSON key file for your service account. This will be used to authenticate your instance with GCP. + ![Service Account Credential Page](/images/app-connections/gcp/create-service-account-credential.png) + + + 1. Copy the entire contents of the downloaded JSON key file. + 2. Set it as a string value for the `INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL` environment variable. + 3. Restart your Infisical instance to apply the changes. + 4. You can now use GCP integration with service account impersonation. + + + + + +## Configure Service Account for Infisical + + + + ![Service Account Page](/images/app-connections/gcp/service-account-overview.png) + + + Create a new service account with an ID that follows this requirement: + + Your service account ID must end with the first two sections of your Infisical organization ID. + + Example: + - Infisical organization ID: `df92581a-0fe9-42b5-b526-0a1e88ec8085` + - Required service account ID suffix: `df92581a-0fe9` + + ![Create Service Account](/images/app-connections/gcp/create-service-account.png) + + + + + Add the required permissions for secret syncs: + ![Assign Service Account Permission](/images/app-connections/gcp/service-account-secret-sync-permission.png) + + + After configuring the appropriate roles, press "DONE". + + + To enable service account impersonation, you'll need to grant the **Service Account Token Creator** role to the Infisical instance's service account. This configuration allows Infisical to securely impersonate the new service account. + - Navigate to the IAM & Admin > Service Accounts section in your Google Cloud Console + - Select the newly created service account + - Click on the "PERMISSIONS" tab + - Click "Grant Access" to add a new principal + + If you're using Infisical Cloud US, use the following service account: infisical-us@infisical-us.iam.gserviceaccount.com + + If you're using Infisical Cloud EU, use the following service account: infisical-eu@infisical-eu.iam.gserviceaccount.com + + ![Service Account Page](/images/app-connections/gcp/service-account-grant-access.png) + + + + +## Setup GCP Connection in Infisical + + + + Navigate to the **App Connections** tab on the **Organization Settings** + page. ![App Connections + Tab](/images/app-connections/general/add-connection.png) + + + Select the **GCP Connection** option from the connection options modal. + ![Select GCP + Connection](/images/app-connections/gcp/select-gcp-connection.png) + + + Select the **Service Account Impersonation** method and click **Connect to + GCP**. ![Connect via GCP + impersonation](/images/app-connections/gcp/create-gcp-impersonation-method.png) + + + Your **GCP Connection** is now available for use. ![Impersonation GCP + Connection](/images/app-connections/gcp/gcp-app-impersonation-connection.png) + + diff --git a/docs/integrations/app-connections/github.mdx b/docs/integrations/app-connections/github.mdx new file mode 100644 index 000000000..2f8caffed --- /dev/null +++ b/docs/integrations/app-connections/github.mdx @@ -0,0 +1,161 @@ +--- +title: "GitHub Connection" +description: "Learn how to configure a GitHub Connection for Infisical." +--- + +Infisical supports two methods for connecting to GitHub. + + + + Infisical will use a GitHub App with finely grained permissions to connect to GitHub. + + + Using the GitHub integration with app authentication on a self-hosted instance of Infisical requires configuring an application on GitHub + and registering your instance with it. + + + + Navigate to the GitHub app settings [here](https://github.com/settings/apps). Click **New GitHub App**. + + ![integrations github app create](/images/integrations/github/app/self-hosted-github-app-create.png) + + Give the application a name, a homepage URL (your self-hosted domain i.e. `https://your-domain.com`), and a callback URL (i.e. `https://your-domain.com/organization/app-connections/github/oauth/callback`). + + ![integrations github app basic details](/images/integrations/github/app/self-hosted-github-app-basic-details.png) + + Enable request user authorization during app installation. + ![integrations github app enable auth](/images/integrations/github/app/self-hosted-github-app-enable-oauth.png) + + Disable webhook by unchecking the Active checkbox. + ![integrations github app webhook](/images/integrations/github/app/self-hosted-github-app-webhook.png) + + Set the repository permissions as follows: Metadata: Read-only, Secrets: Read and write, Environments: Read and write, Actions: Read. + ![integrations github app repository](/images/integrations/github/app/self-hosted-github-app-repository.png) + + Similarly, set the organization permissions as follows: Secrets: Read and write. + ![integrations github app organization](/images/integrations/github/app/self-hosted-github-app-organization.png) + + Create the Github application. + ![integrations github app create confirm](/images/integrations/github/app/self-hosted-github-app-create-confirm.png) + + + If you have a GitHub organization, you can create an application under it + in your organization Settings > Developer settings > GitHub Apps > New GitHub App. + + + + Generate a new **Client Secret** for your GitHub application. + ![integrations github app create secret](/images/integrations/github/app/self-hosted-github-app-secret.png) + + Generate a new **Private Key** for your Github application. + ![integrations github app create private key](/images/integrations/github/app/self-hosted-github-app-private-key.png) + + Obtain the necessary Github application credentials. This would be the application slug, client ID, app ID, client secret, and private key. + ![integrations github app credentials](/images/integrations/github/app/self-hosted-github-app-credentials.png) + + Back in your Infisical instance, add the five new environment variables for the credentials of your GitHub application: + + - `INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID`: The **Client ID** of your GitHub application. + - `INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET`: The **Client Secret** of your GitHub application. + - `INF_APP_CONNECTION_GITHUB_APP_SLUG`: The **Slug** of your GitHub application. This is the one found in the URL. + - `INF_APP_CONNECTION_GITHUB_APP_ID`: The **App ID** of your GitHub application. + - `INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY`: The **Private Key** of your GitHub application. + + Once added, restart your Infisical instance and use the GitHub integration via app authentication. + + + + + ## Setup GitHub Connection in Infisical + + + + Navigate to the **App Connections** tab on the **Organization Settings** page. + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + + Select the **GitHub Connection** option from the connection options modal. + ![Select GitHub Connection](/images/app-connections/github/select-github-connection.png) + + + Select the **GitHub App** method and click **Connect to GitHub**. + ![Connect via GitHub App](/images/app-connections/github/create-github-app-method.png) + + + You will then be redirected to the GitHub app installation page. + + Install and authorize the GitHub application. This will redirect you back to Infisical's App Connections page. + ![Install GitHub App](/images/app-connections/github/install-github-app.png) + + + Your **GitHub Connection** is now available for use. + ![Assume Role AWS Connection](/images/app-connections/github/github-app-connection.png) + + + + + Infisical will use an OAuth App to connect to GitHub. + + + Using the GitHub integration on a self-hosted instance of Infisical requires configuring an OAuth application in GitHub + and registering your instance with it. + + + Navigate to your user Settings > Developer settings > OAuth Apps to create a new GitHub OAuth application. + + ![integrations github config](../../images/integrations/github/integrations-github-config-settings.png) + ![integrations github config](../../images/integrations/github/integrations-github-config-dev-settings.png) + ![integrations github config](../../images/integrations/github/integrations-github-config-new-app.png) + + Create the OAuth application. As part of the form, set the **Homepage URL** to your self-hosted domain `https://your-domain.com` + and the **Authorization callback URL** to `https://your-domain.com/organization/app-connections/github/oauth/callback`. + + ![integrations github config](../../images/integrations/github/integrations-github-config-new-app-form.png) + + + If you have a GitHub organization, you can create an OAuth application under it + in your organization Settings > Developer settings > OAuth Apps > New Org OAuth App. + + + + Obtain the **Client ID** and generate a new **Client Secret** for your GitHub OAuth application. + + ![integrations github config](../../images/integrations/github/integrations-github-config-credentials.png) + + Back in your Infisical instance, add two new environment variables for the credentials of your GitHub OAuth application: + + - `INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID`: The **Client ID** of your GitHub OAuth application. + - `INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET`: The **Client Secret** of your GitHub OAuth application. + + Once added, restart your Infisical instance and use the GitHub integration. + + + + + ## Setup GitHub Connection in Infisical + + + + Navigate to the **App Connections** tab on the **Organization Settings** page. + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + + Select the **GitHub Connection** option from the connection options modal. + ![Select GitHub Connection](/images/app-connections/github/select-github-connection.png) + + + Select the **OAuth** method and click **Connect to GitHub**. + ![Connect via GitHub App](/images/app-connections/github/create-oauth-method.png) + + + You will then be redirected to the GitHub to grant Infisical access to your GitHub account (organization and repo privileges). + Once granted, you will redirect you back to Infisical's App Connections page. + ![GitHub Authorization](/images/integrations/github/integrations-github-auth.png) + + + Your **GitHub Connection** is now available for use. + ![Assume Role AWS Connection](/images/app-connections/github/oauth-connection.png) + + + + diff --git a/docs/integrations/app-connections/humanitec.mdx b/docs/integrations/app-connections/humanitec.mdx new file mode 100644 index 000000000..570d3ba5d --- /dev/null +++ b/docs/integrations/app-connections/humanitec.mdx @@ -0,0 +1,71 @@ +--- +title: "Humanitec Connection" +description: "Learn how to configure a Humanitec Connection for Infisical." +--- + +Infisical supports connecting to Humanitec using a service user. + +## Setup Humanitec Connection in Infisical + + + + Navigate to the Humanitec **Service Users** tab. + ![Humanitec Service Users Tab](/images/app-connections/humanitec/humanitec-service-users.png) + + + Create a new service user. Take into account that the role set here will affect the permissions of the API Token so be sure to set it so the Service User has access permissions to the App you want to integrate to Infisical. + ![Humanitec Create New Service User](/images/app-connections/humanitec/humanitec-create-new-user.png) + + + Add a new API token for the service user. + ![Humanitec Add API Token](/images/app-connections/humanitec/humanitec-add-api-token.png) + + + Create the API token for the service user. + This token's permission will be limited to the **Service User** role. + + If you configure an expiry date for your API token you will need to manually rotate to a new token prior to expiration to avoid integration downtime. + + ![Humanitec Create API Token](/images/app-connections/humanitec/humanitec-create-api-token.png) + + + A modal with the API token will be displayed. Save the token in a secure location for later use in the following steps. + ![Humanitec Copy API Token](/images/app-connections/humanitec/humanitec-copy-api-token.png) + + + After following the previous steps the Service User has been successfully created, and now should be visible on the Service Users tab. + ![Humanitec Service User Created](/images/app-connections/humanitec/humanitec-service-account-filled.png) + + + Move to the **Applications** tab and add the Service User to the Application you want to sync with Infisical. + Clicking on the App Title will open the App details page. + ![Humanitec Applications Tab](/images/app-connections/humanitec/humanitec-applications-tab.png) + + + Move to the **People** tab and add a new member to this Application. The recently created User Service should be visible on the dropdown shown. + Make sure to assign at least Developer role as Write permissions are required. + ![Humanitec Add User to Application](/images/app-connections/humanitec/humanitec-add-user.png) + ![Humanitec Add User Options](/images/app-connections/humanitec/humanitec-add-user-options.png) + ![Humanitec Add User Role](/images/app-connections/humanitec/humanitec-add-user-role.png) + + + Your **Humanitec Connection** is now available for use. + ![Humanitec Connection Created](/images/app-connections/humanitec/humanitec-user-added.png) + + + Navigate to the **App Connections** tab on the **Organization Settings** page. + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + + Select the **Humanitec Connection** option from the connection options modal. + ![Select Humanitec Connection](/images/app-connections/humanitec/humanitec-app-connection-option.png) + + + Fill the Humanitec Connection modal, here you will need to provide the User Service API Token generated in the previous step. + ![Humanitec Connection Modal](/images/app-connections/humanitec/humanitec-app-connection-modal.png) + + + Your **Humanitec Connection** is now available for use. + ![Humanitec Connection Created](/images/app-connections/humanitec/humanitec-app-connection-created.png) + + diff --git a/docs/integrations/app-connections/mssql.mdx b/docs/integrations/app-connections/mssql.mdx new file mode 100644 index 000000000..7e940804d --- /dev/null +++ b/docs/integrations/app-connections/mssql.mdx @@ -0,0 +1,138 @@ +--- +title: "Microsoft SQL Server Connection" +description: "Learn how to configure a Microsoft SQL Server Connection for Infisical." +--- + +Infisical supports connecting to Microsoft SQL Server using database principals. + +## Configure a Microsoft SQL Server Principal for Infisical + + + + Infisical recommends creating a designated server login and database user in your Microsoft SQL Server database for your connection. + ```SQL + -- Create login at the server level + CREATE LOGIN [infisical_app] WITH PASSWORD = 'my-password'; + + -- Grant server-level connect permission + GRANT CONNECT SQL TO [infisical_app]; + + -- If you intend to use Platform Managed Credentials (see below) + GRANT ALTER ANY LOGIN TO [infisical_app]; + + -- Switch to the specific database where you want to create the user + USE my_database; + + -- Create the database user mapped to the login + CREATE USER [infisical_app] FOR LOGIN [infisical_app]; + ``` + + + Depending on how you intend to use your Microsoft SQL Server connection, you'll need to grant one or more of the following permissions. + + + To learn more about Microsoft SQL Server's permission system, please visit their [documentation](https://learn.microsoft.com/en-us/sql/t-sql/statements/grant-transact-sql?view=sql-server-ver16). + + + + + For Secret Rotations, your Infisical user will require the ability to alter other logins' passwords: + ```SQL + GRANT ALTER ANY LOGIN TO infisical_login; + ``` + + + + + You'll need the following information to create your Microsoft SQL Server connection: + - `host` - The hostname or IP address of your Microsoft SQL Server server + - `port` - The port number your Microsoft SQL Server server is listening on (default: 1433) + - `database` - The name of the specific database you want to connect to + - `username` - The username of the login created in the steps above + - `password` - The password of the login created in the steps above + - `sslCertificate` (optional) - The SSL certificate required for connection (if configured) + + + If you are self-hosting Infisical and intend to connect to an internal/private IP address, be sure to set the `ALLOW_INTERNAL_IP_CONNECTIONS` environment variable to `true`. + + + + +## Create Connection in Infisical + + + + 1. Navigate to the App Connections tab on the Organization Settings page. + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + 2. Select the **Microsoft SQL Server Connection** option. + ![Select Microsoft SQL Server Connection](/images/app-connections/mssql/select-mssql-connection.png) + + 3. Select the **Username & Password** method option and provide the details obtained from the previous section and press **Connect to Microsoft SQL Server**. + + Optionally, if you'd like Infisical to manage the credentials of this connection, you can enable the Platform Managed Credentials option. + If enabled, Infisical will update the password of the connection on creation to prevent external access to this database role. + + ![Create Microsoft SQL Server Connection](/images/app-connections/mssql/create-username-and-password-method.png) + + 4. Your **Microsoft SQL Server Connection** is now available for use. + ![Assume Role Microsoft SQL Server Connection](/images/app-connections/mssql/username-and-password-connection.png) + + + To create a Microsoft SQL Server Connection, make an API request to the [Create Microsoft SQL Server + Connection](/api-reference/endpoints/app-connections/mssql/create) API endpoint. + + + Optionally, if you'd like Infisical to manage the credentials of this connection, you can set the `isPlatformManagedCredentials` option to `true`. + If enabled, Infisical will update the password of the connection on creation to prevent external access to this database role. + + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/mssql \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-mssql-connection", + "method": "username-and-password", + "isPlatformManagedCredentials": true, + "credentials": { + "host": "123.4.5.6", + "port": 1433, + "database": "default", + "username": "infisical_login", + "password": "my-password", + "sslEnabled": true, + "sslRejectUnauthorized": true + }, + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-pg-connection", + "version": 1, + "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "app": "mssql", + "method": "username-and-password", + "isPlatformManagedCredentials": true, + "credentials": { + "host": "123.4.5.6", + "port": 1433, + "database": "default", + "username": "infisical_login", + "sslEnabled": true, + "sslRejectUnauthorized": true + } + } + } + ``` + + diff --git a/docs/integrations/app-connections/overview.mdx b/docs/integrations/app-connections/overview.mdx new file mode 100644 index 000000000..92e6ab9d2 --- /dev/null +++ b/docs/integrations/app-connections/overview.mdx @@ -0,0 +1,82 @@ +--- +sidebarTitle: "Overview" +description: "Learn how to manage and configure third-party app connections with Infisical." +--- + +App Connections enable your organization to integrate Infisical with third-party services in a secure and versatile way. + +## Concept + +App Connections are an organization-level resource used to establish connections with third-party applications +that can be used across Infisical projects. Example use cases include syncing secrets, generating dynamic secrets, and more. + +
+ +
+ + ```mermaid + %%{init: {'flowchart': {'curve': 'linear'} } }%% + graph TD + A[AWS] + B[AWS Connection] + C[Project 1 Secret Sync] + D[Project 2 Secret Sync] + E[Project 3 Generate Dynamic Secret] + + B --> A + C --> B + D --> B + E --> B + + classDef default fill:#ffffff,stroke:#666,stroke-width:2px,rx:10px,color:black + classDef aws fill:#FFF2B2,stroke:#E6C34A,stroke-width:2px,color:black,rx:15px + classDef project fill:#E6F4FF,stroke:#0096D6,stroke-width:2px,color:black,rx:15px + classDef connection fill:#F4FFE6,stroke:#96D600,stroke-width:2px,color:black,rx:15px + + class A aws + class B connection + class C,D,E project + ``` + +
+ +## Workflow + +App Connections require initial setup in both your third-party application and Infisical. Follow these steps to establish a secure connection: + + + For step-by-step guides specific to each application, refer to the App Connections section in the Navigation Bar. + + +1. Create Access Entity: If necessary, create an entity such as a service account or role within the third-party application you want to connect to. Be sure +to limit the access of this entity to the minimal permission set required to perform the operations you need. For example: + - For secret syncing: Read/write permissions to specific secret stores + - For dynamic secrets: Permissions to create temporary credentials + + + Whenever possible, Infisical encourages creating a designated service account for your App Connection to limit the scope of permissions based on your use-case. + + +2. Generate Authentication Credentials: Obtain the required credentials from your third-party application. These can vary between applications and might be: + - an API key or access token + - A client ID and secret pair + - other credentials, etc. + +3. Create App Connection: Configure the connection in Infisical using your generated credentials through either the UI or API. + + + Some App Connections can only be created via the UI such as connections using OAuth. + + +4. Utilize the Connection: Use your App Connection for various features across Infisical such as our Secrets Sync by selecting it via the dropdown menu +in the UI or by passing the associated `connectionId` when generating resources via the API. + + + Infisical is continuously expanding its third-party application support. If your desired application isn't listed, + you can still use previous methods of connecting to it such as our Native Integrations. + + +## Platform Managed Credentials + +Some App Connections support the ability to have their credentials managed by Infisical. By enabling this option, +Infisical will modify the credentials to prevent external use of the configured access entity. \ No newline at end of file diff --git a/docs/integrations/app-connections/postgres.mdx b/docs/integrations/app-connections/postgres.mdx new file mode 100644 index 000000000..860e9ee3c --- /dev/null +++ b/docs/integrations/app-connections/postgres.mdx @@ -0,0 +1,128 @@ +--- +title: "PostgreSQL Connection" +description: "Learn how to configure a PostgreSQL Connection for Infisical." +--- + +Infisical supports connecting to PostgreSQL using a database role. + +## Configure a PostgreSQL Role for Infisical + + + + Infisical recommends creating a designated role in your PostgreSQL database for your connection. + ```SQL + -- create user role + CREATE ROLE infisical_role WITH LOGIN PASSWORD 'my-password'; + + -- grant login access to the specified database + GRANT CONNECT ON DATABASE my_database TO infisical_role; + ``` + + + Depending on how you intend to use your PostgreSQL connection, you'll need to grant one or more of the following permissions. + + To learn more about PostgreSQL's permission system, please visit their [documentation](https://www.postgresql.org/docs/current/sql-grant.html). + + + + For Secret Rotations, your Infisical user will require the ability to alter other users' passwords: + ```SQL + -- enable permissions to alter login credentials + ALTER ROLE infisical_role WITH CREATEROLE; + ``` + + + + + You'll need the following information to create your PostgreSQL connection: + - `host` - The hostname or IP address of your PostgreSQL server + - `port` - The port number your PostgreSQL server is listening on (default: 5432) + - `database` - The name of the specific database you want to connect to + - `username` - The role name of the login created in the steps above + - `password` - The role password of the login created in the steps above + - `sslCertificate` (optional) - The SSL certificate required for connection (if configured) + + + If you are self-hosting Infisical and intend to connect to an internal/private IP address, be sure to set the `ALLOW_INTERNAL_IP_CONNECTIONS` environment variable to `true`. + + + + +## Create Connection in Infisical + + + + 1. Navigate to the App Connections tab on the Organization Settings page. + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + 2. Select the **PostgreSQL Connection** option. + ![Select PostgreSQL Connection](/images/app-connections/postgres/select-postgres-connection.png) + + 3. Select the **Username & Password** method option and provide the details obtained from the previous section and press **Connect to PostgreSQL**. + + Optionally, if you'd like Infisical to manage the credentials of this connection, you can enable the Platform Managed Credentials option. + If enabled, Infisical will update the password of the connection on creation to prevent external access to this database role. + + ![Create PostgreSQL Connection](/images/app-connections/postgres/create-username-and-password-method.png) + + 4. Your **PostgreSQL Connection** is now available for use. + ![Assume Role PostgreSQL Connection](/images/app-connections/postgres/username-and-password-connection.png) + + + To create a PostgreSQL Connection, make an API request to the [Create PostgreSQL + Connection](/api-reference/endpoints/app-connections/postgres/create) API endpoint. + + + Optionally, if you'd like Infisical to manage the credentials of this connection, you can set the `isPlatformManagedCredentials` option to `true`. + If enabled, Infisical will update the password of the connection on creation to prevent external access to this database role. + + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/postgres \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-pg-connection", + "method": "username-and-password", + "isPlatformManagedCredentials": true, + "credentials": { + "host": "123.4.5.6", + "port": 5432, + "database": "default", + "username": "infisical_role", + "password": "my-password", + "sslEnabled": true, + "sslRejectUnauthorized": true + }, + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-pg-connection", + "version": 1, + "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "app": "postgres", + "method": "username-and-password", + "isPlatformManagedCredentials": true, + "credentials": { + "host": "123.4.5.6", + "port": 5432, + "database": "default", + "username": "infisical_role", + "sslEnabled": true, + "sslRejectUnauthorized": true + } + } + } + ``` + + diff --git a/docs/integrations/app-connections/terraform-cloud.mdx b/docs/integrations/app-connections/terraform-cloud.mdx new file mode 100644 index 000000000..02deb22cc --- /dev/null +++ b/docs/integrations/app-connections/terraform-cloud.mdx @@ -0,0 +1,83 @@ +--- +title: "Terraform Cloud Connection" +description: "Learn how to configure a Terraform Cloud Connection for Infisical." +--- + +Infisical supports connecting to Terraform Cloud using a service user. + +## Setup Terraform Cloud Connection in Infisical + + + + Navigate to the Terraform Cloud **Account Settings** tab. + ![Terraform Cloud Account Settings](/images/app-connections/terraform-cloud/terraform-cloud-account-settings.png) + + + Move to the **Tokens** tab. + ![Terraform Cloud Tokens Tab](/images/app-connections/terraform-cloud/terraform-cloud-tokens-tab.png) + + + Create the API token to be used by Infisical. + + If you configure an expiry date for your API token you will need to manually rotate to a new token prior to expiration to avoid integration downtime. + + ![Terraform Cloud Create API Token](/images/app-connections/terraform-cloud/terraform-cloud-create-api-token.png) + + + The API token will be displayed after creating it. Save the token in a secure location for later use in the following steps. + ![Terraform Cloud Copy API Token](/images/app-connections/terraform-cloud/terraform-cloud-copy-api-token.png) + + + + + 1. Navigate to the **App Connections** tab on the **Organization Settings** page. + ![App Connections Tab](/images/app-connections/general/add-connection.png) + 2. Select the **Terraform Cloud Connection** option from the connection options modal. + ![Select Terraform Cloud Connection](/images/app-connections/terraform-cloud/terraform-cloud-app-connection-option.png) + 3. Fill out the Terraform Cloud Connection modal, here you will need to provide the API Token generated in the previous step. + ![Terraform Cloud Connection Modal](/images/app-connections/terraform-cloud/terraform-cloud-app-connection-modal.png) + 4. Your **Terraform Cloud Connection** is now available for use. + ![Terraform Cloud Connection Created](/images/app-connections/terraform-cloud/terraform-cloud-app-connection-created.png) + + + To create an Terraform Cloud Connection, make an API request to the [Create Terraform Cloud + Connection](/api-reference/endpoints/app-connections/terraform-cloud/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/terraform-cloud \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-terraform-cloud-connection", + "method": "api-token", + "credentials": { + "apiToken": "...", + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-terraform-cloud-connection", + "version": 123, + "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "app": "terraform-cloud", + "method": "api-token", + "credentials": { + "apiToken": "..." + } + } + } + ``` + + + + diff --git a/docs/integrations/app-connections/vercel.mdx b/docs/integrations/app-connections/vercel.mdx new file mode 100644 index 000000000..8ef4a5647 --- /dev/null +++ b/docs/integrations/app-connections/vercel.mdx @@ -0,0 +1,97 @@ +--- +title: "Vercel Connection" +description: "Learn how to configure a Vercel Connection for Infisical." +--- + +Infisical supports connecting to Vercel using an API Token to securely sync your secrets to Vercel. + +## Setup Vercel Connection in Infisical + + + + Navigate to the Vercel **Account Settings** page by clicking on your profile icon in the top-right corner. + ![Vercel API Tokens Tab](/images/app-connections/vercel/vercel-main-page.png) + + + Select the **API Tokens** tab from the left sidebar navigation menu. + ![Vercel API Tokens Tab](/images/app-connections/vercel/vercel-settings-page.png) + + + Click the **Create** button and provide a name for your token (e.g., "Infisical Integration"). + Choose appropriate scope permissions based on your requirements. + + If you configure an expiry date for your API token, you will need to manually rotate to a new token prior to expiration to avoid integration downtime. Consider setting a calendar reminder for this task. + + ![Vercel Create API Token](/images/app-connections/vercel/vercel-create-token.png) + + + After creation, a modal with the API token will be displayed. Copy this token immediately and store it securely, as you won't be able to view it again after closing this dialog. + ![Vercel Copy API Token](/images/app-connections/vercel/vercel-copy-token.png) + + + You should now see your newly created token in the list of API tokens on the Vercel dashboard. + ![Vercel Connection Created](/images/app-connections/vercel/vercel-token-created.png) + + + + + 1. Navigate to App Connections + + In your Infisical dashboard, go to **Organization Settings** and select the **App Connections** tab. + ![App Connections Tab](/images/app-connections/general/add-connection.png) + 2. Add Connection + + Click the **+ Add Connection** button and select the **Vercel Connection** option from the available integrations. + ![Select Vercel Connection](/images/app-connections/vercel/vercel-app-connection-option.png) + 3. Fill the Vercel Connection Modal + + Complete the Vercel Connection form by entering: + - A descriptive name for the connection + - The API Token you generated in steps 3-4 + - An optional description for future reference + ![Vercel Connection Modal](/images/app-connections/vercel/vercel-app-connection-modal.png) + 4. Connection Created + + After clicking Create, your **Vercel Connection** is established and ready to use with your Infisical projects. + ![Vercel Connection Created](/images/app-connections/vercel/vercel-app-connection-created.png) + + + To create a Vercel Connection, make an API request to the [Create Vercel + Connection](/api-reference/endpoints/app-connections/vercel/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/vercel \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-vercel-connection", + "method": "api-token", + "credentials": { + "apiToken": "...", + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-vercel-connection", + "version": 123, + "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2025-04-01T05:31:56Z", + "updatedAt": "2025-04-01T05:31:56Z", + "app": "vercel", + "method": "api-token", + "credentials": {} + } + } + ``` + + + + \ No newline at end of file diff --git a/docs/integrations/app-connections/windmill.mdx b/docs/integrations/app-connections/windmill.mdx new file mode 100644 index 000000000..ca4aa7da4 --- /dev/null +++ b/docs/integrations/app-connections/windmill.mdx @@ -0,0 +1,112 @@ +--- +title: "Windmill Connection" +description: "Learn how to configure a Windmill Connection for Infisical." +--- + +Infisical supports connecting to Windmill using an **Access Token** to securely sync your secrets to Windmill. + +## Get a Windmill Access Token + +Ensure the user generating the access token has the required role and permissions based on your use-case: + + + + The user generating the access token should be at least a `Developer` in the configured workspace and have `write` permissions for the workspace path secrets will be synced to. + + + + + + + + In Windmill, click on your user in the sidebar and select **Account Settings**. + ![Windmill Account Settings](/images/app-connections/windmill/windmill-account-settings.png) + + + In the **Tokens** section on the drawer, click **Create token**. + ![Windmill Create Token](/images/app-connections/windmill/windmill-create-token.png) + + + Give your token a name and click **New token**. + + If you configure an expiry date for your access token, you must manually rotate to a new token before the expiration date to prevent service interruption. + + ![Windmill New Token](/images/app-connections/windmill/windmill-new-token.png) + + + Copy your new access token and save it for the steps below. + ![Windmill Copy Token](/images/app-connections/windmill/windmill-copy-token.png) + + + + +## Setup Windmill Connection in Infisical + + + + + + + In your Infisical dashboard, go to **Organization Settings** and select the **App Connections** tab. + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + + Click the **+ Add Connection** button and select the **Windmill Connection** option. + ![Select Windmill Connection](/images/app-connections/windmill/select-windmill-connection.png) + + + Configure your Windmill Connection using the access token generated in the steps above. Then click **Connect to Windmill**. + ![Windmill Configure Connection](/images/app-connections/windmill/create-windmill-access-token.png) + + - **Name**: The name of the connection to be created. Must be slug-friendly. + - **Description**: An optional description to provide details about this connection. + - **Instance URL**: The URL of your Windmill instance. If you are not self-hosting Windmill you can leave this field blank. + - **Access Token**: The access token generated in the steps above. + + + Your Windmill Connection is now available for use. + ![Windmill Connection Created](/images/app-connections/windmill/windmill-access-token-created.png) + + + + + To create a Windmill Connection, make an API request to the [Create Windmill + Connection](/api-reference/endpoints/app-connections/windmill/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/windmill \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-windmill-connection", + "method": "access-token", + "credentials": { + "token": "...", + "instanceUrl": "https://app.windmill.dev" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-windmill-connection", + "version": 123, + "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2025-04-01T05:31:56Z", + "updatedAt": "2025-04-01T05:31:56Z", + "app": "windmill", + "method": "access-token", + "credentials": { + "instanceUrl": "https://app.windmill.dev" + } + } + } + ``` + + diff --git a/docs/integrations/cicd/bitbucket.mdx b/docs/integrations/cicd/bitbucket.mdx index 2aa2106da..3c1330308 100644 --- a/docs/integrations/cicd/bitbucket.mdx +++ b/docs/integrations/cicd/bitbucket.mdx @@ -3,29 +3,37 @@ title: "Bitbucket" description: "How to sync secrets from Infisical to Bitbucket" --- +Infisical lets you sync secrets to Bitbucket at the repository-level and deployment environment-level. + + Prerequisites: - Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - Navigate to your project's integrations tab in Infisical. + + + Navigate to your project's integrations tab in Infisical. - ![integrations](../../images/integrations.png) + ![integrations](/images/integrations.png) - Press on the Bitbucket tile and grant Infisical access to your Bitbucket account. + Press on the Bitbucket tile and grant Infisical access to your Bitbucket account. - ![integrations bitbucket authorization](../../images/integrations/bitbucket/integrations-bitbucket-auth.png) + ![integrations bitbucket authorization](/images/integrations/bitbucket/integrations-bitbucket.png) + + + Select which workspace, repository, and optionally, deployment environment, you'd like to sync your secrets + to. + ![integrations configure + bitbucket](/images/integrations/bitbucket/integrations-bitbucket-configuration.png) - - - Select which Infisical environment secrets you want to sync to which Bitbucket repo and press start integration to start syncing secrets to the repo. + Once created, your integration will begin syncing secrets to the configured repository or deployment + environment. - ![integrations bitbucket](../../images/integrations/bitbucket/integrations-bitbucket.png) - - + ![integrations bitbucket](/images/integrations/bitbucket/integrations-bitbucket.png) + + @@ -36,7 +44,7 @@ Prerequisites: Create Bitbucket variables (can be either workspace, repository, or deployment-level) to store Machine Identity Client ID and Client Secret. - ![integrations bitbucket](../../images/integrations/bitbucket/integrations-bitbucket-env.png) + ![integrations bitbucket](/images/integrations/bitbucket/integrations-bitbucket-env.png) Edit your Bitbucket pipeline YAML file to include the use of the Infisical CLI to fetch and inject secrets into any script or command within the pipeline. diff --git a/docs/integrations/cicd/circleci.mdx b/docs/integrations/cicd/circleci.mdx index 0753f40f7..5bf04822d 100644 --- a/docs/integrations/cicd/circleci.mdx +++ b/docs/integrations/cicd/circleci.mdx @@ -11,21 +11,30 @@ Prerequisites: Obtain an API token in User Settings > Personal API Tokens - ![integrations circleci token](../../images/integrations/circleci/integrations-circleci-token.png) + ![integrations circleci token](/images/integrations/circleci/integrations-circleci-token.png) Navigate to your project's integrations tab in Infisical. - ![integrations](../../images/integrations.png) + ![integrations](/images/integrations.png) Press on the CircleCI tile and input your CircleCI API token to grant Infisical access to your CircleCI account. - ![integrations circleci authorization](../../images/integrations/circleci/integrations-circleci-auth.png) + ![integrations circleci authorization](/images/integrations/circleci/integrations-circleci-auth.png) - Select which Infisical environment secrets you want to sync to which CircleCI project and press create integration to start syncing secrets to CircleCI. + Select which Infisical environment secrets you want to sync to which CircleCI project or context. + + + ![integrations circle ci project](/images/integrations/circleci/integrations-circleci-create-project.png) + + + ![integrations circle ci project](/images/integrations/circleci/integrations-circleci-create-context.png) + + + + Finally, press create integration to start syncing secrets to CircleCI. + ![integrations circleci](/images/integrations/circleci/integrations-circleci.png) - ![create integration circleci](../../images/integrations/circleci/integrations-circleci-create.png) - ![integrations circleci](../../images/integrations/circleci/integrations-circleci.png) - \ No newline at end of file + diff --git a/docs/integrations/cicd/octopus-deploy.mdx b/docs/integrations/cicd/octopus-deploy.mdx new file mode 100644 index 000000000..90f06e09a --- /dev/null +++ b/docs/integrations/cicd/octopus-deploy.mdx @@ -0,0 +1,76 @@ +--- +title: "Octopus Deploy" +description: "Learn how to sync secrets from Infisical to Octopus Deploy" +--- + +Prerequisites: + +- Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + + + + Navigate to **Configuration** > **Users** and click on the **Create Service Account** button. + + ![integrations octopus deploy + users](/images/integrations/octopus-deploy/integrations-octopus-deploy-user-settings.png) + + Fill out the required fields and click on the **Save** button. + ![integrations octopus deploy service + account](/images/integrations/octopus-deploy/integrations-octopus-deploy-create-service-account.png) + + + On the **Service Account** user page, expand the **API Keys** section and click on the **New API Key** button. + + ![integrations octopus deploy + new api key](/images/integrations/octopus-deploy/integrations-octopus-deploy-create-api-key.png) + + Fill out the required fields and click on the **Generate New** button. + + ![integrations octopus deploy + generate api key](/images/integrations/octopus-deploy/integrations-octopus-deploy-generate-api-key.png) + + If you configure your access token to expire, + you will need to generate a new API key for Infisical prior to this date to keep your integration running. + + Copy the generated **API Key** and click on the **Close** button. + + ![integrations octopus deploy + copy api key](/images/integrations/octopus-deploy/integrations-octopus-deploy-copy-api-key.png) + + + You can skip creating a new team if you already have an Octopus Deploy team configured with + the **Project Contributor** role to assign your Service Account to. + + Navigate to **Configuration** > **Teams** and click on the **Add Team** button. + + ![integrations octopus deploy + teams](/images/integrations/octopus-deploy/integrations-octopus-deploy-team-settings.png) + + Create a new team for **Service Accounts** and click on the **Save** button. + ![integrations octopus deploy add + team](/images/integrations/octopus-deploy/integrations-octopus-deploy-create-team.png) + + On the **Members** tab, click on the **Add Member** button, add your **Infisical Service Account** and click on the **Add** button. + ![integrations octopus deploy add service account to team](/images/integrations/octopus-deploy/integrations-octopus-deploy-add-to-team.png) + + On the **User Roles** tab, click on the **Include User Role** button, and add the **Project Contributor** role. Optionally, + click on the **Define Scope** button to further refine what projects your Service Account has access to. Click on the **Apply** button once complete. + ![integrations octopus deploy add user roles to team](/images/integrations/octopus-deploy/integrations-octopus-deploy-add-role.png) + + Save your team changes by clicking on the **Save** button. + ![integrations octopus deploy save team changes](/images/integrations/octopus-deploy/integrations-octopus-deploy-save-team.png) + + + In Infisical, navigate to your **Project** > **Integrations** page and select the **Octopus Deploy** integration. + ![integration octopus deploy](/images/integrations/octopus-deploy/integrations-octopus-deploy-integrations.png) + + Enter your **Instance URL** and **API Key** from **Octopus Deploy** to authorize Infisical. + ![integration octopus deploy](/images/integrations/octopus-deploy/integrations-octopus-deploy-authorize.png) + + Select a **Space** and **Project** from **Octopus Deploy** to sync secrets to; configuring additional **Scope Values** as needed. Click on the **Create Integration** button once configured. + ![integration octopus deploy](/images/integrations/octopus-deploy/integrations-octopus-deploy-create.png) + + Your Infisical secrets will begin to sync to **Octopus Deploy**. + ![integration octopus deploy](/images/integrations/octopus-deploy/integrations-octopus-deploy-sync.png) + + \ No newline at end of file diff --git a/docs/integrations/cloud/aws-parameter-store.mdx b/docs/integrations/cloud/aws-parameter-store.mdx index 80b35b8fc..d2bb36a0b 100644 --- a/docs/integrations/cloud/aws-parameter-store.mdx +++ b/docs/integrations/cloud/aws-parameter-store.mdx @@ -3,197 +3,6 @@ title: "AWS Parameter Store" description: "Learn how to sync secrets from Infisical to AWS Parameter Store." --- - - - Infisical will assume the provided role in your AWS account securely, without the need to share any credentials. - - Prerequisites: - - - Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - To connect your Infisical instance with AWS, you need to set up an AWS IAM User account that can assume the AWS IAM Role for the integration. - - If your instance is deployed on AWS, the aws-sdk will automatically retrieve the credentials. Ensure that you assign the provided permission policy to your deployed instance, such as ECS or EC2. - - The following steps are for instances not deployed on AWS - - - Navigate to [Create IAM User](https://console.aws.amazon.com/iamv2/home#/users/create) in your AWS Console. - - - Attach the following inline permission policy to the IAM User to allow it to assume any IAM Roles: - ```json - { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "AllowAssumeAnyRole", - "Effect": "Allow", - "Action": "sts:AssumeRole", - "Resource": "arn:aws:iam::*:role/*" - } - ] - } - ``` - - - Obtain the AWS access key ID and secret access key for your IAM User by navigating to IAM > Users > [Your User] > Security credentials > Access keys. - - ![Access Key Step 1](../../images/integrations/aws/integrations-aws-access-key-1.png) - ![Access Key Step 2](../../images/integrations/aws/integrations-aws-access-key-2.png) - ![Access Key Step 3](../../images/integrations/aws/integrations-aws-access-key-3.png) - - - 1. Set the access key as **CLIENT_ID_AWS_INTEGRATION**. - 2. Set the secret key as **CLIENT_SECRET_AWS_INTEGRATION**. - - - - - - - 1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console. - ![IAM Role Creation](../../images/integrations/aws/integration-aws-iam-assume-role.png) - - 2. Select **AWS Account** as the **Trusted Entity Type**. - 3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If self-hosting, provide your AWS account number instead. - 4. Optionally, enable **Require external ID** and enter your **project ID** to further enhance security. - - - - ![IAM Role Permissions](../../images/integrations/aws/integration-aws-iam-assume-permission.png) - Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Parameter Store: - - ```json - { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "AllowSSMAccess", - "Effect": "Allow", - "Action": [ - "ssm:PutParameter", - "ssm:DeleteParameter", - "ssm:GetParameters", - "ssm:GetParametersByPath", - "ssm:DescribeParameters", - "ssm:DeleteParameters", - "ssm:AddTagsToResource", // if you need to add tags to secrets - "kms:ListKeys", // if you need to specify the KMS key - "kms:ListAliases", // if you need to specify the KMS key - "kms:Encrypt", // if you need to specify the KMS key - "kms:Decrypt" // if you need to specify the KMS key - ], - "Resource": "*" - } - ] - } - ``` - - - - ![Copy IAM Role ARN](../../images/integrations/aws/integration-aws-iam-assume-arn.png) - - - - 1. Navigate to your project's integrations tab in Infisical. - 2. Click on the **AWS Parameter Store** tile. - ![Select AWS Parameter Store](../../images/integrations.png) - - 3. Select the **AWS Assume Role** option. - ![Select Assume Role](../../images/integrations/aws/integration-aws-parameter-store-iam-assume-select.png) - - 4. Provide the **AWS IAM Role ARN** obtained from the previous step and press connect. - - - Select which Infisical environment secrets you want to sync to which AWS Parameter Store region and indicate the path for your secrets. Then, press create integration to start syncing secrets to AWS Parameter Store. - - ![integration create](../../images/integrations/aws/integrations-aws-parameter-store-create.png) - - - Infisical requires you to add a path for your secrets to be stored in AWS - Parameter Store and recommends setting the path structure to - `/[project_name]/[environment]/` according to best practices. This enables a - secret like `TEST` to be stored as `/[project_name]/[environment]/TEST` in AWS - Parameter Store. - - - - - - - Prerequisites: - - - Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - - Navigate to your IAM user permissions and add a permission policy to grant access to AWS Parameter Store. - - ![integration IAM 1](../../images/integrations/aws/integrations-aws-iam-1.png) - ![integration IAM 2](../../images/integrations/aws/integrations-aws-parameter-store-iam-2.png) - ![integrations IAM 3](../../images/integrations/aws/integrations-aws-parameter-store-iam-3.png) - - For enhanced security, here's a custom policy containing the minimum permissions required by Infisical to sync secrets to AWS Parameter Store for the IAM user that you can use: - - ```json - { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "AllowSSMAccess", - "Effect": "Allow", - "Action": [ - "ssm:PutParameter", - "ssm:DeleteParameter", - "ssm:GetParameters", - "ssm:GetParametersByPath", - "ssm:DescribeParameters", - "ssm:DeleteParameters", - "ssm:AddTagsToResource", // if you need to add tags to secrets - "kms:ListKeys", // if you need to specify the KMS key - "kms:ListAliases", // if you need to specify the KMS key - "kms:Encrypt", // if you need to specify the KMS key - "kms:Decrypt" // if you need to specify the KMS key - ], - "Resource": "*" - } - ] - } - ``` - - - - Obtain a AWS access key ID and secret access key for your IAM user in IAM > Users > User > Security credentials > Access keys - - ![access key 1](../../images/integrations/aws/integrations-aws-access-key-1.png) - ![access key 2](../../images/integrations/aws/integrations-aws-access-key-2.png) - ![access key 3](../../images/integrations/aws/integrations-aws-access-key-3.png) - - Navigate to your project's integrations tab in Infisical. - - ![integrations](../../images/integrations.png) - - Press on the AWS Parameter Store tile and select Access Key as the authentication mode. Input your AWS access key ID and secret access key from the previous step. - - ![integration auth](../../images/integrations/aws/integrations-aws-parameter-store-auth.png) - - - - Select which Infisical environment secrets you want to sync to which AWS Parameter Store region and indicate the path for your secrets. Then, press create integration to start syncing secrets to AWS Parameter Store. - - ![integration create](../../images/integrations/aws/integrations-aws-parameter-store-create.png) - - - Infisical requires you to add a path for your secrets to be stored in AWS - Parameter Store and recommends setting the path structure to - `/[project_name]/[environment]/` according to best practices. This enables a - secret like `TEST` to be stored as `/[project_name]/[environment]/TEST` in AWS - Parameter Store. - - - - - - + + The AWS Parameter Store Native Integration will be deprecated in 2026. Please migrate to our new [AWS Parameter Store Sync](../secret-syncs/aws-parameter-store). + \ No newline at end of file diff --git a/docs/integrations/cloud/aws-secret-manager.mdx b/docs/integrations/cloud/aws-secret-manager.mdx index 64df1df32..a56461998 100644 --- a/docs/integrations/cloud/aws-secret-manager.mdx +++ b/docs/integrations/cloud/aws-secret-manager.mdx @@ -3,257 +3,6 @@ title: "AWS Secrets Manager" description: "Learn how to sync secrets from Infisical to AWS Secrets Manager." --- - - -Infisical will assume the provided role in your AWS account securely, without the need to share any credentials. - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - To connect your Infisical instance with AWS, you need to set up an AWS IAM User account that can assume the AWS IAM Role for the integration. - -If your instance is deployed on AWS, the aws-sdk will automatically retrieve the credentials. Ensure that you assign the provided permission policy to your deployed instance, such as ECS or EC2. - -The following steps are for instances not deployed on AWS - - - Navigate to [Create IAM User](https://console.aws.amazon.com/iamv2/home#/users/create) in your AWS Console. - - - Attach the following inline permission policy to the IAM User to allow it to assume any IAM Roles: -```json -{ - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "AllowAssumeAnyRole", - "Effect": "Allow", - "Action": "sts:AssumeRole", - "Resource": "arn:aws:iam::*:role/*" - } - ] -} -``` - - - Obtain the AWS access key ID and secret access key for your IAM User by navigating to IAM > Users > [Your User] > Security credentials > Access keys. - - ![Access Key Step 1](../../images/integrations/aws/integrations-aws-access-key-1.png) - ![Access Key Step 2](../../images/integrations/aws/integrations-aws-access-key-2.png) - ![Access Key Step 3](../../images/integrations/aws/integrations-aws-access-key-3.png) - - - 1. Set the access key as **CLIENT_ID_AWS_INTEGRATION**. - 2. Set the secret key as **CLIENT_SECRET_AWS_INTEGRATION**. - - - - - - - 1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console. - ![IAM Role Creation](../../images/integrations/aws/integration-aws-iam-assume-role.png) - - 2. Select **AWS Account** as the **Trusted Entity Type**. - 3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If self-hosting, provide your AWS account number instead. - 4. Optionally, enable **Require external ID** and enter your **project ID** to further enhance security. - - - - ![IAM Role Permissions](../../images/integrations/aws/integration-aws-iam-assume-permission.png) - Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Secrets Manager: - - ```json - { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "AllowSecretsManagerAccess", - "Effect": "Allow", - "Action": [ - "secretsmanager:GetSecretValue", - "secretsmanager:CreateSecret", - "secretsmanager:UpdateSecret", - "secretsmanager:DescribeSecret", - "secretsmanager:TagResource", - "secretsmanager:UntagResource", - "kms:ListKeys", - "kms:ListAliases", - "kms:Encrypt", - "kms:Decrypt" - ], - "Resource": "*" - } - ] - } - ``` - - - - ![Copy IAM Role ARN](../../images/integrations/aws/integration-aws-iam-assume-arn.png) - - - - 1. Navigate to your project's integrations tab in Infisical. - 2. Click on the **AWS Secrets Manager** tile. - ![Select AWS Secrets Manager](../../images/integrations.png) - - 3. Select the **AWS Assume Role** option. - ![Select Assume Role](../../images/integrations/aws/integration-aws-iam-assume-select.png) - - 4. Provide the **AWS IAM Role ARN** obtained from the previous step. - - Select how you want to integration to work by specifying a number of parameters: - - - The environment in Infisical from which you want to sync secrets to AWS Secrets Manager. - - - The path within the preselected environment form which you want to sync secrets to AWS Secrets Manager. - - - The region that you want to integrate with in AWS Secrets Manager. - - - How you want the integration to map the secrets. The selected value could be either one to one or one to many. - - - The secret name/path in AWS into which you want to sync the secrets from Infisical. - - - ![integration create](../../images/integrations/aws/integrations-aws-secret-manager-create.png) - - Optionally, you can add tags or specify the encryption key of all the secrets created via this integration: - - - The Key/Value of a tag that will be added to secrets in AWS. Please note that it is possible to add multiple tags via API. - - - The alias/ID of the AWS KMS key used for encryption. Please note that key should be enabled in order to work and the IAM user should have access to it. - - ![integration options](../../images/integrations/aws/integrations-aws-secret-manager-options.png) - - Then, press `Create Integration` to start syncing secrets to AWS Secrets Manager. - - - Infisical currently syncs environment variables to AWS Secrets Manager as - key-value pairs under one secret. We're actively exploring ways to help users - group environment variable key-pairs under multiple secrets for greater - control. - - - Please note that upon deleting secrets in Infisical, AWS Secrets Manager immediately makes the secrets inaccessible but only schedules them for deletion after at least 7 days. - - - - - - -Infisical will access your account using the provided AWS access key and secret key. - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) -- Set up AWS and have/create an IAM user - - - - Navigate to your IAM user permissions and add a permission policy to grant access to AWS Secrets Manager. - - ![integration IAM 1](../../images/integrations/aws/integrations-aws-iam-1.png) - ![integration IAM 2](../../images/integrations/aws/integrations-aws-secret-manager-iam-2.png) - ![integrations IAM 3](../../images/integrations/aws/integrations-aws-secret-manager-iam-3.png) - - For better security, here's a custom policy containing the minimum permissions required by Infisical to sync secrets to AWS Secrets Manager for the IAM user that you can use: - - ```json - { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "AllowSecretsManagerAccess", - "Effect": "Allow", - "Action": [ - "secretsmanager:GetSecretValue", - "secretsmanager:CreateSecret", - "secretsmanager:UpdateSecret", - "secretsmanager:DescribeSecret", // if you need to add tags to secrets - "secretsmanager:TagResource", // if you need to add tags to secrets - "secretsmanager:UntagResource", // if you need to add tags to secrets - "kms:ListKeys", // if you need to specify the KMS key - "kms:ListAliases", // if you need to specify the KMS key - "kms:Encrypt", // if you need to specify the KMS key - "kms:Decrypt" // if you need to specify the KMS key - ], - "Resource": "*" - } - ] - } - ``` - - - - Obtain a AWS access key ID and secret access key for your IAM user in IAM > Users > User > Security credentials > Access keys - - ![access key 1](../../images/integrations/aws/integrations-aws-access-key-1.png) - ![access key 2](../../images/integrations/aws/integrations-aws-access-key-2.png) - ![access key 3](../../images/integrations/aws/integrations-aws-access-key-3.png) - - 1. Navigate to your project's integrations tab in Infisical. - 2. Click on the **AWS Secrets Manager** tile. - ![Select AWS Secrets Manager](../../images/integrations.png) - - 3. Select the **Access Key** option for Authentication Mode. - ![Select Access Key](../../images/integrations/aws/integrations-aws-secret-manager-auth.png) - 4. Provide the **access key** and **secret key** for the AWS Iam User. - - - - Select how you want to integration to work by specifying a number of parameters: - - - The environment in Infisical from which you want to sync secrets to AWS Secrets Manager. - - - The path within the preselected environment form which you want to sync secrets to AWS Secrets Manager. - - - The region that you want to integrate with in AWS Secrets Manager. - - - How you want the integration to map the secrets. The selected value could be either one to one or one to many. - - - The secret name/path in AWS into which you want to sync the secrets from Infisical. - - - ![integration create](../../images/integrations/aws/integrations-aws-secret-manager-create.png) - - Optionally, you can add tags or specify the encryption key of all the secrets created via this integration: - - - The Key/Value of a tag that will be added to secrets in AWS. Please note that it is possible to add multiple tags via API. - - - The alias/ID of the AWS KMS key used for encryption. Please note that key should be enabled in order to work and the IAM user should have access to it. - - ![integration options](../../images/integrations/aws/integrations-aws-secret-manager-options.png) - - Then, press `Create Integration` to start syncing secrets to AWS Secrets Manager. - - - Infisical currently syncs environment variables to AWS Secrets Manager as - key-value pairs under one secret. We're actively exploring ways to help users - group environment variable key-pairs under multiple secrets for greater - control. - - - Please note that upon deleting secrets in Infisical, AWS Secrets Manager immediately makes the secrets inaccessible but only schedules them for deletion after at least 7 days. - - - - - - + + The AWS Secrets Manager Native Integration will be deprecated in 2026. Please migrate to our new [AWS Secrets Manager Sync](../secret-syncs/aws-secrets-manager). + \ No newline at end of file diff --git a/docs/integrations/cloud/azure-app-configuration.mdx b/docs/integrations/cloud/azure-app-configuration.mdx index 249325704..4e7dfd94f 100644 --- a/docs/integrations/cloud/azure-app-configuration.mdx +++ b/docs/integrations/cloud/azure-app-configuration.mdx @@ -3,80 +3,6 @@ title: "Azure App Configuration" description: "How to sync secrets from Infisical to Azure App Configuration" --- - - - **Prerequisites:** - - - Set up and add envars to [Infisical Cloud](https://app.infisical.com). - - Set up Azure and have an existing App Configuration instance. - - User setting up the integration on Infisical must have the `App Configuration Data Owner` role for the intended Azure App Configuration instance. - - Azure App Configuration instance must be reachable by Infisical. - - - - Navigate to your project's integrations tab - - ![integrations](../../images/integrations/azure-app-configuration/new-infisical-integration.png) - - Press on the Azure App Configuration tile and grant Infisical access to App Configuration. - - - Obtain the Azure App Configuration endpoint from the overview tab. - ![integrations](../../images/integrations/azure-app-configuration/azure-app-config-endpoint.png) - - Select which Infisical environment secrets you want to sync to your Azure App Configuration. Then, input your App Configuration instance endpoint. Optionally, you can define a prefix for your secrets which will be appended to the keys upon syncing. - - ![integrations](../../images/integrations/azure-app-configuration/create-integration-form.png) - - Press create integration to start syncing secrets to Azure App Configuration. - - - - - - Using the Azure App Configuration integration on a self-hosted instance of Infisical requires configuring an application in Azure - and registering your instance with it. - - **Prerequisites:** - - - Set up Azure and have an existing App Configuration instance. - - - - Navigate to Azure Active Directory > App registrations to create a new application. - - - Azure Active Directory is now Microsoft Entra ID. - - ![integrations Azure app config](../../images/integrations/azure-app-configuration/config-aad.png) - ![integrations Azure app config](../../images/integrations/azure-app-configuration/config-new-app.png) - - Create the application. As part of the form, set the **Redirect URI** to `https://your-domain.com/integrations/azure-app-configuration/oauth2/callback`. - - The domain you defined in the Redirect URI should be equivalent to the `SITE_URL` configured in your Infisical instance. - - - ![integrations Azure app config](../../images/integrations/azure-app-configuration/app-registration-redirect.png) - - After registration, set the API permissions of the app to include the following Azure App Configuration permissions: KeyValue.Delete, KeyValue.Read, and KeyValue.Write. - ![integrations Azure app config](../../images/integrations/azure-app-configuration/app-api-permissions.png) - - - - Obtain the **Application (Client) ID** in Overview and generate a **Client Secret** in Certificate & secrets for your Azure application. - - ![integrations Azure app config](../../images/integrations/azure-app-configuration/config-credentials-1.png) - ![integrations Azure app config](../../images/integrations/azure-app-configuration/config-credentials-2.png) - ![integrations Azure app config](../../images/integrations/azure-app-configuration/config-credentials-3.png) - - Back in your Infisical instance, add two new environment variables for the credentials of your Azure application. - - - `CLIENT_ID_AZURE`: The **Application (Client) ID** of your Azure application. - - `CLIENT_SECRET_AZURE`: The **Client Secret** of your Azure application. - - Once added, restart your Infisical instance and use the Azure App Configuration integration. - - - - - + + The Azure App Configuration Native Integration will be deprecated in 2026. Please migrate to our new [Azure App Configuration Sync](../secret-syncs/azure-app-configuration). + \ No newline at end of file diff --git a/docs/integrations/cloud/azure-devops.mdx b/docs/integrations/cloud/azure-devops.mdx index 6d1ba6b17..4eaaf0cc1 100644 --- a/docs/integrations/cloud/azure-devops.mdx +++ b/docs/integrations/cloud/azure-devops.mdx @@ -21,7 +21,7 @@ You'll need to create a new personal access token (PAT) in order to authenticate ![integrations](../../images/integrations/azure-devops/create-new-token.png) - Please make sure that the token has access to the following scopes: Variable Groups _(read/write)_, Release _(read/write)_, Project and Team _(read)_, Service Connections _(read & query)_ + Please make sure that the token has access to the following scopes: Variable Groups _(read, create, & manage)_, Release _(read/write)_, Project and Team _(read)_, Service Connections _(read & query)_ diff --git a/docs/integrations/cloud/azure-key-vault.mdx b/docs/integrations/cloud/azure-key-vault.mdx index d04d90b4f..b0bd80c63 100644 --- a/docs/integrations/cloud/azure-key-vault.mdx +++ b/docs/integrations/cloud/azure-key-vault.mdx @@ -3,75 +3,6 @@ title: "Azure Key Vault" description: "How to sync secrets from Infisical to Azure Key Vault" --- - - - Prerequisites: - - - Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - Set up Azure and have an existing key vault - - - - Navigate to your project's integrations tab - - ![integrations](../../images/integrations.png) - - Press on the Azure Key Vault tile and grant Infisical access to Azure Key Vault. - - - Obtain the Vault URI of your key vault in the Overview tab. - - ![integrations](../../images/integrations/azure-key-vault/integrations-azure-key-vault-vault-uri.png) - - Select which Infisical environment secrets you want to sync to your key vault. Then, input your Vault URI from the previous step. Finally, press create integration to start syncing secrets to Azure Key Vault. - - ![integrations](../../images/integrations/azure-key-vault/integrations-azure-key-vault-create.png) - - ![integrations](../../images/integrations/azure-key-vault/integrations-azure-key-vault.png) - - - The Azure Key Vault integration requires the following secrets permissions to be set on the user / service principal - for Infisical to sync secrets to Azure Key Vault: `secrets/list`, `secrets/get`, `secrets/set`, `secrets/recover`. - - Any role with these permissions would work such as the **Key Vault Secrets Officer** role. - - - - - - - Using the Azure KV integration on a self-hosted instance of Infisical requires configuring an application in Azure - and registering your instance with it. - - - - Navigate to Azure Active Directory > App registrations to create a new application. - - - Azure Active Directory is now Microsoft Entra ID. - - ![integrations Azure KV config](../../images/integrations/azure-key-vault/integrations-azure-key-vault-config-aad.png) - ![integrations Azure KV config](../../images/integrations/azure-key-vault/integrations-azure-key-vault-config-new-app.png) - - Create the application. As part of the form, set the **Redirect URI** to `https://your-domain.com/integrations/azure-key-vault/oauth2/callback`. - - ![integrations Azure KV config](../../images/integrations/azure-key-vault/integrations-azure-key-vault-config-new-app-form.png) - - - Obtain the **Application (Client) ID** in Overview and generate a **Client Secret** in Certificate & secrets for your Azure application. - - ![integrations Azure KV config](../../images/integrations/azure-key-vault/integrations-azure-key-vault-config-credentials-1.png) - ![integrations Azure KV config](../../images/integrations/azure-key-vault/integrations-azure-key-vault-config-credentials-2.png) - ![integrations Azure KV config](../../images/integrations/azure-key-vault/integrations-azure-key-vault-config-credentials-3.png) - - Back in your Infisical instance, add two new environment variables for the credentials of your Azure application. - - - `CLIENT_ID_AZURE`: The **Application (Client) ID** of your Azure application. - - `CLIENT_SECRET_AZURE`: The **Client Secret** of your Azure application. - - Once added, restart your Infisical instance and use the Azure KV integration. - - - - - + + The Azure Key Vault Native Integration will be deprecated in 2026. Please migrate to our new [Azure Key Vault Sync](../secret-syncs/azure-key-vault). + \ No newline at end of file diff --git a/docs/integrations/cloud/databricks.mdx b/docs/integrations/cloud/databricks.mdx index 7fee3acd3..e5ad22939 100644 --- a/docs/integrations/cloud/databricks.mdx +++ b/docs/integrations/cloud/databricks.mdx @@ -3,29 +3,6 @@ title: "Databricks" description: "Learn how to sync secrets from Infisical to Databricks." --- -Prerequisites: - -- Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - - - - Obtain a Personal Access Token in **User Settings** > **Developer** > **Access Tokens**. - - ![integrations databricks token](../../images/integrations/databricks/pat-token.png) - - Navigate to your project's integrations tab in Infisical. - - ![integrations](../../images/integrations.png) - - Press on the Databricks tile and enter your Databricks instance URL in the following format: `https://xxx.cloud.databricks.com`. Then, input your Databricks Access Token to grant Infisical the necessary permissions in your Databricks account. - - ![integrations databricks authorization](../../images/integrations/databricks/integrations-databricks-auth.png) - - - - Select which Infisical environment and secret path you want to sync to which Databricks scope. Then, press create integration to start syncing secrets to Databricks. - - ![create integration Databricks](../../images/integrations/databricks/integrations-databricks-create.png) - ![integrations Databricks](../../images/integrations/databricks/integrations-databricks.png) - - \ No newline at end of file + + The Databricks Native Integration will be deprecated in 2026. Please migrate to our new [Databricks Sync](../secret-syncs/databricks). + \ No newline at end of file diff --git a/docs/integrations/cloud/gcp-secret-manager.mdx b/docs/integrations/cloud/gcp-secret-manager.mdx index e57a976f0..22462feef 100644 --- a/docs/integrations/cloud/gcp-secret-manager.mdx +++ b/docs/integrations/cloud/gcp-secret-manager.mdx @@ -3,138 +3,6 @@ title: "GCP Secret Manager" description: "How to sync secrets from Infisical to GCP Secret Manager" --- - - - - - Prerequisites: - - Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - - Navigate to your project's integrations tab in Infisical. - - ![integrations](../../images/integrations.png) - - Press on the GCP Secret Manager tile and select **Continue with OAuth** - - ![integrations GCP authorization options](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-auth-options.png) - - Grant Infisical access to GCP. - - ![integrations GCP authorization](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-auth.png) - - - - In the **Connection** tab, select which Infisical environment secrets you want to sync to which GCP secret manager project. Lastly, press create integration to start syncing secrets to GCP secret manager. - - ![integrations GCP secret manager](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-create.png) - - Note that the GCP Secret Manager integration supports a few options in the **Options** tab: - - - Secret Prefix: If inputted, the prefix is appended to the front of every secret name prior to being synced. - - Secret Suffix: If inputted, the suffix to appended to the back of every name of every secret prior to being synced. - - Label in GCP Secret Manager: If selected, every secret will be labeled in GCP Secret Manager (e.g. as `managed-by:infisical`); labels can be customized. - - Setting a secret prefix, suffix, or enabling the labeling option ensures that existing secrets in GCP Secret Manager are not overwritten during the sync. As part of this process, Infisical abstains from mutating any secrets in GCP Secret Manager without the specified prefix, suffix, or attached label. - - ![integrations GCP secret manager options](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-create-options.png) - - ![integrations GCP secret manager](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager.png) - - - Using Infisical to sync secrets to GCP Secret Manager requires that you enable - the Service Usage API and Cloud Resource Manager API in the Google Cloud project you want to sync secrets to. More on that [here](https://cloud.google.com/service-usage/docs/set-up-development-environment). - - Additionally, ensure that your GCP account has sufficient permission to manage secret and service resources (you can assign Secret Manager Admin and Service Usage Admin roles for testing purposes) - - - - - - Prerequisites: - - Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - Have a GCP project and have/create a [service account](https://cloud.google.com/iam/docs/service-account-overview) in it - - - - Navigate to **IAM & Admin** page in GCP and add the **Secret Manager Admin** and **Service Usage Admin** roles to the service account. - - ![integrations GCP secret manager IAM](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-iam.png) - - - For enhanced security, you may want to assign more granular permissions to the service account. At minimum, - the service account should be able to read/write secrets from/to GCP Secret Manager (e.g. **Secret Manager Admin** role) - and list which GCP services are enabled/disabled (e.g. **Service Usage Admin** role). - - - Navigate to your project's integrations tab in Infisical. - - ![integrations](../../images/integrations.png) - - Press on the GCP Secret Manager tile and paste in your **GCP Service Account JSON** (you can create and download the JSON for your - service account in IAM & Admin > Service Accounts > Service Account > Keys). - - ![integrations GCP authorization IAM key](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-iam-key.png) - - ![integrations GCP authorization options](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-auth-options.png) - - - - In the **Connection** tab, select which Infisical environment secrets you want to sync to the GCP secret manager project. Lastly, press create integration to start syncing secrets to GCP secret manager. - - ![integrations GCP secret manager](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-create.png) - - Note that the GCP Secret Manager integration supports a few options in the **Options** tab: - - - Secret Prefix: If inputted, the prefix is appended to the front of every secret name prior to being synced. - - Secret Suffix: If inputted, the suffix to appended to the back of every name of every secret prior to being synced. - - Label in GCP Secret Manager: If selected, every secret will be labeled in GCP Secret Manager (e.g. as `managed-by:infisical`); labels can be customized. - - Setting a secret prefix, suffix, or enabling the labeling option ensures that existing secrets in GCP Secret Manager are not overwritten during the sync. As part of this process, Infisical abstains from mutating any secrets in GCP Secret Manager without the specified prefix, suffix, or attached label. - - ![integrations GCP secret manager options](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-create-options.png) - - ![integrations GCP secret manager](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager.png) - - - Using Infisical to sync secrets to GCP Secret Manager requires that you enable - the Service Usage API and Cloud Resource Manager API in the Google Cloud project you want to sync secrets to. More on that [here](https://cloud.google.com/service-usage/docs/set-up-development-environment). - - - - - - - - - Using the GCP Secret Manager integration (via the OAuth2 method) on a self-hosted instance of Infisical requires configuring an OAuth2 application in GCP - and registering your instance with it. - - - - Navigate to your project API & Services > Credentials to create a new OAuth2 application. - - ![integrations GCP secret manager config](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-config-api-services.png) - ![integrations GCP secret manager config](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-config-new-app.png) - - Create the application. As part of the form, add to **Authorized redirect URIs**: `https://your-domain.com/integrations/gcp-secret-manager/oauth2/callback`. - - ![integrations GCP secret manager config](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-config-new-app-form.png) - - - Obtain the **Client ID** and **Client Secret** for your GCP OAuth2 application. - - ![integrations GCP secret manager config](../../images/integrations/gcp-secret-manager/integrations-gcp-secret-manager-config-credentials.png) - - Back in your Infisical instance, add two new environment variables for the credentials of your GCP OAuth2 application: - - - `CLIENT_ID_GCP_SECRET_MANAGER`: The **Client ID** of your GCP OAuth2 application. - - `CLIENT_SECRET_GCP_SECRET_MANAGER`: The **Client Secret** of your GCP OAuth2 application. - - Once added, restart your Infisical instance and use the GCP Secret Manager integration. - - - - - + + The GCP Secret Manager Native Integration will be deprecated in 2026. Please migrate to our new [GCP Secret Manager Sync](../secret-syncs/gcp-secret-manager). + \ No newline at end of file diff --git a/docs/integrations/cloud/terraform-cloud.mdx b/docs/integrations/cloud/terraform-cloud.mdx index d68e8a14f..63398ef4a 100644 --- a/docs/integrations/cloud/terraform-cloud.mdx +++ b/docs/integrations/cloud/terraform-cloud.mdx @@ -3,35 +3,6 @@ title: "Terraform Cloud" description: "How to sync secrets from Infisical to Terraform Cloud" --- -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - - Obtain a Terraform Cloud API Token in User Settings > Tokens - - ![integrations terraform cloud dashboard](../../images/integrations/terraform/integrations-terraformcloud-dashboard.png) - ![integrations terraform cloud tokens](../../images/integrations/terraform/integrations-terraformcloud-tokens.png) - - Obtain your Terraform Cloud Workspace Id in Projects & Workspaces > Workspace > ID - - ![integrations terraform cloud projects & workspaces](../../images/integrations/terraform/integrations-terraformcloud-workspaces.png) - ![integrations terraform cloud workspace id](../../images/integrations/terraform/integrations-terraformcloud-workspaceid.png) - - Navigate to your project's integrations tab in Infisical. - - ![integrations](../../images/integrations.png) - - Press on the Terraform Cloud tile and input your Terraform Cloud API Token and Workspace Id to grant Infisical access to your Terraform Cloud account. - - ![integrations terraform cloud authorization](../../images/integrations/terraform/integrations-terraformcloud-auth.png) - - - - Select which Infisical environment secrets and Terraform Cloud variable type you want to sync to which Terraform Cloud workspace/project and press create integration to start syncing secrets to Terraform Cloud. - - ![integrations terraform cloud](../../images/integrations/terraform/integrations-terraformcloud-create.png) - ![integrations terraform cloud](../../images/integrations/terraform/integrations-terraformcloud.png) - - + + The Terraform Cloud Native Integration will be deprecated in 2026. Please migrate to our new [Terraform Cloud Sync](../secret-syncs/terraform-cloud). + \ No newline at end of file diff --git a/docs/integrations/cloud/vercel.mdx b/docs/integrations/cloud/vercel.mdx index 1cb1c06c3..7456776bd 100644 --- a/docs/integrations/cloud/vercel.mdx +++ b/docs/integrations/cloud/vercel.mdx @@ -3,77 +3,6 @@ title: "Vercel" description: "How to sync secrets from Infisical to Vercel" --- - - - Prerequisites: - - Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - - Navigate to your project's integrations tab in Infisical. - - ![integrations](../../images/integrations.png) - - Press on the Vercel tile and grant Infisical access to your Vercel account. - - ![integrations vercel authorization](../../images/integrations/vercel/integrations-vercel-auth.png) - - - Select which Infisical environment secrets you want to sync to which Vercel app and environment. Lastly, press create integration to start syncing secrets to Vercel. - - ![integrations vercel](../../images/integrations/vercel/integrations-vercel-create.png) - ![integrations vercel](../../images/integrations/vercel/integrations-vercel.png) - - - Infisical syncs every envar to Vercel with type `encrypted` unless an existing - envar with the same name in Vercel exists with a different type. Note that - Infisical will not be able to update Vercel envars with type `sensitive` since - they can only be decrypted and modified by Vercel's deployment systems. - - - - The following environment variable names are reserved by Vercel and cannot be - synced: `AWS_SECRET_KEY`, `AWS_EXECUTION_ENV`, `AWS_LAMBDA_LOG_GROUP_NAME`, - `AWS_LAMBDA_LOG_STREAM_NAME`, `AWS_LAMBDA_FUNCTION_NAME`, - `AWS_LAMBDA_FUNCTION_MEMORY_SIZE`, `AWS_LAMBDA_FUNCTION_VERSION`, - `NOW_REGION`, `TZ`, `LAMBDA_TASK_ROOT`, `LAMBDA_RUNTIME_DIR`, - `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, - `AWS_REGION`, and `AWS_DEFAULT_REGION`. - - - - - - Using the Vercel integration on a self-hosted instance of Infisical requires configuring an integration in Vercel. - and registering your instance with it. - - - - Navigate to Integrations > Integration Console to create a new integration. - - ![integrations Vercel config](../../images/integrations/vercel/integrations-vercel-config-integrations-console.png) - ![integrations Vercel config](../../images/integrations/vercel/integrations-vercel-config-new-app.png) - - Create the application. As part of the form, set a **URL Slug** to a unique slug like `infisical-your-domain` and keep it handy. Also, set **Redirect URL** to `https://your-domain.com/integrations/vercel/oauth2/callback`. Lastly, - be sure to set the API Scopes according to the second screenshot below. - - ![integrations Vercel config](../../images/integrations/vercel/integrations-vercel-config-new-app-form-1.png) - ![integrations Vercel config](../../images/integrations/vercel/integrations-vercel-config-new-app-form-2.png) - - - Obtain the **Client (Integration) ID** and **Client (Integration) Secret** as well as the **URL Slug** from earlier for your Vercel integration. - - ![integrations Vercel config](../../images/integrations/vercel/integrations-vercel-config-credentials.png) - - Back in your Infisical instance, add three new environment variables for the credentials of your Vercel integration. - - - `CLIENT_ID_VERCEL`: The **Client (Integration) ID** of your Vercel integration. - - `CLIENT_SECRET_VERCEL`: The **Client (Integration) Secret** of your Vercel integration. - - `CLIENT_SLUG_VERCEL`: The **URL Slug** of your Vercel integration. - - Once added, restart your Infisical instance and use the Vercel integration. - - - - - + + The Vercel Native Integration will be deprecated in 2026. Please migrate to our new [Vercel Sync](../secret-syncs/vercel). + \ No newline at end of file diff --git a/docs/integrations/external/backstage.mdx b/docs/integrations/external/backstage.mdx new file mode 100644 index 000000000..106beee44 --- /dev/null +++ b/docs/integrations/external/backstage.mdx @@ -0,0 +1,123 @@ +--- +title: Backstage Infisical Plugin +description: A powerful plugin that integrates Infisical secrets management into your Backstage developer portal. +--- + +Integrate secrets management into your developer portal with the Backstage Infisical plugin suite. This plugin provides a seamless interface to manage your [Infisical](https://infisical.com) secrets directly within Backstage, including full support for environments and folder structure. + +## Features + +- **Secrets Management**: View, create, update, and delete secrets from Infisical +- **Folder Navigation**: Explore the full folder structure of your Infisical projects +- **Multi-Environment Support**: Easily switch between and manage different environments +- **Entity Linking**: Map Backstage entities to specific Infisical projects via annotations + +--- +## Installation + +### Frontend Plugin + +```bash +# From your Backstage root directory +yarn --cwd packages/app add @infisical/backstage-plugin-infisical +``` + +### Backend Plugin + +```bash +# From your Backstage root directory +yarn --cwd packages/backend add @infisical/backstage-backend-plugin-infisical +``` + +## Configuration + +### Backend + +Update your `app-config.yaml`: + +```yaml +infisical: + baseUrl: https://app.infisical.com + + authentication: + # Option 1: API Token Authentication + auth_token: + token: ${INFISICAL_API_TOKEN} + + # Option 2: Client Credentials Authentication + universalAuth: + clientId: ${INFISICAL_CLIENT_ID} + clientSecret: ${INFISICAL_CLIENT_SECRET} +``` + + + If you have not created a machine identity yet, you can do so in [Identities](/documentation/platform/identities/machine-identities) + + +Register the plugin in `packages/backend/src/index.ts`: + +```ts +import { createBackend } from '@backstage/backend-defaults'; + +const backend = createBackend(); + +backend.add(import('@infisical/backstage-backend-plugin-infisical')); + +backend.start(); +``` + +### Frontend + +Update `packages/app/src/App.tsx` to include the plugin: + +```tsx +import { infisicalPlugin } from '@infisical/backstage-plugin-infisical'; + +const app = createApp({ + plugins: [ + infisicalPlugin, + // ...other plugins + ], +}); +``` + +Modify `packages/app/src/components/catalog/EntityPage.tsx`: + +```tsx +import { EntityInfisicalContent } from '@infisical/backstage-plugin-infisical'; + +const serviceEntityPage = ( + + {/* ...other tabs */} + + + + +); +``` + +### Entity Annotation + +Add the Infisical project ID to your entity yaml settings: + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: example-service + annotations: + infisical/projectId: +``` + +> Replace `` with the actual project ID from Infisical. + +## Usage + +Once installed and configured, you can: + +1. **View and manage secrets** in Infisical from within Backstage +2. **Create, update, and delete** secrets using the Infisical tab in entity pages +3. **Navigate environments and folders** +4. **Search and filter** secrets by key, value, or comments + +![Backstage Plugin Table](/images/integrations/external/backstage/backstage-plugin-infisical.png) \ No newline at end of file diff --git a/docs/integrations/frameworks/ab-initio.mdx b/docs/integrations/frameworks/ab-initio.mdx new file mode 100644 index 000000000..e56ff62d6 --- /dev/null +++ b/docs/integrations/frameworks/ab-initio.mdx @@ -0,0 +1,32 @@ +--- +title: "AB Initio" +description: "How to use Infisical secrets in AB Initio." +--- + +## Prerequisites + +- Set up and add envars to [Infisical](https://app.infisical.com). +- Install the [Infisical CLI](https://infisical.com/docs/cli/overview) to your server. + +## Setup + + + + Create a [machine identity](https://infisical.com/docs/documentation/platform/identities/machine-identities#machine-identities) in Infisical and give it the appropriate read permissions for the desired project and secret paths. + + + Update your AB Initio workflows to use Infisical CLI to inject Infisical secrets as environment variables. + + ```bash + # Login using the machine identity. Modify this accordingly based on the authentication method used. + export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id=$INFISICAL_CLIENT_ID --client-secret=$INFISICAL_CLIENT_SECRET --silent --plain) + + # Fetch secrets from Infisical + infisical export --projectId="<>" --env="prod" > infisical.env + + # Inject secrets as environment variables + source infisical.env + ``` + + + diff --git a/docs/integrations/frameworks/terraform.mdx b/docs/integrations/frameworks/terraform.mdx index e7a60ac7e..898a77b00 100644 --- a/docs/integrations/frameworks/terraform.mdx +++ b/docs/integrations/frameworks/terraform.mdx @@ -1,101 +1,237 @@ --- title: "Terraform" -description: "Learn how to fetch Secrets From Infisical With Terraform." +description: "Learn how to fetch secrets from Infisical with Terraform using both traditional data sources and ephemeral resources" --- -This guide provides step-by-step guidance on how to fetch secrets from Infisical using Terraform. +This guide demonstrates how to use Infisical to manage secrets in your Terraform infrastructure code, supporting both traditional data sources and ephemeral resources for enhanced security. It uses: + +- Infisical (you can use [Infisical Cloud](https://app.infisical.com) or a [self-hosted instance of Infisical](https://infisical.com/docs/self-hosting/overview)) to store your secrets +- The [Terraform Provider](https://registry.terraform.io/providers/Infisical/infisical/latest/docs) to fetch secrets for your infrastructure ## Prerequisites -- Basic understanding of Terraform -- Install [Terraform](https://www.terraform.io/downloads.html) +Before you begin, make sure you have: -## Steps +- [Terraform](https://www.terraform.io/downloads.html) installed (v1.10.0+ for ephemeral resources) +- An Infisical account with access to a project +- Basic understanding of Terraform and infrastructure as code -### 1. Define Required Providers +## Project Setup -Specify `infisical` in the `required_providers` block within the `terraform` block of your configuration file. If you would like to use a specific version of the provider, uncomment and replace `` with the version of the Infisical provider that you want to use. +### Configure Provider -```hcl main.tf +First, specify the Infisical provider in your Terraform configuration: + +```hcl terraform { required_providers { infisical = { - # version = source = "infisical/infisical" } } } ``` -### 2. Configure the Infisical Provider +### Authentication -Set up the Infisical provider by specifying the `host` and `service_token`. Replace `<>` in `service_token` with your actual token. The `host` is only required if you are using a self-hosted instance of Infisical. +Configure the provider using one of these authentication methods: -```hcl main.tf +#### Machine Identity (Recommended) + +Using a Machine Identity, you can authenticate your Terraform provider using either [OIDC Auth](https://infisical.com/docs/documentation/platform/identities/oidc-auth/general) or [Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth) methods. + +```hcl provider "infisical" { - host = "https://app.infisical.com" # Only required if using a self-hosted instance of Infisical, default is https://app.infisical.com - client_id = "<>" - client_secret = "<>" - service_token = "<>" # DEPRECATED, USE MACHINE IDENTITY AUTH INSTEAD + host = "https://app.infisical.com" # Optional for cloud, required for self-hosted + auth { + universal { # or use oidc authentication method by providing an identity_id + client_id = var.infisical_client_id + client_secret = var.infisical_client_secret + } + } +} +``` +Learn more about [machine identities](/documentation/platform/identities/machine-identities). + +#### Service Token (Legacy) + + +Machine Identity authentication is strongly recommended as the secure and modern method. Service tokens are considered legacy and will be deprecated in a future release. + + +```hcl +provider "infisical" { + host = "https://app.infisical.com" + service_token = var.infisical_service_token +} +``` + +## Using Secrets in Terraform + +Infisical provides two methods to fetch and use secrets in your Terraform configurations: + +### Method 1: Ephemeral Resources (Recommended) + +Ephemeral resources, introduced in Terraform v1.10, provide enhanced security by ensuring sensitive values are never persisted in state files. This is the recommended approach for handling secrets in your infrastructure code. + +```hcl +# Fetch database credentials ephemerally +ephemeral "infisical_secret" "db_creds" { + name = "DB_CREDENTIALS" + env_slug = "prod" + workspace_id = var.infisical_workspace_id + folder_path = "/database" +} + +# Use the credentials to configure a provider +provider "postgresql" { + host = data.aws_db_instance.example.address + port = data.aws_db_instance.example.port + username = jsondecode(ephemeral.infisical_secret.db_creds.value)["username"] + password = jsondecode(ephemeral.infisical_secret.db_creds.value)["password"] +} +``` + +Key benefits: +- Values are never stored in state files +- Secrets are fetched on-demand during each Terraform operation +- Perfect for GitOps workflows +- Improved security posture for your infrastructure as code + +### Method 2: Data Sources + +For backwards compatibility or when working with older Terraform versions, you can use the traditional data source approach: + +```hcl +# Fetch all secrets in a folder +data "infisical_secrets" "my_secrets" { + env_slug = "dev" + workspace_id = var.infisical_workspace_id + folder_path = "/api" +} + +# Use individual secrets +resource "aws_db_instance" "example" { + username = data.infisical_secrets.my_secrets.secrets["DB_USER"] + password = data.infisical_secrets.my_secrets.secrets["DB_PASS"] } ``` - It is recommended to use Terraform variables to pass your service token dynamically to avoid hard coding it + When using data sources, secret values are stored in Terraform's state file. Ensure your state file is properly secured. -### 3. Fetch Infisical Secrets +## Common Use Cases -Use the `infisical_secrets` data source to fetch your secrets. In this block, you must set the `env_slug` and `folder_path` to scope the secrets you want. +### Secure Database Credential Management -`env_slug` is the slug of the environment name. This slug name can be found under the project settings page on the Infisical dashboard. - -`folder_path` is the path to the folder in a given environment. The path `/` for root of the environment where as `/folder1` is the folder at the root of the environment. - -```hcl main.tf -data "infisical_secrets" "my-secrets" { - env_slug = "dev" - folder_path = "/some-folder/another-folder" - workspace_id = "your-project-id" -} -``` - -### 4. Define Outputs - -As an example, we are going to output your fetched secrets. Replace `SECRET-NAME` with the actual name of your secret. - -For a single secret: - -```hcl main.tf -output "single-secret" { - value = data.infisical_secrets.my-secrets.secrets["SECRET-NAME"] -} -``` - -For all secrets: +Manage database credentials securely without exposing sensitive information in your state files: ```hcl -output "all-secrets" { - value = data.infisical_secrets.my-secrets.secrets +# Fetch database credentials securely +ephemeral "infisical_secret" "db_creds" { + name = "DB_CREDENTIALS" + env_slug = "prod" + workspace_id = var.infisical_workspace_id + folder_path = "/database" +} + +# Use the credentials in your database instance +resource "aws_db_instance" "example" { + identifier = "my-database" + allocated_storage = 20 + engine = "postgres" + engine_version = "14.0" + instance_class = "db.t3.micro" + + # Securely inject credentials from Infisical + username = jsondecode(ephemeral.infisical_secret.db_creds.value)["username"] + password = jsondecode(ephemeral.infisical_secret.db_creds.value)["password"] } ``` -### 5. Run Terraform +### GitOps Workflow with OIDC -Once your configuration is complete, initialize your Terraform working directory: +To eliminate the need for static credentials, you can authenticate your workflow using [OpenID Connect (OIDC)](https://infisical.com/docs/documentation/platform/identities/oidc-auth/general) through providers like the [Infisical Secrets GitHub Action](https://github.com/Infisical/secrets-action). +Once authenticated, you can securely access secrets through the Infisical provider: -```bash -$ terraform init +```hcl +provider "infisical" { + # Auth credentials automatically injected from the environment +} + +# Fetch deployment credentials +ephemeral "infisical_secret" "deploy_token" { + name = "DEPLOY_TOKEN" + env_slug = "prod" + workspace_id = var.infisical_workspace_id + folder_path = "/deployment" +} ``` +For detailed instructions on setting up OIDC authentication with GitHub Actions, refer to our [GitHub Actions OIDC guide](https://infisical.com/docs/documentation/platform/identities/oidc-auth/github). -Then, run the plan command to view the fetched secrets: +## Best Practices -```bash -$ terraform plan -``` +1. **Use Ephemeral Resources**: Whenever possible, use ephemeral resources instead of data sources for improved security. -Terraform will now fetch your secrets from Infisical and display them as output according to your configuration. +2. **Organize Secrets**: Structure your secrets in Infisical using folders to maintain clean separation: + ```hcl + ephemeral "infisical_secret" "db_secret" { + folder_path = "/databases/postgresql" # Organized by service + # ... + } + ``` -## Conclusion +3. **Variable Usage**: Use Terraform variables for workspace IDs and environment slugs: + ```hcl + variable "environment" { + description = "Environment (dev, staging, prod)" + type = string + } -You have now successfully set up and used the Infisical provider with Terraform to fetch secrets. For more information, visit the [Infisical documentation](https://registry.terraform.io/providers/Infisical/infisical/latest/docs). + ephemeral "infisical_secret" "secret" { + env_slug = var.environment + # ... + } + ``` + +4. **Error Handling**: Add lifecycle blocks for critical secrets: + ```hcl + ephemeral "infisical_secret" "critical_secret" { + # ... + lifecycle { + postcondition { + condition = length(self.value) > 0 + error_message = "Critical secret must not be empty" + } + } + } + ``` + +## FAQ + + + + If you're using Terraform < v1.10.0, you'll need to use the data source approach. + Consider upgrading to take advantage of the enhanced security features provided + by ephemeral resources. + + + Yes, you can use both in the same configuration. However, we recommend using + ephemeral resources for any sensitive values to ensure they're not stored in state. + + + When using data sources, follow Terraform's best practices for state management: + - Use remote state with encryption at rest + - Implement proper access controls + - Consider using state encryption + - Treat the state like a secret + + Better yet, use ephemeral resources to avoid storing sensitive values in state entirely. + + + +See also: +- [Machine Identity setup guide](/documentation/platform/identities/machine-identities) +- [Terraform Provider Registry](https://registry.terraform.io/providers/Infisical/infisical/latest/docs) +- [GitOps Best Practices](https://www.infisical.com/blog/gitops-best-practices) diff --git a/docs/integrations/overview.mdx b/docs/integrations/overview.mdx index f138ee8ec..08debf8cf 100644 --- a/docs/integrations/overview.mdx +++ b/docs/integrations/overview.mdx @@ -42,6 +42,7 @@ Missing an integration? [Throw in a request](https://github.com/Infisical/infisi | [CircleCI](/integrations/cicd/circleci) | CI/CD | Available | | [Travis CI](/integrations/cicd/travisci) | CI/CD | Available | | [Rundeck](/integrations/cicd/rundeck) | CI/CD | Available | +| [Octopus Deploy](/integrations/cicd/octopus-deploy) | CI/CD | Available | | [React](/integrations/frameworks/react) | Framework | Available | | [Vue](/integrations/frameworks/vue) | Framework | Available | | [Express](/integrations/frameworks/express) | Framework | Available | diff --git a/docs/integrations/platforms/apache-airflow.mdx b/docs/integrations/platforms/apache-airflow.mdx new file mode 100644 index 000000000..db4bd3eb2 --- /dev/null +++ b/docs/integrations/platforms/apache-airflow.mdx @@ -0,0 +1,5 @@ +--- +title: "Apache Airflow" +description: "Learn how to use Infisical as your custom secrets backend in Apache Airflow." +url: "https://github.com/Infisical/airflow-provider-infisical?tab=readme-ov-file#airflow-infisical-provider" +--- diff --git a/docs/integrations/platforms/kubernetes-csi.mdx b/docs/integrations/platforms/kubernetes-csi.mdx new file mode 100644 index 000000000..88df9585c --- /dev/null +++ b/docs/integrations/platforms/kubernetes-csi.mdx @@ -0,0 +1,281 @@ +--- +title: "Kubernetes CSI" +description: "How to use Infisical to inject secrets directly into Kubernetes pods." +--- + +## Overview + +The Infisical CSI provider allows you to use Infisical with the [Secrets Store CSI driver](https://secrets-store-csi-driver.sigs.k8s.io) to inject secrets directly into your Kubernetes pods through a volume mount. +In contrast to the [Infisical Kubernetes Operator](https://infisical.com/docs/integrations/platforms/kubernetes), the Infisical CSI provider will allow you to sync Infisical secrets directly to pods as files, removing the need for Kubernetes secret resources. + +```mermaid +flowchart LR + subgraph Secrets Management + SS(Infisical) --> CSP(Infisical CSI Provider) + CSP --> CSD(Secrets Store CSI Driver) + end + + subgraph Application + CSD --> V(Volume) + V <--> P(Pod) + end + +``` + +## Features + +The following features are supported by the Infisical CSI Provider: + +- Integration with Secrets Store CSI Driver for direct pod mounting +- Authentication using Kubernetes service accounts via machine identities +- Auto-syncing secrets when enabled via CSI Driver +- Configurable secret paths and file mounting locations +- Installation via Helm + +## Prerequisites + +The Infisical CSI provider is only supported for Kubernetes clusters with version >= 1.20. + +## Limitations + +Currently, the Infisical CSI provider only supports static secrets. + +## Deploy to Kubernetes cluster + +### Install Secrets Store CSI Driver + +In order to use the Infisical CSI provider, you will first have to install the [Secrets Store CSI driver](https://secrets-store-csi-driver.sigs.k8s.io/getting-started/installation) to your cluster. It is important that you define +the audience value for token requests as demonstrated below. The Infisical CSI provider will **NOT WORK** if this is not set. + +```bash +helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts +``` + +```bash +helm install csi secrets-store-csi-driver/secrets-store-csi-driver \ +--namespace=kube-system \ +--set "tokenRequests[0].audience=infisical" \ +--set enableSecretRotation=true \ +--set rotationPollInterval=2m \ +--set "syncSecret.enabled=true" \ +``` + +The flags configure the following: + +- `tokenRequests[0].audience=infisical`: Sets the audience value for service account token authentication (required) +- `enableSecretRotation=true`: Enables automatic secret updates from Infisical +- `rotationPollInterval=2m`: Checks for secret updates every 2 minutes +- `syncSecret.enabled=true`: Enables syncing secrets to Kubernetes secrets + + + If you do not wish to use the auto-syncing feature of the secrets store CSI + driver, you can omit the `enableSecretRotation` and the `rotationPollInterval` + flags. Do note that by default, secrets from Infisical are only fetched and + mounted during pod creation. If there are any changes made to the secrets in + Infisical, they will not propagate to the pods unless auto-syncing is enabled + for the CSI driver. + + +### Install Infisical CSI Provider + +You would then have to install the Infisical CSI provider to your cluster. + +**Install the latest Infisical Helm repository** + +```bash +helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/' + +helm repo update +``` + +**Install the Helm Chart** + +```bash +helm install infisical-csi-provider infisical-helm-charts/infisical-csi-provider +``` + +For a list of all supported arguments for the helm installation, you can run the following: + +```bash +helm show values infisical-helm-charts/infisical-csi-provider +``` + +### Authentication + +In order for the Infisical CSI provider to pull secrets from your Infisical project, you will have to configure +a machine identity with [Kubernetes authentication](https://infisical.com/docs/documentation/platform/identities/kubernetes-auth) configured with your cluster. +You can refer to the documentation for setting it up [here](https://infisical.com/docs/documentation/platform/identities/kubernetes-auth#guide). + + + The allowed audience field of the Kubernetes authentication settings should + match the audience specified for the Secrets Store CSI driver during + installation. + + +### Creating Secret Provider Class + +With the Secrets Store CSI driver and the Infisical CSI provider installed, create a Kubernetes [SecretProviderClass](https://secrets-store-csi-driver.sigs.k8s.io/concepts.html#secretproviderclass) resource to establish +the connection between the CSI driver and the Infisical CSI provider for secret retrieval. You can create as many Secret Provider Classes as needed for your cluster. + +```yaml +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: my-infisical-app-csi-provider +spec: + provider: infisical + parameters: + infisicalUrl: "https://app.infisical.com" + authMethod: "kubernetes" + identityId: "ad2f8c67-cbe2-417a-b5eb-1339776ec0b3" + projectId: "09eda1f8-85a3-47a9-8a6f-e27f133b2a36" + envSlug: "prod" + secrets: | + - secretPath: "/" + fileName: "dbPassword" + secretKey: "DB_PASSWORD" + - secretPath: "/app" + fileName: "appSecret" + secretKey: "APP_SECRET" +``` + + + The SecretProviderClass should be provisioned in the same namespace as the pod + you intend to mount secrets to. + + +#### Supported Parameters + + + The base URL of your Infisical instance. If you're using Infisical Cloud US, + this should be set to `https://app.infisical.com`. If you're using Infisical + Cloud EU, then this should be set to `https://eu.infisical.com`. + + + + The CA certificate of the Infisical instance in order to establish SSL/TLS + when the instance uses a private or self-signed certificate. Unless necessary, + this should be omitted. + + + + The auth method to use for authenticating the Infisical CSI provider with + Infisical. For now, the only supported method is `kubernetes`. + + + + The ID of the machine identity to use for authenticating the Infisical CSI + provider with your Infisical organization. This should be the machine identity + configured with Kubernetes authentication. + + + + The project ID of the Infisical project to pull secrets from. + + + + The slug of the project environment to pull secrets from. + + + + An array that defines which secrets to retrieve and how to mount them. Each + entry requires three properties: `secretPath` and `secretKey` work together to + identify the source secret to fetch, while `fileName` specifies the path where + the secret's value will be mounted within the pod's filesystem. + + + + The custom audience value configured for the CSI driver. This defaults to + `infisical`. + + +### Using Secret Provider Class + +A pod can use the Secret Provider Class by mounting it as a CSI volume: + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-secrets-store + labels: + app: nginx +spec: + containers: + - name: nginx + image: nginx + volumeMounts: + - name: secrets-store-inline + mountPath: "/mnt/secrets-store" + readOnly: true + volumes: + - name: secrets-store-inline + csi: + driver: secrets-store.csi.k8s.io + readOnly: true + volumeAttributes: + secretProviderClass: "my-infisical-app-csi-provider" +``` + +When the pod is created, the secrets are mounted as individual files in the /mnt/secrets-store directory. + +### Verifying Secret Mounts + +To verify your secrets are mounted correctly: + +```bash +# Check pod status +kubectl get pod nginx-secrets-store + +# View mounted secrets +kubectl exec -it nginx-secrets-store -- ls -l /mnt/secrets-store +``` + +### Troubleshooting + +To troubleshoot issues with the Infisical CSI provider, refer to the logs of the Infisical CSI provider running on the same node as your pod. + +```bash +kubectl logs infisical-csi-provider-7x44t +``` + +You can also refer to the logs of the secrets store CSI driver. Modify the command below with the appropriate pod and namespace of your secrets store CSI driver installation. + +```bash +kubectl logs csi-secrets-store-csi-driver-7h4jp -n=kube-system +``` + +**Common issues include:** + +- Mismatch in the audience value of the CSI driver with the machine identity's Kubernetes auth configuration +- SecretProviderClass in the wrong namespace +- Invalid machine identity configuration +- Incorrect secret paths or keys + +## Best Practices + +For additional guidance on setting this up for your production cluster, you can refer to the Secrets Store CSI driver documentation [here](https://secrets-store-csi-driver.sigs.k8s.io/topics/best-practices). + +## Frequently Asked Questions + + + + Yes, but it requires an indirect approach: + + 1. First enable syncing to Kubernetes secrets by setting `syncSecret.enabled=true` in the CSI driver installation + 2. Configure the Secret Provider Class to sync specific secrets to Kubernetes secrets + 3. Use the resulting Kubernetes secrets in your pod's environment variables + + This means secrets are first synced to Kubernetes secrets before they can be used as environment variables. You can find detailed examples in the [Secrets Store CSI driver documentation](https://secrets-store-csi-driver.sigs.k8s.io/topics/set-as-env-var). + + + + + + + Yes, you will need to explicitly list each secret you want to sync in the + Secret Provider Class configuration. This is a common requirement across all + CSI providers as the Secrets Store CSI Driver architecture requires specific + mapping of secrets to their mounted file locations. + + diff --git a/docs/integrations/platforms/kubernetes.mdx b/docs/integrations/platforms/kubernetes.mdx deleted file mode 100644 index c39041375..000000000 --- a/docs/integrations/platforms/kubernetes.mdx +++ /dev/null @@ -1,1071 +0,0 @@ ---- -title: "Kubernetes Operator" -description: "How to use Infisical to inject secrets into Kubernetes clusters." ---- - -![title](../../images/k8-diagram.png) - -The Infisical Secrets Operator is a Kubernetes controller that retrieves secrets from Infisical and stores them in a designated cluster. -It uses an `InfisicalSecret` resource to specify authentication and storage methods. -The operator continuously updates secrets and can also reload dependent deployments automatically. - - - If you are already using the External Secrets operator, you can view the - integration documentation for it - [here](https://external-secrets.io/latest/provider/infisical/). - - -## Install Operator - -The operator can be install via [Helm](https://helm.sh) or [kubectl](https://github.com/kubernetes/kubectl) - - - - **Install the latest Infisical Helm repository** - ```bash - helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/' - - helm repo update - ``` - - **Install the Helm chart** - - To select a specific version, view the application versions [here](https://hub.docker.com/r/infisical/kubernetes-operator/tags) and chart versions [here](https://cloudsmith.io/~infisical/repos/helm-charts/packages/detail/helm/secrets-operator/#versions) - - ```bash - helm install --generate-name infisical-helm-charts/secrets-operator - ``` - - ```bash - # Example installing app version v0.2.0 and chart version 0.1.4 - helm install --generate-name infisical-helm-charts/secrets-operator --version=0.1.4 --set controllerManager.manager.image.tag=v0.2.0 - ``` - - - - For production deployments, it is highly recommended to set the version of the Kubernetes operator manually instead of pointing to the latest version. - Doing so will help you avoid accidental updates to the newest release which may introduce unintended breaking changes. View all application versions [here](https://hub.docker.com/r/infisical/kubernetes-operator/tags). - -The command below will install the most recent version of the Kubernetes operator. -However, to set the version manually, download the manifest and set the image tag version of `infisical/kubernetes-operator` according to your desired version. - -Once you apply the manifest, the operator will be installed in `infisical-operator-system` namespace. - - ``` - kubectl apply -f https://raw.githubusercontent.com/Infisical/infisical/main/k8-operator/kubectl-install/install-secrets-operator.yaml - ``` - - - - -## Sync Infisical Secrets to your cluster - -Once you have installed the operator to your cluster, you'll need to create a `InfisicalSecret` custom resource definition (CRD). - -```yaml example-infisical-secret-crd.yaml -apiVersion: secrets.infisical.com/v1alpha1 -kind: InfisicalSecret -metadata: - name: infisicalsecret-sample - labels: - label-to-be-passed-to-managed-secret: sample-value - annotations: - example.com/annotation-to-be-passed-to-managed-secret: "sample-value" -spec: - hostAPI: https://app.infisical.com/api - resyncInterval: 10 - authentication: - # Make sure to only have 1 authentication method defined, serviceToken/universalAuth. - # If you have multiple authentication methods defined, it may cause issues. - - # (Deprecated) Service Token Auth - serviceToken: - serviceTokenSecretReference: - secretName: service-token - secretNamespace: default - secretsScope: - envSlug: - secretsPath: - recursive: true - - # Universal Auth - universalAuth: - secretsScope: - projectSlug: new-ob-em - envSlug: dev # "dev", "staging", "prod", etc.. - secretsPath: "/" # Root is "/" - recursive: true # Wether or not to use recursive mode (Fetches all secrets in an environment from a given secret path, and all folders inside the path) / defaults to false - credentialsRef: - secretName: universal-auth-credentials - secretNamespace: default - - # Native Kubernetes Auth - kubernetesAuth: - identityId: - serviceAccountRef: - name: - namespace: - - # secretsScope is identical to the secrets scope in the universalAuth field in this sample. - secretsScope: - projectSlug: your-project-slug - envSlug: prod - secretsPath: "/path" - recursive: true - - # AWS IAM Auth - awsIamAuth: - identityId: - - # secretsScope is identical to the secrets scope in the universalAuth field in this sample. - secretsScope: - projectSlug: your-project-slug - envSlug: prod - secretsPath: "/path" - recursive: true - - # Azure Auth - azureAuth: - identityId: - resource: https://management.azure.com/&client_id=CLIENT_ID # (Optional) This is the Azure resource that you want to access. For example, "https://management.azure.com/". If no value is provided, it will default to "https://management.azure.com/" - - # secretsScope is identical to the secrets scope in the universalAuth field in this sample. - secretsScope: - projectSlug: your-project-slug - envSlug: prod - secretsPath: "/path" - recursive: true - - # GCP ID Token Auth - gcpIdTokenAuth: - identityId: - - # secretsScope is identical to the secrets scope in the universalAuth field in this sample. - secretsScope: - projectSlug: your-project-slug - envSlug: prod - secretsPath: "/path" - recursive: true - - # GCP IAM Auth - gcpIamAuth: - identityId: - - # secretsScope is identical to the secrets scope in the universalAuth field in this sample. - secretsScope: - projectSlug: your-project-slug - envSlug: prod - secretsPath: "/path" - recursive: true - - managedSecretReference: - secretName: managed-secret - secretNamespace: default - creationPolicy: "Orphan" ## Owner | Orphan - # secretType: kubernetes.io/dockerconfigjson -``` - -### InfisicalSecret CRD properties - - - If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to - ` https://your-self-hosted-instace.com/api` - -When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud. - - - If you have installed your Infisical instance within the same cluster as the Infisical operator, you can optionally access the Infisical backend's service directly without having to route through the public internet. - To achieve this, use the following address for the hostAPI field: - - ``` bash - http://..svc.cluster.local:4000/api - ``` - - Make sure to replace `` and `` with the appropriate values for your backend service and namespace. - - - - - - This property defines the time in seconds between each secret re-sync from - Infisical. Shorter time between re-syncs will require higher rate limits only - available on paid plans. Default re-sync interval is every 1 minute. - - - - This block defines the TLS settings to use for connecting to the Infisical - instance. - - - - This block defines the reference to the CA certificate to use for connecting - to the Infisical instance with SSL/TLS. - - - - The name of the Kubernetes secret containing the CA certificate to use for - connecting to the Infisical instance with SSL/TLS. - - - - The namespace of the Kubernetes secret containing the CA certificate to use - for connecting to the Infisical instance with SSL/TLS. - - - - The name of the key in the Kubernetes secret which contains the value of the - CA certificate to use for connecting to the Infisical instance with SSL/TLS. - - - - This block defines the method that will be used to authenticate with Infisical - so that secrets can be fetched - - - - The universal machine identity authentication method is used to authenticate with Infisical. The client ID and client secret needs to be stored in a Kubernetes secret. This block defines the reference to the name and namespace of secret that stores these credentials. - - - - You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about machine identities here](/documentation/platform/identities/universal-auth). - - - Once you have created your machine identity and added it to your project(s), you will need to create a Kubernetes secret containing the identity credentials. - To quickly create a Kubernetes secret containing the identity credentials, you can run the command below. - - Make sure you replace `` with the identity client ID and `` with the identity client secret. - - ``` bash - kubectl create secret generic universal-auth-credentials --from-literal=clientId="" --from-literal=clientSecret="" - ``` - - - - Once the secret is created, add the `secretName` and `secretNamespace` of the secret that was just created under `authentication.universalAuth.credentialsRef` field in the InfisicalSecret resource. - - - - - - Make sure to also populate the `secretsScope` field with the project slug - _`projectSlug`_, environment slug _`envSlug`_, and secrets path - _`secretsPath`_ that you want to fetch secrets from. Please see the example - below. - - -## Example - -```yaml -apiVersion: secrets.infisical.com/v1alpha1 -kind: InfisicalSecret -metadata: - name: infisicalsecret-sample-crd -spec: - authentication: - universalAuth: - secretsScope: - projectSlug: # <-- project slug - envSlug: # "dev", "staging", "prod", etc.. - secretsPath: "" # Root is "/" - credentialsRef: - secretName: universal-auth-credentials # <-- name of the Kubernetes secret that stores our machine identity credentials - secretNamespace: default # <-- namespace of the Kubernetes secret that stores our machine identity credentials - ... -``` - - - - - The Kubernetes machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within a Kubernetes environment. - - - - 1.1. Start by creating a service account in your Kubernetes cluster that will be used by Infisical to authenticate with the Kubernetes API Server. - - ```yaml infisical-service-account.yaml - apiVersion: v1 - kind: ServiceAccount - metadata: - name: infisical-auth - namespace: default - - ``` - - ``` - kubectl apply -f infisical-service-account.yaml - ``` - - 1.2. Bind the service account to the `system:auth-delegator` cluster role. As described [here](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#other-component-roles), this role allows delegated authentication and authorization checks, specifically for Infisical to access the [TokenReview API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/). You can apply the following configuration file: - - ```yaml cluster-role-binding.yaml - apiVersion: rbac.authorization.k8s.io/v1 - kind: ClusterRoleBinding - metadata: - name: role-tokenreview-binding - namespace: default - roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: system:auth-delegator - subjects: - - kind: ServiceAccount - name: infisical-auth - namespace: default - ``` - - ``` - kubectl apply -f cluster-role-binding.yaml - ``` - - 1.3. Next, create a long-lived service account JWT token (i.e. the token reviewer JWT token) for the service account using this configuration file for a new `Secret` resource: - - ```yaml service-account-token.yaml - apiVersion: v1 - kind: Secret - type: kubernetes.io/service-account-token - metadata: - name: infisical-auth-token - annotations: - kubernetes.io/service-account.name: "infisical-auth" - ``` - - - ``` - kubectl apply -f service-account-token.yaml - ``` - - 1.4. Link the secret in step 1.3 to the service account in step 1.1: - - ```bash - kubectl patch serviceaccount infisical-auth -p '{"secrets": [{"name": "infisical-auth-token"}]}' -n default - ``` - - 1.5. Finally, retrieve the token reviewer JWT token from the secret. - - ```bash - kubectl get secret infisical-auth-token -n default -o=jsonpath='{.data.token}' | base64 --decode - ``` - - Keep this JWT token handy as you will need it for the **Token Reviewer JWT** field when configuring the Kubernetes Auth authentication method for the identity in step 2. - - - - - To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. - - ![identities organization](/images/platform/identities/identities-org.png) - - When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles. - - ![identities organization create](/images/platform/identities/identities-org-create.png) - - Now input a few details for your new identity. Here's some guidance for each field: - - - Name (required): A friendly name for the identity. - - Role (required): A role from the **Organization Roles** tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to. - - Once you've created an identity, you'll be prompted to configure the authentication method for it. Here, select **Kubernetes Auth**. - - - To learn more about each field of the Kubernetes native authentication method, see step 2 of [guide](/documentation/platform/identities/kubernetes-auth#guide). - - - ![identities organization create auth method](/images/platform/identities/identities-org-create-kubernetes-auth-method.png) - - - - - To allow the operator to use the given identity to access secrets, you will need to add the identity to project(s) that you would like to grant it access to. - - To do this, head over to the project you want to add the identity to and go to Project Settings > Access Control > Machine Identities and press **Add identity**. - - Next, select the identity you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this identity can have access to. - - ![identities project](/images/platform/identities/identities-project.png) - - ![identities project create](/images/platform/identities/identities-project-create.png) - - - - Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource. - In the `authentication.kubernetesAuth.identityId` field, add the identity ID of the machine identity you created. - See the example below for more details. - - - Add the service account details from the previous steps under `authentication.kubernetesAuth.serviceAccountRef`. - Here you will need to enter the name and namespace of the service account. - The example below shows a complete InfisicalSecret resource with all required fields defined. - - - - - - Make sure to also populate the `secretsScope` field with the project slug - _`projectSlug`_, environment slug _`envSlug`_, and secrets path - _`secretsPath`_ that you want to fetch secrets from. Please see the example - below. - - -## Example - -```yaml example-kubernetes-auth.yaml -apiVersion: secrets.infisical.com/v1alpha1 -kind: InfisicalSecret -metadata: - name: infisicalsecret-sample-crd -spec: - authentication: - kubernetesAuth: - identityId: - serviceAccountRef: - name: - namespace: - - # secretsScope is identical to the secrets scope in the universalAuth field in this sample. - secretsScope: - projectSlug: your-project-slug - envSlug: prod - secretsPath: "/path" - recursive: true - ... -``` - - - - - The AWS IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within an AWS environment like an EC2 or a Lambda function. - - - - You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about AWS machine identities here](/documentation/platform/identities/aws-auth). - - - Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource. In the `authentication.awsIamAuth.identityId` field, add the identity ID of the machine identity you created. See the example below for more details. - - - - - - Make sure to also populate the `secretsScope` field with the project slug - _`projectSlug`_, environment slug _`envSlug`_, and secrets path - _`secretsPath`_ that you want to fetch secrets from. Please see the example - below. - - -## Example - -```yaml example-aws-iam-auth.yaml -apiVersion: secrets.infisical.com/v1alpha1 -kind: InfisicalSecret -metadata: - name: infisicalsecret-sample-crd -spec: - authentication: - awsIamAuth: - identityId: - - # secretsScope is identical to the secrets scope in the universalAuth field in this sample. - secretsScope: - projectSlug: your-project-slug - envSlug: prod - secretsPath: "/path" - recursive: true - ... -``` - - - - - The Azure machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within an Azure environment. - - - - You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about Azure machine identities here](/documentation/platform/identities/azure-auth). - - - Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource. In the `authentication.azureAuth.identityId` field, add the identity ID of the machine identity you created. See the example below for more details. - - - - - - Make sure to also populate the `secretsScope` field with the project slug - _`projectSlug`_, environment slug _`envSlug`_, and secrets path - _`secretsPath`_ that you want to fetch secrets from. Please see the example - below. - - -## Example - -```yaml example-azure-auth.yaml -apiVersion: secrets.infisical.com/v1alpha1 -kind: InfisicalSecret -metadata: - name: infisicalsecret-sample-crd -spec: - authentication: - azureAuth: - identityId: - - # secretsScope is identical to the secrets scope in the universalAuth field in this sample. - secretsScope: - projectSlug: your-project-slug - envSlug: prod - secretsPath: "/path" - recursive: true - ... -``` - - - - - The GCP ID Token machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within GCP environments. - - - - You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about GCP machine identities here](/documentation/platform/identities/gcp-auth). - - - Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource. In the `authentication.gcpIdTokenAuth.identityId` field, add the identity ID of the machine identity you created. See the example below for more details. - - - - - - Make sure to also populate the `secretsScope` field with the project slug - _`projectSlug`_, environment slug _`envSlug`_, and secrets path - _`secretsPath`_ that you want to fetch secrets from. Please see the example - below. - - -## Example - -```yaml example-gcp-id-token-auth.yaml -apiVersion: secrets.infisical.com/v1alpha1 -kind: InfisicalSecret -metadata: - name: infisicalsecret-sample-crd -spec: - authentication: - gcpIdTokenAuth: - identityId: - - # secretsScope is identical to the secrets scope in the universalAuth field in this sample. - secretsScope: - projectSlug: your-project-slug - envSlug: prod - secretsPath: "/path" - recursive: true - ... -``` - - - - - The GCP IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used both within and outside GCP environments. - - - - You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about GCP machine identities here](/documentation/platform/identities/gcp-auth). - - - Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource. In the `authentication.gcpIamAuth.identityId` field, add the identity ID of the machine identity you created. - You'll also need to add the service account key file path to your InfisicalSecret resource. In the `authentication.gcpIamAuth.serviceAccountKeyFilePath` field, add the path to your service account key file path. Please see the example below for more details. - - - - - - Make sure to also populate the `secretsScope` field with the project slug - _`projectSlug`_, environment slug _`envSlug`_, and secrets path - _`secretsPath`_ that you want to fetch secrets from. Please see the example - below. - - -## Example - -```yaml example-gcp-id-token-auth.yaml -apiVersion: secrets.infisical.com/v1alpha1 -kind: InfisicalSecret -metadata: - name: infisicalsecret-sample-crd -spec: - authentication: - gcpIamAuth: - identityId: - serviceAccountKeyFilePath: "/path/to-service-account-key-file-path.json" - - # secretsScope is identical to the secrets scope in the universalAuth field in this sample. - secretsScope: - projectSlug: your-project-slug - envSlug: prod - secretsPath: "/path" - recursive: true - ... -``` - - - - - -The service token required to authenticate with Infisical needs to be stored in a Kubernetes secret. This block defines the reference to the name and namespace of secret that stores this service token. -Follow the instructions below to create and store the service token in a Kubernetes secrets and reference it in your CRD. - -#### 1. Generate service token - -You can generate a [service token](../../documentation/platform/token) for an Infisical project by heading over to the Infisical dashboard then to Project Settings. - -#### 2. Create Kubernetes secret containing service token - -Once you have generated the service token, you will need to create a Kubernetes secret containing the service token you generated. -To quickly create a Kubernetes secret containing the generated service token, you can run the command below. Make sure you replace `` with your service token. - -```bash -kubectl create secret generic service-token --from-literal=infisicalToken="" -``` - -#### 3. Add reference for the Kubernetes secret containing service token - -Once the secret is created, add the name and namespace of the secret that was just created under `authentication.serviceToken.serviceTokenSecretReference` field in the InfisicalSecret resource. - -{" "} - - - Make sure to also populate the `secretsScope` field with the, environment slug - _`envSlug`_, and secrets path _`secretsPath`_ that you want to fetch secrets - from. Please see the example below. - - -## Example - -```yaml -apiVersion: secrets.infisical.com/v1alpha1 -kind: InfisicalSecret -metadata: - name: infisicalsecret-sample-crd -spec: - authentication: - serviceToken: - serviceTokenSecretReference: - secretName: service-token # <-- name of the Kubernetes secret that stores our service token - secretNamespace: option # <-- namespace of the Kubernetes secret that stores our service token - secretsScope: - envSlug: # "dev", "staging", "prod", etc.. - secretsPath: # Root is "/" - ... -``` - - - - -The `managedSecretReference` field is used to define the target location for storing secrets retrieved from an Infisical project. -This field requires specifying both the name and namespace of the Kubernetes secret that will hold these secrets. -The Infisical operator will automatically create the Kubernetes secret with the specified name/namespace and keep it continuously updated. - -Note: The managed secret be should be created in the same namespace as the deployment that will use it. - - - -The name of the managed Kubernetes secret to be created - - -The namespace of the managed Kubernetes secret to be created. - - -Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets. - - -Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator. -This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically. - -#### Available options - -- `Orphan` (default) -- `Owner` - - - When creation policy is set to `Owner`, the `InfisicalSecret` CRD must be in - the same namespace as where the managed kubernetes secret. - - - - -### Propagating labels & annotations - -The operator will transfer all labels & annotations present on the `InfisicalSecret` CRD to the managed Kubernetes secret to be created. -Thus, if a specific label is required on the resulting secret, it can be applied as demonstrated in the following example: - - -```yaml -apiVersion: secrets.infisical.com/v1alpha1 -kind: InfisicalSecret -metadata: - name: infisicalsecret-sample - labels: - label-to-be-passed-to-managed-secret: sample-value - annotations: - example.com/annotation-to-be-passed-to-managed-secret: "sample-value" -spec: - .. - authentication: - ... - managedSecretReference: - ... -``` - -This would result in the following managed secret to be created: - -```yaml -apiVersion: v1 -data: ... -kind: Secret -metadata: - annotations: - example.com/annotation-to-be-passed-to-managed-secret: sample-value - secrets.infisical.com/version: W/"3f1-ZyOSsrCLGSkAhhCkY2USPu2ivRw" - labels: - label-to-be-passed-to-managed-secret: sample-value - name: managed-token - namespace: default -type: Opaque -``` - - - -### Apply the Infisical CRD to your cluster - -Once you have configured the Infisical CRD with the required fields, you can apply it to your cluster. -After applying, you should notice that the managed secret has been created in the desired namespace your specified. - -``` -kubectl apply -f example-infisical-secret-crd.yaml -``` - -### Verify managed secret creation - -To verify that the operator has successfully created the managed secret, you can check the secrets in the namespace that was specified. - -```bash -# Verify managed secret is created -kubectl get secrets -n -``` - - - The Infisical secrets will be synced and stored into the managed secret every - 1 minutes. - - -### Using managed secret in your deployment - -Incorporating the managed secret created by the operator into your deployment can be achieved through several methods. -Here, we will highlight three of the most common ways to utilize it. Learn more about Kubernetes secrets [here](https://kubernetes.io/docs/concepts/configuration/secret/) - - - This will take all the secrets from your managed secret and expose them to your container - -````yaml - envFrom: - - secretRef: - name: managed-secret # managed secret name - ``` - - Example usage in a deployment - ```yaml - apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx-deployment - labels: - app: nginx -spec: - replicas: 1 - selector: - matchLabels: - app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - name: nginx - image: nginx:1.14.2 - envFrom: - - secretRef: - name: managed-secret # <- name of managed secret - ports: - - containerPort: 80 -```` - - - - - This will allow you to select individual secrets by key name from your managed secret and expose them to your container - - ```yaml - env: - - name: SECRET_NAME # The environment variable's name which is made available in the container - valueFrom: - secretKeyRef: - name: managed-secret # managed secret name - key: SOME_SECRET_KEY # The name of the key which exists in the managed secret - ``` - -Example usage in a deployment - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: -name: nginx-deployment -labels: -app: nginx -spec: -replicas: 1 -selector: -matchLabels: -app: nginx -template: -metadata: -labels: -app: nginx -spec: -containers: - name: nginx -image: nginx:1.14.2 -env: - name: STRIPE_API_SECRET -valueFrom: -secretKeyRef: -name: managed-secret # <- name of managed secret -key: STRIPE_API_SECRET -ports: - containerPort: 80 - -``` - - - - -This will allow you to create a volume on your container which comprises of files holding the secrets in your managed kubernetes secret -```yaml -volumes: - - name: secrets-volume-name # The name of the volume under which secrets will be stored - secret: - secretName: managed-secret # managed secret name -```` - -You can then mount this volume to the container's filesystem so that your deployment can access the files containing the managed secrets - -```yaml -volumeMounts: - - name: secrets-volume-name - mountPath: /etc/secrets - readOnly: true -``` - -Example usage in a deployment - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx-deployment - labels: - app: nginx -spec: - replicas: 1 - selector: - matchLabels: - app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - name: nginx - image: nginx:1.14.2 - volumeMounts: - - name: secrets-volume-name - mountPath: /etc/secrets - readOnly: true - ports: - - containerPort: 80 - volumes: - - name: secrets-volume-name - secret: - secretName: managed-secret # <- managed secrets -``` - - - -### Connecting to instances with private/self-signed certificate - -To connect to Infisical instances behind a private/self-signed certificate, you can configure the TLS settings in the `InfisicalSecret` CRD -to point to a CA certificate stored in a Kubernetes secret resource. - -```yaml ---- -spec: - hostAPI: https://app.infisical.com/api - resyncInterval: 10 - tls: - caRef: - secretName: custom-ca-certificate - secretNamespace: default - key: ca.crt - authentication: ---- -``` - -The definition file of the Kubernetes secret for the CA certificate can be structured like the following: - -```yaml -apiVersion: v1 -kind: Secret -metadata: - name: custom-ca-certificate -type: Opaque -stringData: - ca.crt: | - -----BEGIN CERTIFICATE----- - MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL - ... - BQAwDTELMAkGA1UEChMCUEgwHhcNMjQxMDI1MTU0MjAzWhcNMjUxMDI1MjE0MjAz - -----END CERTIFICATE----- -``` - -## Auto redeployment - -Deployments using managed secrets don't reload automatically on updates, so they may use outdated secrets unless manually redeployed. -To address this, we added functionality to automatically redeploy your deployment when its managed secret updates. - -### Enabling auto redeploy - -To enable auto redeployment you simply have to add the following annotation to the deployment that consumes a managed secret - -```yaml -secrets.infisical.com/auto-reload: "true" -``` - - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx-deployment - labels: - app: nginx - annotations: - secrets.infisical.com/auto-reload: "true" # <- redeployment annotation -spec: - replicas: 1 - selector: - matchLabels: - app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - name: nginx - image: nginx:1.14.2 - envFrom: - - secretRef: - name: managed-secret - ports: - - containerPort: 80 -``` - - - #### How it works - When a secret change occurs, the operator will check to see which deployments are using the operator-managed Kubernetes secret that received the update. - Then, for each deployment that has this annotation present, a rolling update will be triggered. - -## Global configuration - -To configure global settings that will apply to all instances of `InfisicalSecret`, you can define these configurations in a Kubernetes ConfigMap. -For example, you can configure all `InfisicalSecret` instances to fetch secrets from a single backend API without specifying the `hostAPI` parameter for each instance. - -### Available global properties - -| Property | Description | Default value | -| -------- | --------------------------------------------------------------------------------- | ----------------------------- | -| hostAPI | If `hostAPI` in `InfisicalSecret` instance is left empty, this value will be used | https://app.infisical.com/api | - -### Applying global configurations - -All global configurations must reside in a Kubernetes ConfigMap named `infisical-config` in the namespace `infisical-operator-system`. -To apply global configuration to the operator, copy the following yaml into `infisical-config.yaml` file. - -```yaml infisical-config.yaml -apiVersion: v1 -kind: Namespace -metadata: - name: infisical-operator-system ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: infisical-config - namespace: infisical-operator-system -data: - hostAPI: https://example.com/api # <-- global hostAPI -``` - -Then apply this change via kubectl by running the following - -```bash -kubectl apply -f infisical-config.yaml -``` - -## Troubleshoot operator - -If the operator is unable to fetch secrets from the API, it will not affect the managed Kubernetes secret. -It will continue attempting to reconnect to the API indefinitely. -The InfisicalSecret resource uses the `status.conditions` field to report its current state and any errors encountered. - -```yaml -$ kubectl get infisicalSecrets -NAME AGE -infisicalsecret-sample 12s - -$ kubectl describe infisicalSecret infisicalsecret-sample -... -Spec: -... -Status: - Conditions: - Last Transition Time: 2022-12-18T04:29:09Z - Message: Infisical controller has located the Infisical token in provided Kubernetes secret - Reason: OK - Status: True - Type: secrets.infisical.com/LoadedInfisicalToken - Last Transition Time: 2022-12-18T04:29:10Z - Message: Failed to update secret because: 400 Bad Request - Reason: Error - Status: False - Type: secrets.infisical.com/ReadyToSyncSecrets -Events: -``` - -## Uninstall Operator - -The managed secret created by the operator will not be deleted when the operator is uninstalled. - - - - Install Infisical Helm repository - ```bash - helm uninstall - ``` - - - ``` - kubectl delete -f https://raw.githubusercontent.com/Infisical/infisical/main/k8-operator/kubectl-install/install-secrets-operator.yaml - ``` - - - -## Useful Articles - -- [Managing secrets in OpenShift with Infisical](https://xphyr.net/post/infisical_ocp/) diff --git a/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx new file mode 100644 index 000000000..21f54994a --- /dev/null +++ b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx @@ -0,0 +1,473 @@ +--- +sidebarTitle: "InfisicalDynamicSecret CRD" +title: "Using the InfisicalDynamicSecret CRD" +description: "Learn how to generate dynamic secret leases in Infisical and sync them to your Kubernetes cluster." +--- + +## Overview + +The **InfisicalDynamicSecret** CRD allows you to easily create and manage dynamic secret leases in Infisical and automatically sync them to your Kubernetes cluster as native **Kubernetes Secret** resources. +This means any Pod, Deployment, or other Kubernetes resource can make use of dynamic secrets from Infisical just like any other K8s secret. + +This CRD offers the following features: + +- **Generate a dynamic secret lease** in Infisical and track its lifecycle. +- **Write** the dynamic secret from Infisical to your cluster as native Kubernetes secret. +- **Automatically rotate** the dynamic secret value before it expires to make sure your cluster always has valid credentials. +- **Optionally trigger redeployments** of any workloads that consume the secret if you enable auto-reload. + +### Prerequisites + +- A project within Infisical. +- A [machine identity](/docs/documentation/platform/identities/overview) ready for use in Infisical that has permissions to create dynamic secret leases in the project. +- You have already configured a dynamic secret in Infisical. +- The operator is installed on to your Kubernetes cluster. + +## Configure Dynamic Secret CRD + +The example below shows a sample **InfisicalDynamicSecret** CRD that creates a dynamic secret lease in Infisical, and syncs the lease to your Kubernetes cluster. + +```yaml dynamic-secret-crd.yaml +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalDynamicSecret +metadata: + name: infisicaldynamicsecret +spec: + hostAPI: https://app.infisical.com/api # Optional, defaults to https://app.infisical.com/api + + dynamicSecret: + secretName: + projectId: + secretsPath: # Root directory is / + environmentSlug: + + # Lease revocation policy defines what should happen to leases created by the operator if the CRD is deleted. + # If set to "Revoke", leases will be revoked when the InfisicalDynamicSecret CRD is deleted. + leaseRevocationPolicy: Revoke + + # Lease TTL defines how long the lease should last for the dynamic secret. + # This value must be less than 1 day, and if a max TTL is defined on the dynamic secret, it must be below the max TTL. + leaseTTL: 1m + + # A reference to the secret that the dynamic secret lease should be stored in. + # If the secret doesn't exist, it will automatically be created. + managedSecretReference: + secretName: + secretNamespace: default # Must be the same namespace as the InfisicalDynamicSecret CRD. + creationPolicy: Orphan + + # Only have one authentication method defined or you are likely to run into authentication issues. + # Remove all except one authentication method. + authentication: + awsIamAuth: + identityId: + azureAuth: + identityId: + gcpIamAuth: + identityId: + serviceAccountKeyFilePath: + gcpIdTokenAuth: + identityId: + kubernetesAuth: + identityId: + serviceAccountRef: + name: + namespace: + universalAuth: + credentialsRef: + secretName: # universal-auth-credentials + secretNamespace: # default +``` + +Apply the InfisicalDynamicSecret CRD to your cluster. + +```bash +kubectl apply -f dynamic-secret-crd.yaml +``` + +After applying the InfisicalDynamicSecret CRD, you should notice that the dynamic secret lease has been created in Infisical and synced to your Kubernetes cluster. You can verify that the lease has been created by doing: + +```bash +kubectl get secret -o yaml +``` + +After getting the secret, you should should see that the secret has data that contains the lease credentials. + +```yaml +apiVersion: v1 +data: + DB_PASSWORD: VHhETjZ4c2xsTXpOSWdPYW5LLlRyNEc2alVKYml6WiQjQS0tNTdodyREM3ZLZWtYSi4hTkdyS0F+TVFsLU9CSA== + DB_USERNAME: cHg4Z0dJTUVBcHdtTW1aYnV3ZWRsekJRRll6cW4wFEE= +kind: Secret +# ..... +``` + +### InfisicalDynamicSecret CRD properties + + + If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to + ` https://your-self-hosted-instace.com/api` + +When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud. + + + If you have installed your Infisical instance within the same cluster as the Infisical operator, you can optionally access the Infisical backend's service directly without having to route through the public internet. + To achieve this, use the following address for the hostAPI field: + + ``` bash + http://..svc.cluster.local:4000/api + ``` + + Make sure to replace `` and `` with the appropriate values for your backend service and namespace. + + + + + + The `leaseTTL` is a string-formatted duration that defines the time the lease should last for the dynamic secret. + + The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time. + + The following units are supported: + + - `s` for seconds (must be at least 5 seconds) + - `m` for minutes + - `h` for hours + - `d` for days + + + The lease duration at most be 1 day (24 hours). And the TTL must be less than the max TTL defined on the dynamic secret. + + + + + The `managedSecretReference` field is used to define the Kubernetes secret where the dynamic secret lease should be stored. The required fields are `secretName` and `secretNamespace`. + +```yaml +spec: + managedSecretReference: + secretName: + secretNamespace: default +``` + +{" "} + + + The name of the Kubernetes secret where the dynamic secret lease should be + stored. + + +{" "} + + + The namespace of the Kubernetes secret where the dynamic secret lease should + be stored. + + + + Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator. + This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically. + + #### Available options + - `Orphan` (default) + - `Owner` + + + When creation policy is set to `Owner`, the `InfisicalSecret` CRD must be in + the same namespace as where the managed kubernetes secret. + + + This field is optional. + + + + + Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets. + + This field is optional. + + + + + + + +The field is optional and will default to `None` if not defined. + +The lease revocation policy defines what the operator should do with the leases created by the operator, when the InfisicalDynamicSecret CRD is deleted. + +Valid values are `None` and `Revoke`. + +Behavior of each policy: + +- `None`: The operator will not override existing secrets in Infisical. If a secret with the same key already exists, the operator will skip pushing that secret, and the secret will not be managed by the operator. +- `Revoke`: The operator will revoke the leases created by the operator when the InfisicalDynamicSecret CRD is deleted. + +```yaml +spec: + leaseRevocationPolicy: Revoke +``` + + + + + The `dynamicSecret` field is used to specify which dynamic secret to create leases for. The required fields are `secretName`, `projectId`, `secretsPath`, and `environmentSlug`. + + ```yaml + spec: + dynamicSecret: + secretName: + projectId: + environmentSlug: + secretsPath: + ``` + +{" "} + + + The name of the dynamic secret. + + +{" "} + + + The project ID of where the dynamic secret is stored in Infisical. + + +{" "} + + + The environment slug of where the dynamic secret is stored in Infisical. + + +{" "} + + + The path of where the dynamic secret is stored in Infisical. The root path is + `/`. + + + + + + +The `authentication` field dictates which authentication method to use when pushing secrets to Infisical. +The available authentication methods are `universalAuth`, `kubernetesAuth`, `awsIamAuth`, `azureAuth`, `gcpIdTokenAuth`, and `gcpIamAuth`. + + + The universal authentication method is one of the easiest ways to get started with Infisical. Universal Auth works anywhere and is not tied to any specific cloud provider. + [Read more about Universal Auth](/documentation/platform/identities/universal-auth). + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + - `credentialsRef`: The name and namespace of the Kubernetes secret that stores the service token. + - `credentialsRef.secretName`: The name of the Kubernetes secret. + - `credentialsRef.secretNamespace`: The namespace of the Kubernetes secret. + + + Example: + + ```yaml + # infisical-push-secret.yaml + spec: + universalAuth: + credentialsRef: + secretName: + secretNamespace: + ``` + + ```yaml + # machine-identity-credentials.yaml + apiVersion: v1 + kind: Secret + metadata: + name: universal-auth-credentials + type: Opaque + stringData: + clientId: + clientSecret: + ``` + + + + The Kubernetes machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within a Kubernetes environment. + [Read more about Kubernetes Auth](/documentation/platform/identities/kubernetes-auth). + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + - `serviceAccountRef`: The name and namespace of the service account that will be used to authenticate with Infisical. + - `serviceAccountRef.name`: The name of the service account. + - `serviceAccountRef.namespace`: The namespace of the service account. + - `autoCreateServiceAccountToken`: If set to `true`, the operator will automatically create a short-lived service account token on-demand for the service account. Defaults to `false`. + - `serviceAccountTokenAudiences`: Optionally specify audience for the service account token. This field is only relevant if you have set `autoCreateServiceAccountToken` to `true`. No audience is specified by default. + + + Example: + + ```yaml + spec: + kubernetesAuth: + identityId: + autoCreateServiceAccountToken: true # Automatically creates short-lived service account tokens for the service account. + serviceAccountTokenAudiences: + - # Optionally specify audience for the service account token. No audience is specified by default. + serviceAccountRef: + name: + namespace: + ``` + + + + + The AWS IAM machine identity authentication method is used to authenticate with Infisical. + [Read more about AWS IAM Auth](/documentation/platform/identities/aws-auth). + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + + Example: + + ```yaml + spec: + authentication: + awsIamAuth: + identityId: + ``` + + + + The AWS IAM machine identity authentication method is used to authenticate with Infisical. Azure Auth can only be used from within an Azure environment. + [Read more about Azure Auth](/documentation/platform/identities/azure-auth). + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + + Example: + + ```yaml + spec: + authentication: + azureAuth: + identityId: + ``` + + + + The GCP IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used both within and outside GCP environments. + [Read more about Azure Auth](/documentation/platform/identities/gcp-auth). + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + - `serviceAccountKeyFilePath`: The path to the GCP service account key file. + + Example: + + ```yaml + spec: + gcpIamAuth: + identityId: + serviceAccountKeyFilePath: + ``` + + + + The GCP ID Token machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within GCP environments. + [Read more about Azure Auth](/documentation/platform/identities/gcp-auth). + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + + Example: + + ```yaml + spec: + gcpIdTokenAuth: + identityId: + ``` + + + + + + + This block defines the TLS settings to use for connecting to the Infisical + instance. + + Fields: + + This block defines the reference to the CA certificate to use for connecting to the Infisical instance with SSL/TLS. + + Valid fields: + - `secretName`: The name of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS. + - `secretNamespace`: The namespace of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS. + - `key`: The name of the key in the Kubernetes secret which contains the value of the CA certificate to use for connecting to the Infisical instance with SSL/TLS. + + Example: + + ```yaml + tls: + caRef: + secretName: custom-ca-certificate + secretNamespace: default + key: ca.crt + ``` + + + + + +### Applying the InfisicalDynamicSecret CRD to your cluster + +Once you have configured the `InfisicalDynamicSecret` CRD with the required fields, you can apply it to your cluster. After applying, you should notice that a lease has been created in Infisical and synced to your Kubernetes cluster. + +```bash +kubectl apply -f dynamic-secret-crd.yaml +``` + +## Auto redeployment + +Deployments referring to Kubernetes secrets containing Infisical dynamic secrets don't automatically reload when the dynamic secret lease expires. This means your deployment may use expired dynamic secrets unless manually redeployed. +To address this, we've added functionality to automatically redeploy your deployment when the associated Kubernetes secret containing your Infisical dynamic secret updates. + +#### Enabling auto redeploy + +To enable auto redeployment you simply have to add the following annotation to the deployment, statefulset, or daemonset that consumes a managed secret. + +```yaml +secrets.infisical.com/auto-reload: "true" +``` + + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx + annotations: + secrets.infisical.com/auto-reload: "true" # <- redeployment annotation +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + envFrom: + - secretRef: + name: managed-secret # The name of your managed secret, the same that you're using in your InfisicalDynamicSecret CRD (spec.managedSecretReference.secretName) + ports: + - containerPort: 80 +``` + + + #### How it works + When the lease changes, the operator will check to see which deployments are using the operator-managed Kubernetes secret that received the update. + Then, for each deployment that has this annotation present, a rolling update will be triggered. A redeployment won't happen if the lease is renewed, only if it's recreated. + diff --git a/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx new file mode 100644 index 000000000..50f07bb76 --- /dev/null +++ b/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx @@ -0,0 +1,470 @@ +--- +sidebarTitle: "InfisicalPushSecret CRD" +title: "Using the InfisicalPushSecret CRD" +description: "Learn how to use the InfisicalPushSecret CRD to push and manage secrets in Infisical." +--- + + +## Overview + +The **InfisicalPushSecret** CRD allows you to create secrets in your Kubernetes cluster and push them to Infisical. + + +This CRD offers the following features: +- **Push Secrets** from a Kubernetes secret into Infisical. +- **Manage secret lifecycle** of pushed secrets in Infisical. When the Kubernetes secret is updated, the operator will automatically update the secrets in Infisical. Optionally, when the Kubernetes secret is deleted, the operator will delete the secrets in Infisical automatically. + +### Prerequisites + +- A project within Infisical. +- A [machine identity](/docs/documentation/platform/identities/overview) ready for use in Infisical that has permissions to create secrets in your project. +- The operator is installed on to your Kubernetes cluster. + +## Example usage + +Below is a sample InfisicalPushSecret CRD that pushes secrets defined in a Kubernetes secret to Infisical. + +After filling out the fields in the InfisicalPushSecret CRD, you can apply it directly to your cluster. + +Before applying the InfisicalPushSecret CRD, you need to create a Kubernetes secret containing the secrets you want to push to Infisical. An example can be seen below the InfisicalPushSecret CRD. + +```yaml infisical-push-secret.yaml + apiVersion: secrets.infisical.com/v1alpha1 + kind: InfisicalPushSecret + metadata: + name: infisical-push-secret-demo + spec: + resyncInterval: 1m + hostAPI: https://app.infisical.com/api + + # Optional, defaults to no replacement. + updatePolicy: Replace # If set to replace, existing secrets inside Infisical will be replaced by the value of the PushSecret on sync. + + # Optional, defaults to no deletion. + deletionPolicy: Delete # If set to delete, the secret(s) inside Infisical managed by the operator, will be deleted if the InfisicalPushSecret CRD is deleted. + + destination: + projectId: + environmentSlug: + secretsPath: + + push: + secret: + secretName: push-secret-demo # Secret CRD + secretNamespace: default + + # Only have one authentication method defined or you are likely to run into authentication issues. + # Remove all except one authentication method. + authentication: + awsIamAuth: + identityId: + azureAuth: + identityId: + gcpIamAuth: + identityId: + serviceAccountKeyFilePath: + gcpIdTokenAuth: + identityId: + kubernetesAuth: + identityId: + serviceAccountRef: + name: + namespace: + universalAuth: + credentialsRef: + secretName: # universal-auth-credentials + secretNamespace: # default +``` + +```yaml source-secret.yaml + apiVersion: v1 + kind: Secret + metadata: + name: push-secret-demo + namespace: default + stringData: # can also be "data", but needs to be base64 encoded + API_KEY: some-api-key + DATABASE_URL: postgres://127.0.0.1:5432 + ENCRYPTION_KEY: fabcc12-a22-facbaa4-11aa568aab +``` + +```bash + kubectl apply -f source-secret.yaml +``` + +After applying the soruce-secret.yaml file, you are ready to apply the InfisicalPushSecret CRD. + +```bash + kubectl apply -f infisical-push-secret.yaml +``` + +After applying the InfisicalPushSecret CRD, you should notice that the secrets you have defined in your source-secret.yaml file have been pushed to your specified destination in Infisical. + + +## InfisicalPushSecret CRD properties + + + If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to + ` https://your-self-hosted-instace.com/api` + + When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud. + + + If you have installed your Infisical instance within the same cluster as the Infisical operator, you can optionally access the Infisical backend's service directly without having to route through the public internet. + To achieve this, use the following address for the hostAPI field: + + ``` bash + http://..svc.cluster.local:4000/api + ``` + + Make sure to replace `` and `` with the appropriate values for your backend service and namespace. + + + + + + + The `resyncInterval` is a string-formatted duration that defines the time between each resync. + + The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time. + + The following units are supported: + - `s` for seconds (must be at least 5 seconds) + - `m` for minutes + - `h` for hours + - `d` for days + - `w` for weeks + + The default value is `1m` (1 minute). + + Valid intervals examples: + ```yaml + resyncInterval: 5s # 10 seconds + resyncInterval: 10s # 10 seconds + resyncInterval: 5m # 5 minutes + resyncInterval: 1h # 1 hour + resyncInterval: 1d # 1 day + ``` + + + + + The field is optional and will default to `None` if not defined. + + The update policy defines how the operator should handle conflicting secrets when pushing secrets to Infisical. + + Valid values are `None` and `Replace`. + + Behavior of each policy: + - `None`: The operator will not override existing secrets in Infisical. If a secret with the same key already exists, the operator will skip pushing that secret, and the secret will not be managed by the operator. + - `Replace`: The operator will replace existing secrets in Infisical with the new secrets. If a secret with the same key already exists, the operator will update the secret with the new value. + + ```yaml + spec: + updatePolicy: Replace + ``` + + + + + This field is optional and will default to `None` if not defined. + + The deletion policy defines what the operator should do in case the InfisicalPushSecret CRD is deleted. + + Valid values are `None` and `Delete`. + + Behavior of each policy: + - `None`: The operator will not delete the secrets in Infisical when the InfisicalPushSecret CRD is deleted. + - `Delete`: The operator will delete the secrets in Infisical that are managed by the operator when the InfisicalPushSecret CRD is deleted. + + ```yaml + spec: + deletionPolicy: Delete + ``` + + + + The `destination` field is used to specify where you want to create the secrets in Infisical. The required fields are `projectId`, `environmentSlug`, and `secretsPath`. + + ```yaml + spec: + destination: + projectId: + environmentSlug: + secretsPath: + ``` + + + The project ID where you want to create the secrets in Infisical. + + + + The environment slug where you want to create the secrets in Infisical. + + + + The path where you want to create the secrets in Infisical. The root path is `/`. + + + + + + The `push` field is used to define what you want to push to Infisical. Currently the operator only supports pushing Kubernetes secrets to Infisical. An example of the `push` field is shown below. + + + + + The `secret` field is used to define the Kubernetes secret you want to push to Infisical. The required fields are `secretName` and `secretNamespace`. + + + + Example usage of the `push.secret` field: + + ```yaml infisical-push-secret.yaml + push: + secret: + secretName: push-secret-demo + secretNamespace: default + ``` + + ```yaml push-secret-demo.yaml + apiVersion: v1 + kind: Secret + metadata: + name: push-secret-demo + namespace: default + # Pass in the secrets you wish to push to Infisical + stringData: + API_KEY: some-api-key + DATABASE_URL: postgres://127.0.0.1:5432 + ENCRYPTION_KEY: fabcc12-a22-facbaa4-11aa568aab + ``` + + + + + + + The `authentication` field dictates which authentication method to use when pushing secrets to Infisical. + The available authentication methods are `universalAuth`, `kubernetesAuth`, `awsIamAuth`, `azureAuth`, `gcpIdTokenAuth`, and `gcpIamAuth`. + + + + The universal authentication method is one of the easiest ways to get started with Infisical. Universal Auth works anywhere and is not tied to any specific cloud provider. + [Read more about Universal Auth](/documentation/platform/identities/universal-auth). + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + - `credentialsRef`: The name and namespace of the Kubernetes secret that stores the service token. + - `credentialsRef.secretName`: The name of the Kubernetes secret. + - `credentialsRef.secretNamespace`: The namespace of the Kubernetes secret. + + Example: + + ```yaml + # infisical-push-secret.yaml + spec: + universalAuth: + credentialsRef: + secretName: + secretNamespace: + ``` + + ```yaml + # machine-identity-credentials.yaml + apiVersion: v1 + kind: Secret + metadata: + name: universal-auth-credentials + type: Opaque + stringData: + clientId: + clientSecret: + ``` + + + + The Kubernetes machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalPushSecret resource. This authentication method can only be used within a Kubernetes environment. + [Read more about Kubernetes Auth](/documentation/platform/identities/kubernetes-auth). + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + - `serviceAccountRef`: The name and namespace of the service account that will be used to authenticate with Infisical. + - `serviceAccountRef.name`: The name of the service account. + - `serviceAccountRef.namespace`: The namespace of the service account. + - `autoCreateServiceAccountToken`: If set to `true`, the operator will automatically create a short-lived service account token on-demand for the service account. Defaults to `false`. + - `serviceAccountTokenAudiences`: Optionally specify audience for the service account token. This field is only relevant if you have set `autoCreateServiceAccountToken` to `true`. No audience is specified by default. + + Example: + + ```yaml + spec: + kubernetesAuth: + identityId: + autoCreateServiceAccountToken: true # Automatically creates short-lived service account tokens for the service account. + serviceAccountTokenAudiences: + - # Optionally specify audience for the service account token. No audience is specified by default. + serviceAccountRef: + name: + namespace: + ``` + + + + The AWS IAM machine identity authentication method is used to authenticate with Infisical. + [Read more about AWS IAM Auth](/documentation/platform/identities/aws-auth). + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + + Example: + + ```yaml + spec: + authentication: + awsIamAuth: + identityId: + ``` + + + + The AWS IAM machine identity authentication method is used to authenticate with Infisical. Azure Auth can only be used from within an Azure environment. + [Read more about Azure Auth](/documentation/platform/identities/azure-auth). + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + + Example: + + ```yaml + spec: + authentication: + azureAuth: + identityId: + ``` + + + The GCP IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalPushSecret resource. This authentication method can only be used both within and outside GCP environments. + [Read more about Azure Auth](/documentation/platform/identities/gcp-auth). + + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + - `serviceAccountKeyFilePath`: The path to the GCP service account key file. + + Example: + + ```yaml + spec: + gcpIamAuth: + identityId: + serviceAccountKeyFilePath: + ``` + + + The GCP ID Token machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalPushSecret resource. This authentication method can only be used within GCP environments. + [Read more about Azure Auth](/documentation/platform/identities/gcp-auth). + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + + Example: + + ```yaml + spec: + gcpIdTokenAuth: + identityId: + ``` + + + + + + + This block defines the TLS settings to use for connecting to the Infisical + instance. + + Fields: + + This block defines the reference to the CA certificate to use for connecting to the Infisical instance with SSL/TLS. + + Valid fields: + - `secretName`: The name of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS. + - `secretNamespace`: The namespace of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS. + - `key`: The name of the key in the Kubernetes secret which contains the value of the CA certificate to use for connecting to the Infisical instance with SSL/TLS. + + Example: + + ```yaml + tls: + caRef: + secretName: custom-ca-certificate + secretNamespace: default + key: ca.crt + ``` + + + + + +## Using templating to push secrets + +Pushing secrets to Infisical from the operator may not always be enough. +Templating is a useful utility of the Infisical secrets operator that allows you to use Go Templating to template the secrets you want to push to Infisical. +Using Go templates, you can format, combine, and create new key-value pairs of secrets that you want to push to Infisical. + + + + This property controls what secrets are included in your push to Infisica. + When set to `true`, all secrets included in the `push.secret.secretName` Kubernetes secret will be pushed to Infisical. + **Use this option when you would like to push all secrets to Infisical from the secrets operator, but want to template a subset of them.** + + When set to `false`, only secrets defined in the `push.secret.template.data` field of the template will be pushed to Infisical. + Use this option when you would like to push **only** a subset of secrets from the Kubernetes secret to Infisical. + + + Define secret keys and their corresponding templates. + Each data value uses a Golang template with access to all secrets defined in the `push.secret.secretName` Kubernetes secret. + + Secrets are structured as follows: + + ```go + type TemplateSecret struct { + Value string `json:"value"` + SecretPath string `json:"secretPath"` + } + ``` + + #### Example template configuration: + + ```yaml + # This example assumes that the `push-secret-demo` Kubernetes secret contains the following secrets: + # SITE_URL = "https://example.com" + # REGION = "us-east-1" + # OTHER_SECRET = "other-secret" + + push: + secret: + secretName: push-secret-demo + secretNamespace: default + template: + includeAllSecrets: true # Includes all secrets from the `push-secret-demo` Kubernetes secret + data: + SITE_URL: "{{ .SITE_URL.Value }}" + API_URL: "https://api.{{.SITE_URL.Value}}.{{.REGION.Value}}.com" # Will create a new secret in Infisical with the key `API_URL` with the value of the `SITE_URL` and `REGION` secrets + ``` + + To help transform your config map data further, the operator provides a set of built-in functions that you can use in your templates. + + ### Available templating functions + Please refer to the [templating functions documentation](/integrations/platforms/kubernetes/overview#available-helper-functions) for more information. + + +## Applying the InfisicalPushSecret CRD to your cluster + +Once you have configured the `InfisicalPushSecret` CRD with the required fields, you can apply it to your cluster. +After applying, you should notice that the secrets have been pushed to Infisical. + +```bash + kubectl apply -f source-push-secret.yaml # The secret that you're referencing in the InfisicalPushSecret CRD push.secret field + kubectl apply -f example-infisical-push-secret-crd.yaml # The InfisicalPushSecret CRD itself +``` \ No newline at end of file diff --git a/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx new file mode 100644 index 000000000..4c33b893b --- /dev/null +++ b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx @@ -0,0 +1,1479 @@ +--- +sidebarTitle: "InfisicalSecret CRD" +title: "Using the InfisicalSecret CRD" +description: "Learn how to use the InfisicalSecret CRD to fetch secrets from Infisical and store them as native Kubernetes secret resource" +--- + +Once you have installed the operator to your cluster, you'll need to create a `InfisicalSecret` custom resource definition (CRD). +In this CRD, you'll define the authentication method to use, the secrets to fetch, and the target location to store the secrets within your cluster. + +```yaml example-infisical-secret-crd.yaml +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalSecret +metadata: + name: infisicalsecret-sample + labels: + label-to-be-passed-to-managed-secret: sample-value + annotations: + example.com/annotation-to-be-passed-to-managed-secret: "sample-value" +spec: + hostAPI: https://app.infisical.com/api + resyncInterval: 10 + authentication: + kubernetesAuth: + identityId: + serviceAccountRef: + name: + namespace: + + managedKubeSecretReferences: + - secretName: managed-secret + secretNamespace: default + creationPolicy: "Orphan" + template: + includeAllSecrets: true + data: + NEW_KEY_NAME: "{{ .KEY.SecretPath }} {{ .KEY.Value }}" + KEY_WITH_BINARY_VALUE: "{{ .KEY.SecretPath }} {{ .KEY.Value }}" +``` + +## CRD properties + +### Generic + +The following properties help define what instance of Infisical the operator will interact with, the interval it will sync secrets and any CA certificates that may be required to connect. + + + If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to + ` https://your-self-hosted-instace.com/api` + +When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud. + + + If you have installed your Infisical instance within the same cluster as the Infisical operator, you can optionally access the Infisical backend's service directly without having to route through the public internet. + To achieve this, use the following address for the hostAPI field: + + ``` bash + http://..svc.cluster.local:4000/api + ``` + + Make sure to replace `` and `` with the appropriate values for your backend service and namespace. + + + + + + This property defines the time in seconds between each secret re-sync from + Infisical. Shorter time between re-syncs will require higher rate limits only + available on paid plans. Default re-sync interval is every 1 minute. + + + + This block defines the TLS settings to use for connecting to the Infisical + instance. + + + + This block defines the reference to the CA certificate to use for connecting + to the Infisical instance with SSL/TLS. + + + + The name of the Kubernetes secret containing the CA certificate to use for + connecting to the Infisical instance with SSL/TLS. + + + + The namespace of the Kubernetes secret containing the CA certificate to use + for connecting to the Infisical instance with SSL/TLS. + + + + The name of the key in the Kubernetes secret which contains the value of the + CA certificate to use for connecting to the Infisical instance with SSL/TLS. + + +### Authentication Methods + +To retrieve the requested secrets, the operator must first authenticate with Infisical. +The list of available authentication methods are shown below. + + + + + The universal machine identity authentication method is used to authenticate with Infisical. The client ID and client secret needs to be stored in a Kubernetes secret. This block defines the reference to the name and namespace of secret that stores these credentials. + + + + You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about machine identities here](/documentation/platform/identities/universal-auth). + + + Once you have created your machine identity and added it to your project(s), you will need to create a Kubernetes secret containing the identity credentials. + To quickly create a Kubernetes secret containing the identity credentials, you can run the command below. + + Make sure you replace `` with the identity client ID and `` with the identity client secret. + + ``` bash + kubectl create secret generic universal-auth-credentials --from-literal=clientId="" --from-literal=clientSecret="" + ``` + + + + Once the secret is created, add the `secretName` and `secretNamespace` of the secret that was just created under `authentication.universalAuth.credentialsRef` field in the InfisicalSecret resource. + + + + + + Make sure to also populate the `secretsScope` field with the project slug + _`projectSlug`_, environment slug _`envSlug`_, and secrets path + _`secretsPath`_ that you want to fetch secrets from. Please see the example + below. + + +## Example + +```yaml +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalSecret +metadata: + name: infisicalsecret-sample-crd +spec: + authentication: + universalAuth: + secretsScope: + projectSlug: # <-- project slug + envSlug: # "dev", "staging", "prod", etc.. + secretsPath: "" # Root is "/" + credentialsRef: + secretName: universal-auth-credentials # <-- name of the Kubernetes secret that stores our machine identity credentials + secretNamespace: default # <-- namespace of the Kubernetes secret that stores our machine identity credentials + ... +``` + + + + + The Kubernetes machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within a Kubernetes environment. + + + + Short-lived service account tokens are automatically created by the operator and are valid only for a short period of time. This is the recommended approach for using Kubernetes auth in the Infisical Secrets Operator. + + + + **1.1.** Start by creating a reviewer service account in your Kubernetes cluster that will be used by Infisical to authenticate with the Kubernetes API Server. + + ```yaml infisical-reviewer-service-account.yaml + apiVersion: v1 + kind: ServiceAccount + metadata: + name: infisical-token-reviewer + namespace: default + + ``` + + ```bash + kubectl apply -f infisical-reviewer-service-account.yaml + ``` + + **1.2.** Bind the reviewer service account to the `system:auth-delegator` cluster role. As described [here](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#other-component-roles), this role allows delegated authentication and authorization checks, specifically for Infisical to access the [TokenReview API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/). You can apply the following configuration file: + + ```yaml infisical-reviewer-cluster-role-binding.yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: infisical-token-reviewer-role-binding + namespace: default + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:auth-delegator + subjects: + - kind: ServiceAccount + name: infisical-token-reviewer + namespace: default + ``` + + ```bash + kubectl apply -f infisical-reviewer-cluster-role-binding.yaml + ``` + + **1.3.** Next, create a long-lived service account JWT token (i.e. the token reviewer JWT token) for the service account using this configuration file for a new `Secret` resource: + + ```yaml service-account-reviewer-token.yaml + apiVersion: v1 + kind: Secret + type: kubernetes.io/service-account-token + metadata: + name: infisical-token-reviewer-token + annotations: + kubernetes.io/service-account.name: "infisical-token-reviewer" + ``` + + + ```bash + kubectl apply -f service-account-reviewer-token.yaml + ``` + + **1.4.** Link the secret in step 1.3 to the service account in step 1.1: + + ```bash + kubectl patch serviceaccount infisical-token-reviewer -p '{"secrets": [{"name": "infisical-token-reviewer-token"}]}' -n default + ``` + + **1.5.** Finally, retrieve the token reviewer JWT token from the secret. + + ```bash + kubectl get secret infisical-token-reviewer-token -n default -o=jsonpath='{.data.token}' | base64 --decode + ``` + + Keep this JWT token handy as you will need it for the **Token Reviewer JWT** field when configuring the Kubernetes Auth authentication method for the identity in step 2. + + + + To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. + + ![identities organization](/images/platform/identities/identities-org.png) + + When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles. + + ![identities organization create](/images/platform/identities/identities-org-create.png) + + Now input a few details for your new identity. Here's some guidance for each field: + + - Name (required): A friendly name for the identity. + - Role (required): A role from the **Organization Roles** tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to. + + Once you've created an identity, you'll be prompted to configure the authentication method for it. Here, select **Kubernetes Auth**. + + + To learn more about each field of the Kubernetes native authentication method, see step 2 of [guide](/documentation/platform/identities/kubernetes-auth#guide). + + + ![identities organization create auth method](/images/platform/identities/identities-org-create-kubernetes-auth-method.png) + + + + + To allow the operator to use the given identity to access secrets, you will need to add the identity to project(s) that you would like to grant it access to. + + To do this, head over to the project you want to add the identity to and go to Project Settings > Access Control > Machine Identities and press **Add identity**. + + Next, select the identity you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this identity can have access to. + + ![identities project](/images/platform/identities/identities-project.png) + + ![identities project create](/images/platform/identities/identities-project-create.png) + + + + + You have already created the reviewer service account in step **1.1**. Now, create a new Kubernetes service account that will be used to authenticate with Infisical. + This service account will create short-lived tokens that will be used to authenticate with Infisical. The operator itself will handle the creation of these tokens automatically. + + ```yaml infisical-service-account.yaml + kind: ServiceAccount + apiVersion: v1 + metadata: + name: infisical-service-account + ``` + + ```bash + kubectl apply -f infisical-service-account.yaml -n default + ``` + + + + + Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource. + In the `authentication.kubernetesAuth.identityId` field, add the identity ID of the machine identity you created. + See the example below for more details. + + + Add the service account details from the previous steps under `authentication.kubernetesAuth.serviceAccountRef`. + Here you will need to enter the name and namespace of the service account. + The example below shows a complete InfisicalSecret resource with all required fields defined. + Make sure you set `authentication.kubernetesAuth.autoCreateServiceAccountToken` to `true` to automatically create short-lived service account tokens for the service account. + + + + + Make sure to also populate the `secretsScope` field with the project slug + _`projectSlug`_, environment slug _`envSlug`_, and secrets path + _`secretsPath`_ that you want to fetch secrets from. Please see the example + below. + + + ## Example + + ```yaml example-kubernetes-auth.yaml + apiVersion: secrets.infisical.com/v1alpha1 + kind: InfisicalSecret + metadata: + name: infisicalsecret-sample-crd + spec: + authentication: + kubernetesAuth: + identityId: + autoCreateServiceAccountToken: true # Automatically creates short-lived service account tokens for the service account. + serviceAccountTokenAudiences: + - # Optionally specify audience for the service account token. No audience is specified by default. + serviceAccountRef: + name: infisical-service-account # The service account we just created in the previous step. + namespace: + + # secretsScope is identical to the secrets scope in the universalAuth field in this sample. + secretsScope: + projectSlug: your-project-slug + envSlug: prod + secretsPath: "/path" + recursive: true + ... + ``` + + + + Manual long-lived service account tokens are manually created by the user and are valid indefinitely unless deleted or rotated. In most cases, you should be using the automatic short-lived service account tokens as they are more secure and easier to use. + + + **1.1.** Start by creating a reviewer service account in your Kubernetes cluster that will be used by Infisical to authenticate with the Kubernetes API Server. + + ```yaml infisical-reviewer-service-account.yaml + apiVersion: v1 + kind: ServiceAccount + metadata: + name: infisical-token-reviewer + namespace: default + + ``` + + ```bash + kubectl apply -f infisical-reviewer-service-account.yaml + ``` + + **1.2.** Bind the reviewer service account to the `system:auth-delegator` cluster role. As described [here](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#other-component-roles), this role allows delegated authentication and authorization checks, specifically for Infisical to access the [TokenReview API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/). You can apply the following configuration file: + + ```yaml infisical-reviewer-cluster-role-binding.yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: infisical-token-reviewer-role-binding + namespace: default + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:auth-delegator + subjects: + - kind: ServiceAccount + name: infisical-token-reviewer + namespace: default + ``` + + ```bash + kubectl apply -f infisical-reviewer-cluster-role-binding.yaml + ``` + + **1.3.** Next, create a long-lived service account JWT token (i.e. the token reviewer JWT token) for the service account using this configuration file for a new `Secret` resource: + + ```yaml service-account-reviewer-token.yaml + apiVersion: v1 + kind: Secret + type: kubernetes.io/service-account-token + metadata: + name: infisical-token-reviewer-token + annotations: + kubernetes.io/service-account.name: "infisical-token-reviewer" + ``` + + + ```bash + kubectl apply -f service-account-reviewer-token.yaml + ``` + + **1.4.** Link the secret in step 1.3 to the service account in step 1.1: + + ```bash + kubectl patch serviceaccount infisical-token-reviewer -p '{"secrets": [{"name": "infisical-token-reviewer-token"}]}' -n default + ``` + + **1.5.** Finally, retrieve the token reviewer JWT token from the secret. + + ```bash + kubectl get secret infisical-token-reviewer-token -n default -o=jsonpath='{.data.token}' | base64 --decode + ``` + + Keep this JWT token handy as you will need it for the **Token Reviewer JWT** field when configuring the Kubernetes Auth authentication method for the identity in step 2. + + + + To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. + + ![identities organization](/images/platform/identities/identities-org.png) + + When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles. + + ![identities organization create](/images/platform/identities/identities-org-create.png) + + Now input a few details for your new identity. Here's some guidance for each field: + + - Name (required): A friendly name for the identity. + - Role (required): A role from the **Organization Roles** tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to. + + Once you've created an identity, you'll be prompted to configure the authentication method for it. Here, select **Kubernetes Auth**. + + + To learn more about each field of the Kubernetes native authentication method, see step 2 of [guide](/documentation/platform/identities/kubernetes-auth#guide). + + + ![identities organization create auth method](/images/platform/identities/identities-org-create-kubernetes-auth-method.png) + + + + + To allow the operator to use the given identity to access secrets, you will need to add the identity to project(s) that you would like to grant it access to. + + To do this, head over to the project you want to add the identity to and go to Project Settings > Access Control > Machine Identities and press **Add identity**. + + Next, select the identity you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this identity can have access to. + + ![identities project](/images/platform/identities/identities-project.png) + + ![identities project create](/images/platform/identities/identities-project-create.png) + + + + + You have already created the reviewer service account in step **1.1**. Now, create a new Kubernetes service account that will be used to authenticate with Infisical. + + ```yaml infisical-service-account.yaml + kind: ServiceAccount + apiVersion: v1 + metadata: + name: infisical-service-account + ``` + + ```bash + kubectl apply -f infisical-service-account.yaml -n default + ``` + + + + Create a service account token for the newly created Kubernetes service account from the previous step. + + ```yaml infisical-service-account-token.yaml + apiVersion: v1 + kind: Secret + type: kubernetes.io/service-account-token + metadata: + name: infisical-service-account-token + annotations: + kubernetes.io/service-account.name: "infisical-service-account" + ``` + + ```bash + kubectl apply -f infisical-service-account-token.yaml -n default + ``` + + Patch the service account with the newly created service account token. + + ```bash + kubectl patch serviceaccount infisical-service-account -p '{"secrets": [{"name": "infisical-service-account-token"}]}' -n default + ``` + + + + Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource. + In the `authentication.kubernetesAuth.identityId` field, add the identity ID of the machine identity you created. + See the example below for more details. + + + Add the service account details from the previous steps under `authentication.kubernetesAuth.serviceAccountRef`. + Here you will need to enter the name and namespace of the service account. + The example below shows a complete InfisicalSecret resource with all required fields defined. + + + + + Make sure to also populate the `secretsScope` field with the project slug + _`projectSlug`_, environment slug _`envSlug`_, and secrets path + _`secretsPath`_ that you want to fetch secrets from. Please see the example + below. + + + ## Example + + ```yaml example-kubernetes-auth.yaml + apiVersion: secrets.infisical.com/v1alpha1 + kind: InfisicalSecret + metadata: + name: infisicalsecret-sample-crd + spec: + authentication: + kubernetesAuth: + identityId: + serviceAccountRef: + name: infisical-service-account # The service account we just created in the previous step. (*not* the reviewer service account) + namespace: + + # secretsScope is identical to the secrets scope in the universalAuth field in this sample. + secretsScope: + projectSlug: your-project-slug + envSlug: prod + secretsPath: "/path" + recursive: true + ... + ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The AWS IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within an AWS environment like an EC2 or a Lambda function. + + + + You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about AWS machine identities here](/documentation/platform/identities/aws-auth). + + + Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource. In the `authentication.awsIamAuth.identityId` field, add the identity ID of the machine identity you created. See the example below for more details. + + + + + + Make sure to also populate the `secretsScope` field with the project slug + _`projectSlug`_, environment slug _`envSlug`_, and secrets path + _`secretsPath`_ that you want to fetch secrets from. Please see the example + below. + + +## Example + +```yaml example-aws-iam-auth.yaml +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalSecret +metadata: + name: infisicalsecret-sample-crd +spec: + authentication: + awsIamAuth: + identityId: + + # secretsScope is identical to the secrets scope in the universalAuth field in this sample. + secretsScope: + projectSlug: your-project-slug + envSlug: prod + secretsPath: "/path" + recursive: true + ... +``` + + + + + The Azure machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within an Azure environment. + + + + You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about Azure machine identities here](/documentation/platform/identities/azure-auth). + + + Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource. In the `authentication.azureAuth.identityId` field, add the identity ID of the machine identity you created. See the example below for more details. + + + + + + Make sure to also populate the `secretsScope` field with the project slug + _`projectSlug`_, environment slug _`envSlug`_, and secrets path + _`secretsPath`_ that you want to fetch secrets from. Please see the example + below. + + +## Example + +```yaml example-azure-auth.yaml +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalSecret +metadata: + name: infisicalsecret-sample-crd +spec: + authentication: + azureAuth: + identityId: + + # secretsScope is identical to the secrets scope in the universalAuth field in this sample. + secretsScope: + projectSlug: your-project-slug + envSlug: prod + secretsPath: "/path" + recursive: true + ... +``` + + + + + The GCP ID Token machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within GCP environments. + + + + You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about GCP machine identities here](/documentation/platform/identities/gcp-auth). + + + Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource. In the `authentication.gcpIdTokenAuth.identityId` field, add the identity ID of the machine identity you created. See the example below for more details. + + + + + + Make sure to also populate the `secretsScope` field with the project slug + _`projectSlug`_, environment slug _`envSlug`_, and secrets path + _`secretsPath`_ that you want to fetch secrets from. Please see the example + below. + + +## Example + +```yaml example-gcp-id-token-auth.yaml +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalSecret +metadata: + name: infisicalsecret-sample-crd +spec: + authentication: + gcpIdTokenAuth: + identityId: + + # secretsScope is identical to the secrets scope in the universalAuth field in this sample. + secretsScope: + projectSlug: your-project-slug + envSlug: prod + secretsPath: "/path" + recursive: true + ... +``` + + + + + The GCP IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used both within and outside GCP environments. + + + + You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about GCP machine identities here](/documentation/platform/identities/gcp-auth). + + + Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource. In the `authentication.gcpIamAuth.identityId` field, add the identity ID of the machine identity you created. + You'll also need to add the service account key file path to your InfisicalSecret resource. In the `authentication.gcpIamAuth.serviceAccountKeyFilePath` field, add the path to your service account key file path. Please see the example below for more details. + + + + + + Make sure to also populate the `secretsScope` field with the project slug + _`projectSlug`_, environment slug _`envSlug`_, and secrets path + _`secretsPath`_ that you want to fetch secrets from. Please see the example + below. + + +## Example + +```yaml example-gcp-id-token-auth.yaml +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalSecret +metadata: + name: infisicalsecret-sample-crd +spec: + authentication: + gcpIamAuth: + identityId: + serviceAccountKeyFilePath: "/path/to-service-account-key-file-path.json" + + # secretsScope is identical to the secrets scope in the universalAuth field in this sample. + secretsScope: + projectSlug: your-project-slug + envSlug: prod + secretsPath: "/path" + recursive: true + ... +``` + + + + + +The service token required to authenticate with Infisical needs to be stored in a Kubernetes secret. This block defines the reference to the name and namespace of secret that stores this service token. +Follow the instructions below to create and store the service token in a Kubernetes secrets and reference it in your CRD. + +#### 1. Generate service token + +You can generate a [service token](../../documentation/platform/token) for an Infisical project by heading over to the Infisical dashboard then to Project Settings. + +#### 2. Create Kubernetes secret containing service token + +Once you have generated the service token, you will need to create a Kubernetes secret containing the service token you generated. +To quickly create a Kubernetes secret containing the generated service token, you can run the command below. Make sure you replace `` with your service token. + +```bash +kubectl create secret generic service-token --from-literal=infisicalToken="" +``` + +#### 3. Add reference for the Kubernetes secret containing service token + +Once the secret is created, add the name and namespace of the secret that was just created under `authentication.serviceToken.serviceTokenSecretReference` field in the InfisicalSecret resource. + +{" "} + + + Make sure to also populate the `secretsScope` field with the, environment slug + _`envSlug`_, and secrets path _`secretsPath`_ that you want to fetch secrets + from. Please see the example below. + + +## Example + +```yaml +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalSecret +metadata: + name: infisicalsecret-sample-crd +spec: + authentication: + serviceToken: + serviceTokenSecretReference: + secretName: service-token # <-- name of the Kubernetes secret that stores our service token + secretNamespace: option # <-- namespace of the Kubernetes secret that stores our service token + secretsScope: + envSlug: # "dev", "staging", "prod", etc.. + secretsPath: # Root is "/" + ... +``` + + + +### Operator Managed Secrets + +The managed secret properties specify where to store the secrets retrieved from your Infisical project. +This includes defining the name and namespace of the Kubernetes secret that will hold these secrets. +The Infisical operator will automatically create the Kubernetes secret in the specified name/namespace and ensure it stays up-to-date. + + + +The `managedSecretReference` field is deprecated and will be removed in a future release. +Replace it with `managedKubeSecretReferences`, which now accepts an array of references to support multiple managed secrets in a single InfisicalSecret CRD. + +Example: + +```yaml +managedKubeSecretReferences: + - secretName: managed-secret + secretNamespace: default + creationPolicy: "Orphan" +``` + + + + + + +The name of the managed Kubernetes secret to be created + + +The namespace of the managed Kubernetes secret to be created. + + +Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets. + + +Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator. +This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically. + +#### Available options + +- `Orphan` (default) +- `Owner` + + + When creation policy is set to `Owner`, the `InfisicalSecret` CRD must be in + the same namespace as where the managed kubernetes secret. + + + + +#### Managed Secret Templating + +Fetching secrets from Infisical as is via the operator may not be enough. This is where templating functionality may be helpful. +Using Go templates, you can format, combine, and create new key-value pairs from secrets fetched from Infisical before storing them as Kubernetes Secrets. + + + + + This property controls what secrets are included in your managed secret when using templates. + When set to `true`, all secrets fetched from your Infisical project will be added into your managed Kubernetes secret resource. + **Use this option when you would like to sync all secrets from Infisical to Kubernetes but want to template a subset of them.** + +When set to `false`, only secrets defined in the `managedKubeSecretReferences[].template.data` field of the template will be included in the managed secret. +Use this option when you would like to sync **only** a subset of secrets from Infisical to Kubernetes. + + + +Define secret keys and their corresponding templates. +Each data value uses a Golang template with access to all secrets retrieved from the specified scope. + +Secrets are structured as follows: + +```golang +type TemplateSecret struct { + Value string `json:"value"` + SecretPath string `json:"secretPath"` +} +``` + +#### Example template configuration: + +```yaml +managedKubeSecretReferences: + - secretName: managed-secret + secretNamespace: default + template: + includeAllSecrets: true + data: + # Create new secret key that doesn't exist in your Infisical project using values of other secrets + NEW_KEY: "{{ .DB_PASSWORD.Value }}" + # Override an existing secret key in Infisical project with a new value using values of other secrets + API_URL: "https://api.{{.COMPANY_NAME.Value}}.{{.REGION.Value}}.com" +``` + +For this example, let's assume the following secrets exist in your Infisical project: + +``` +DB_PASSWORD = "secret123" +COMPANY_NAME = "acme" +REGION = "us-east-1" +API_URL = "old-url" # This will be overridden +``` + +The resulting managed Kubernetes secret will then contain: + +``` +# Original secrets (from includeAllSecrets: true) +DB_PASSWORD = "secret123" +COMPANY_NAME = "acme" +REGION = "us-east-1" + +# New and overridden templated secrets +NEW_KEY = "secret123" # New secret created from template +API_URL = "https://api.acme.us-east-1.com" # Existing secret overridden by template +``` + +To help transform your secrets further, the operator provides a set of built-in functions that you can use in your templates. + +### Available templating functions + +Please refer to the [templating functions documentation](/integrations/platforms/kubernetes/overview#available-helper-functions) for more information. + + + +### Operator Managed ConfigMaps + +The managed config map properties specify where to store the secrets retrieved from your Infisical project. Config maps can be used to store **non-sensitive** data, such as application configuration variables. +The properties includes defining the name and namespace of the Kubernetes config map that will hold the data retrieved from your Infisical project. +The Infisical operator will automatically create the Kubernetes config map in the specified name/namespace and ensure it stays up-to-date. If a config map already exists in the specified namespace, the operator will update the existing config map with the new data. + + + The usage of config maps is only intended for storing non-sensitive data. If you are looking to store sensitive data, please use the [managed secret](#operator-managed-secrets) property instead. + + + + + + The name of the managed Kubernetes config map that your Infisical data will be stored in. + + + The namespace of the managed Kubernetes config map that your Infisical data will be stored in. + + + Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes config map that is generated by the Infisical operator. + This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically. + + #### Available options + + - `Orphan` (default) + - `Owner` + + + When creation policy is set to `Owner`, the `InfisicalSecret` CRD must be in + the same namespace as where the managed kubernetes config map. + + + + + +#### Managed ConfigMap Templating + +Fetching secrets from Infisical as is via the operator may not be enough. This is where templating functionality may be helpful. +Using Go templates, you can format, combine, and create new key-value pairs from secrets fetched from Infisical before storing them as Kubernetes Config Maps. + + + + + This property controls what secrets are included in your managed config map when using templates. + When set to `true`, all secrets fetched from your Infisical project will be added into your managed Kubernetes config map resource. + **Use this option when you would like to sync all secrets from Infisical to Kubernetes but want to template a subset of them.** + + When set to `false`, only secrets defined in the `managedKubeConfigMapReferences[].template.data` field of the template will be included in the managed config map. + Use this option when you would like to sync **only** a subset of secrets from Infisical to Kubernetes. + + + + Define secret keys and their corresponding templates. + Each data value uses a Golang template with access to all secrets retrieved from the specified scope. + + Secrets are structured as follows: + + ```golang + type TemplateSecret struct { + Value string `json:"value"` + SecretPath string `json:"secretPath"` + } + ``` + + #### Example template configuration: + + ```yaml + managedKubeConfigMapReferences: + - configMapName: managed-configmap + configMapNamespace: default + template: + includeAllSecrets: true + data: + # Create new key that doesn't exist in your Infisical project using values of other secrets + SITE_URL: "{{ .SITE_URL.Value }}" + # Override an existing key in Infisical project with a new value using values of other secrets + API_URL: "https://api.{{.SITE_URL.Value}}.{{.REGION.Value}}.com" + ``` + + For this example, let's assume the following secrets exist in your Infisical project: + + ``` + SITE_URL = "https://example.com" + REGION = "us-east-1" + API_URL = "old-url" # This will be overridden + ``` + + The resulting managed Kubernetes config map will then contain: + + ``` + # Original config map data (from includeAllSecrets: true) + SITE_URL = "https://example.com" + REGION = "us-east-1" + + # New and overridden config map data + SITE_URL = "https://example.com" + API_URL = "https://api.example.com.us-east-1.com" # Existing secret overridden by template + ``` + + To help transform your config map data further, the operator provides a set of built-in functions that you can use in your templates. + + ### Available templating functions + + Please refer to the [templating functions documentation](/integrations/platforms/kubernetes/overview#available-helper-functions) for more information. + + +## Applying CRD + +Once you have configured the InfisicalSecret CRD with the required fields, you can apply it to your cluster. +After applying, you should notice that the managed secret has been created in the desired namespace your specified. + +``` +kubectl apply -f example-infisical-secret-crd.yaml +``` + +To verify that the operator has successfully created the managed secret, you can check the secrets in the namespace that was specified. + + + + ```bash + # Verify managed secret is created + kubectl get secrets -n + ``` + + The Infisical secrets will be synced and stored into the managed secret every + 1 minute unless configured otherwise. + + + + ```bash + # Verify managed config map is created + kubectl get configmaps -n + ``` + + The Infisical config map data will be synced and stored into the managed config map every + 1 minute unless configured otherwise. + + + + + + +## Using Managed Secret In Your Deployment + +To make use of the managed secret created by the operator into your deployment can be achieved through several methods. +Here, we will highlight three of the most common ways to utilize it. Learn more about Kubernetes secrets [here](https://kubernetes.io/docs/concepts/configuration/secret/) + + + This will take all the secrets from your managed secret and expose them to your container + + ````yaml + envFrom: + - secretRef: + name: managed-secret # managed secret name + ``` + + Example usage in a deployment + ```yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: nginx-deployment + labels: + app: nginx + spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + envFrom: + - secretRef: + name: managed-secret # <- name of managed secret + ports: + - containerPort: 80 + ```` + + + + + This will allow you to select individual secrets by key name from your managed secret and expose them to your container + + ```yaml + env: + - name: SECRET_NAME # The environment variable's name which is made available in the container + valueFrom: + secretKeyRef: + name: managed-secret # managed secret name + key: SOME_SECRET_KEY # The name of the key which exists in the managed secret + ``` + + Example usage in a deployment + + ```yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: nginx-deployment + labels: + app: nginx + spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + env: + - name: STRIPE_API_SECRET + valueFrom: + secretKeyRef: + name: managed-secret # <- name of managed secret + key: STRIPE_API_SECRET + ports: + - containerPort: 80 + ``` + + + + This will allow you to create a volume on your container which comprises of files holding the secrets in your managed kubernetes secret + ```yaml + volumes: + - name: secrets-volume-name # The name of the volume under which secrets will be stored + secret: + secretName: managed-secret # managed secret name + ```` + + You can then mount this volume to the container's filesystem so that your deployment can access the files containing the managed secrets + + ```yaml + volumeMounts: + - name: secrets-volume-name + mountPath: /etc/secrets + readOnly: true + ``` + + Example usage in a deployment + + ```yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: nginx-deployment + labels: + app: nginx + spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + volumeMounts: + - name: secrets-volume-name + mountPath: /etc/secrets + readOnly: true + ports: + - containerPort: 80 + volumes: + - name: secrets-volume-name + secret: + secretName: managed-secret # <- managed secrets + ``` + + + +The definition file of the Kubernetes secret for the CA certificate can be structured like the following: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: custom-ca-certificate +type: Opaque +stringData: + ca.crt: | + -----BEGIN CERTIFICATE----- + MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL + ... + BQAwDTELMAkGA1UEChMCUEgwHhcNMjQxMDI1MTU0MjAzWhcNMjUxMDI1MjE0MjAz + -----END CERTIFICATE----- +``` + +### Automatic Redeployment + +Deployments using managed secrets don't reload automatically on updates, so they may use outdated secrets unless manually redeployed. +To address this, we added functionality to automatically redeploy your deployment when its managed secret updates. + +#### Enabling Automatic Redeployment + +To enable auto redeployment you simply have to add the following annotation to the deployment, statefulset, or daemonset that consumes a managed secret. + +```yaml +secrets.infisical.com/auto-reload: "true" +``` + + + ```yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: nginx-deployment + labels: + app: nginx + annotations: + secrets.infisical.com/auto-reload: "true" # <- redeployment annotation + spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + envFrom: + - secretRef: + name: managed-secret + ports: + - containerPort: 80 + ``` + + + #### How it works + When a secret change occurs, the operator will check to see which deployments are using the operator-managed Kubernetes secret that received the update. + Then, for each deployment that has this annotation present, a rolling update will be triggered. + + +## Using Managed ConfigMap In Your Deployment + +To make use of the managed ConfigMap created by the operator into your deployment can be achieved through several methods. +Here, we will highlight three of the most common ways to utilize it. Learn more about Kubernetes ConfigMaps [here](https://kubernetes.io/docs/concepts/configuration/configmap/) + + + Automatic redeployment of deployments using managed ConfigMaps is not yet supported. + + + + + This will take all the secrets from your managed ConfigMap and expose them to your container + + ````yaml + envFrom: + - configMapRef: + name: managed-configmap # managed configmap name + ``` + + Example usage in a deployment + ```yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: nginx-deployment + labels: + app: nginx + spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + envFrom: + - configMapRef: + name: managed-configmap # <- name of managed configmap + ports: + - containerPort: 80 + ```` + + + + + This will allow you to select individual secrets by key name from your managed ConfigMap and expose them to your container + + ```yaml + env: + - name: CONFIG_NAME # The environment variable's name which is made available in the container + valueFrom: + configMapKeyRef: + name: managed-configmap # managed configmap name + key: SOME_CONFIG_KEY # The name of the key which exists in the managed configmap + ``` + + Example usage in a deployment + + ```yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: nginx-deployment + labels: + app: nginx + spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + env: + - name: STRIPE_API_SECRET + valueFrom: + configMapKeyRef: + name: managed-configmap # <- name of managed configmap + key: STRIPE_API_SECRET + ports: + - containerPort: 80 + ``` + + + + + This will allow you to create a volume on your container which comprises of files holding the secrets in your managed kubernetes secret + ```yaml + volumes: + - name: configmaps-volume-name # The name of the volume under which configmaps will be stored + configMap: + name: managed-configmap # managed configmap name + ```` + + You can then mount this volume to the container's filesystem so that your deployment can access the files containing the managed secrets + + ```yaml + volumeMounts: + - name: configmaps-volume-name + mountPath: /etc/config + readOnly: true + ``` + + Example usage in a deployment + + ```yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: nginx-deployment + labels: + app: nginx + spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + volumeMounts: + - name: configmaps-volume-name + mountPath: /etc/config + readOnly: true + ports: + - containerPort: 80 + volumes: + - name: configmaps-volume-name + configMap: + name: managed-configmap # <- managed configmap + ``` + + +The definition file of the Kubernetes secret for the CA certificate can be structured like the following: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: custom-ca-certificate +type: Opaque +stringData: + ca.crt: | + -----BEGIN CERTIFICATE----- + MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL + ... + BQAwDTELMAkGA1UEChMCUEgwHhcNMjQxMDI1MTU0MjAzWhcNMjUxMDI1MjE0MjAz + -----END CERTIFICATE----- +``` + +## Propagating Labels & Annotations + +The operator will transfer all labels & annotations present on the `InfisicalSecret` CRD to the managed Kubernetes secret to be created. +Thus, if a specific label is required on the resulting secret, it can be applied as demonstrated in the following example: + + + ```yaml + apiVersion: secrets.infisical.com/v1alpha1 + kind: InfisicalSecret + metadata: + name: infisicalsecret-sample + labels: + label-to-be-passed-to-managed-secret: sample-value + annotations: + example.com/annotation-to-be-passed-to-managed-secret: "sample-value" + spec: + .. + authentication: + ... + managedKubeSecretReferences: + ... + ``` + + This would result in the following managed secret to be created: + + ```yaml + apiVersion: v1 + data: ... + kind: Secret + metadata: + annotations: + example.com/annotation-to-be-passed-to-managed-secret: sample-value + secrets.infisical.com/version: W/"3f1-ZyOSsrCLGSkAhhCkY2USPu2ivRw" + labels: + label-to-be-passed-to-managed-secret: sample-value + name: managed-token + namespace: default + type: Opaque + ``` + diff --git a/docs/integrations/platforms/kubernetes/overview.mdx b/docs/integrations/platforms/kubernetes/overview.mdx new file mode 100644 index 000000000..ca700d777 --- /dev/null +++ b/docs/integrations/platforms/kubernetes/overview.mdx @@ -0,0 +1,237 @@ +--- +title: "Kubernetes Operator" +sidebarTitle: "Overview" +description: "How to use Infisical to inject, push, and manage secrets within Kubernetes clusters" +--- + +The Infisical Operator is a collection of Kubernetes controllers that streamline how secrets are managed between Infisical and your Kubernetes cluster. +It provides multiple Custom Resource Definitions (CRDs) which enable you to: + +- **Sync** secrets from Infisical into Kubernetes (`InfisicalSecret`). +- **Push** new secrets from Kubernetes to Infisical (`InfisicalPushSecret`). +- **Manage** dynamic secrets and automatically create time-bound leases (`InfisicalDynamicSecret`). + +When these CRDs are configured, the Infisical Operator will continuously monitors for changes and performs necessary updates to keep your Kubernetes secrets up to date. +It can also automatically reload dependent Deployments resources whenever relevant secrets are updated. + + + If you are already using the External Secrets operator, you can view the + integration documentation for it + [here](https://external-secrets.io/latest/provider/infisical/). + + +## Install + +The operator can be install via [Helm](https://helm.sh). Helm is a package manager for Kubernetes that allows you to define, install, and upgrade Kubernetes applications. + +**Install the latest Helm repository** +```bash +helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/' +``` + +```bash +helm repo update +``` + +The operator can be installed either cluster-wide or restricted to a specific namespace. +If you require stronger isolation and stricter access controls, a namespace-scoped installation may make more sense. + + + + ```bash + helm install --generate-name infisical-helm-charts/secrets-operator + ``` + + + The operator can be configured to watch and manage secrets in a specific namespace instead of having cluster-wide access. This is useful for: + + - **Enhanced Security**: Limit the operator's permissions to only specific namespaces instead of cluster-wide access + - **Multi-tenant Clusters**: Run separate operator instances for different teams or applications + - **Resource Isolation**: Ensure operators in different namespaces don't interfere with each other + - **Development & Testing**: Run development and production operators side by side in isolated namespaces + + **Note**: For multiple namespace-scoped installations, only the first installation should install CRDs. Subsequent installations should set `installCRDs: false` to avoid conflicts. + + ```bash + # First namespace installation (with CRDs) + helm install operator-namespace1 infisical-helm-charts/secrets-operator \ + --namespace first-namespace \ + --set scopedNamespace=first-namespace \ + --set scopedRBAC=true + + # Subsequent namespace installations + helm install operator-namespace2 infisical-helm-charts/secrets-operator \ + --namespace another-namespace \ + --set scopedNamespace=another-namespace \ + --set scopedRBAC=true \ + --set installCRDs=false + ``` + + When scoped to a namespace, the operator will: + + - Only watch InfisicalSecrets in the specified namespace + - Only create/update Kubernetes secrets in that namespace + - Only access deployments in that namespace + + The default configuration gives cluster-wide access: + + ```yaml + installCRDs: true # Install CRDs (set to false for additional namespace installations) + scopedNamespace: "" # Empty for cluster-wide access + scopedRBAC: false # Cluster-wide permissions + ``` + + If you want to install operators in multiple namespaces simultaneously: + - Make sure to set `installCRDs: false` for all but one of the installations to avoid conflicts, as CRDs are cluster-wide resources. + - Use unique release names for each installation (e.g., operator-namespace1, operator-namespace2). + + + + +## Custom Resource Definitions + +Currently the operator supports the following CRD's. We are constantly expanding the functionality of the operator, and this list will be updated as new CRD's are added. + +1. [InfisicalSecret](/integrations/platforms/kubernetes/infisical-secret-crd): Sync secrets from Infisical to a Kubernetes secret. +2. [InfisicalPushSecret](/integrations/platforms/kubernetes/infisical-push-secret-crd): Push secrets from a Kubernetes secret to Infisical. +3. [InfisicalDynamicSecret](/integrations/platforms/kubernetes/infisical-dynamic-secret-crd): Sync dynamic secrets and create leases automatically in Kubernetes. + +## General Configuration +### Private/self-signed certificate +To connect to Infisical instances behind a private/self-signed certificate, you can configure the TLS settings in the CRD +to point to a CA certificate stored in a Kubernetes secret resource. + +```yaml +--- +spec: + hostAPI: https://app.infisical.com/api + tls: + caRef: + secretName: custom-ca-certificate + secretNamespace: default + key: ca.crt +--- +``` + + +## Advanced Templating + +With the Infisical Secrets Operator, you can use templating to dynamically generate secrets in Kubernetes. The templating is built on top of [Go templates](https://pkg.go.dev/text/template), which is a powerful and flexible template engine built into Go. + +Please be aware that trying to reference non-existing keys will result in an error. Additionally, each template field is processed individually, which means one template field cannot reference another template field. + + + Please note that templating is currently only supported for the `InfisicalPushSecret` and `InfisicalSecret` CRDs. + + +### Available helper functions + +The Infisical Secrets Operator exposes a wide range of helper functions to make it easier to work with secrets in Kubernetes. + +| Function | Description | Signature | +| -------- | ----------- | --------- | +| `decodeBase64ToBytes` | Given a base64 encoded string, this function will decode the base64-encoded string. | `decodeBase64ToBytes(encodedString string) string` | +| `encodeBase64` | Given a string, this function will encode the string to a base64 encoded string. | `encodeBase64(plainString string) string` | +| `pkcs12key`| Extracts all private keys from a PKCS#12 archive and encodes them in PKCS#8 PEM format. | `pkcs12key(input string) string` | +| `pkcs12keyPass`|Same as pkcs12key. Uses the provided password to decrypt the PKCS#12 archive. | `pkcs12keyPass(pass string, input string) string` | +| `pkcs12cert` | Extracts all certificates from a PKCS#12 archive and orders them if possible. If disjunct or multiple leaf certs are provided they are returned as-is. Sort order: `leaf / intermediate(s) / root`. | `pkcs12cert(input string) string` | +| `pkcs12certPass` | Same as `pkcs12cert`. Uses the provided password to decrypt the PKCS#12 archive. | `pkcs12certPass(pass string, input string) string` | +| `pemToPkcs12` | Takes a PEM encoded certificate and key and creates a base64 encoded PKCS#12 archive. | `pemToPkcs12(cert string, key string) string` | +| `pemToPkcs12Pass` | Same as `pemToPkcs12`. Uses the provided password to encrypt the PKCS#12 archive. | `pemToPkcs12Pass(cert string, key string, pass string) string` | +| `fullPemToPkcs12` | Takes a PEM encoded certificates chain and key and creates a base64 encoded PKCS#12 archive. | `fullPemToPkcs12(cert string, key string) string` | +| `fullPemToPkcs12Pass` | Same as `fullPemToPkcs12`. Uses the provided password to encrypt the PKCS#12 archive. | `fullPemToPkcs12Pass(cert string, key string, pass string) string` | +| `filterPEM` | Filters PEM blocks with a specific type from a list of PEM blocks.. | `filterPEM(pemType string, input string) string` | +| `filterCertChain` | Filters PEM block(s) with a specific certificate type (`leaf`, `intermediate` or `root`) from a certificate chain of PEM blocks (PEM blocks with type `CERTIFICATE`). | `filterCertChain(certType string, input string) string` | +| `jwkPublicKeyPem` | Takes an json-serialized JWK and returns an PEM block of type `PUBLIC KEY` that contains the public key. [See here](https://golang.org/pkg/crypto/x509/#MarshalPKIXPublicKey) for details. | `jwkPublicKeyPem(jwkjson string) string` | +| `jwkPrivateKeyPem` | Takes an json-serialized JWK and returns an PEM block of type `PRIVATE KEY` that contains the private key. [See here](https://pkg.go.dev/crypto/x509#MarshalPKCS8PrivateKey) for details. | `jwkPrivateKeyPem(jwkjson string) string` | +| `toYaml` | Takes an interface, marshals it to yaml. It returns a string, even on marshal error (empty string). | `toYaml(v any) string` | +| `fromYaml` | Function converts a YAML document into a `map[string]any`. | `fromYaml(str string) map[string]any` | + +### Sprig functions + +The Infisical Secrets Operator integrates with the [Sprig library](https://github.com/Masterminds/sprig) to provide additional helper functions. + + + We've removed `expandEnv` and `env` from the supported functions for security reasons. + + + +## Global configuration + +To configure global settings that will apply to all instances of `InfisicalSecret`, you can define these configurations in a Kubernetes ConfigMap. +For example, you can configure all `InfisicalSecret` instances to fetch secrets from a single backend API without specifying the `hostAPI` parameter for each instance. + +### Available global properties + +| Property | Description | Default value | +| -------- | --------------------------------------------------------------------------------- | ----------------------------- | +| hostAPI | If `hostAPI` in `InfisicalSecret` instance is left empty, this value will be used | https://app.infisical.com/api | + +### Applying global configurations + +All global configurations must reside in a Kubernetes ConfigMap named `infisical-config` in the namespace `infisical-operator-system`. +To apply global configuration to the operator, copy the following yaml into `infisical-config.yaml` file. + +```yaml infisical-config.yaml +apiVersion: v1 +kind: Namespace +metadata: + name: infisical-operator-system +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: infisical-config + namespace: infisical-operator-system +data: + hostAPI: https://example.com/api # <-- global hostAPI +``` + +Then apply this change via kubectl by running the following + +```bash +kubectl apply -f infisical-config.yaml +``` + +## Troubleshoot operator + +If the operator is unable to fetch secrets from the API, it will not affect the managed Kubernetes secret. +It will continue attempting to reconnect to the API indefinitely. +The InfisicalSecret resource uses the `status.conditions` field to report its current state and any errors encountered. + +```yaml +$ kubectl get infisicalSecrets +NAME AGE +infisicalsecret-sample 12s + +$ kubectl describe infisicalSecret infisicalsecret-sample +... +Spec: +... +Status: + Conditions: + Last Transition Time: 2022-12-18T04:29:09Z + Message: Infisical controller has located the Infisical token in provided Kubernetes secret + Reason: OK + Status: True + Type: secrets.infisical.com/LoadedInfisicalToken + Last Transition Time: 2022-12-18T04:29:10Z + Message: Failed to update secret because: 400 Bad Request + Reason: Error + Status: False + Type: secrets.infisical.com/ReadyToSyncSecrets +Events: +``` + +## Uninstall Operator + +The managed secret created by the operator will not be deleted when the operator is uninstalled. + + + + Install Infisical Helm repository + ```bash + helm uninstall + ``` + + \ No newline at end of file diff --git a/docs/integrations/secret-syncs/aws-parameter-store.mdx b/docs/integrations/secret-syncs/aws-parameter-store.mdx new file mode 100644 index 000000000..fad37265a --- /dev/null +++ b/docs/integrations/secret-syncs/aws-parameter-store.mdx @@ -0,0 +1,146 @@ +--- +title: "AWS Parameter Store Sync" +description: "Learn how to configure an AWS Parameter Store Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create an [AWS Connection](/integrations/app-connections/aws) with the required **Secret Sync** permissions + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **AWS Parameter Store** option. + ![Select AWS Parameter Store](/images/secret-syncs/aws-parameter-store/select-aws-parameter-store-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/aws-parameter-store/aws-parameter-store-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/aws-parameter-store/aws-parameter-store-destination.png) + + - **AWS Connection**: The AWS Connection to authenticate with. + - **Region**: The AWS region to deploy secrets to. + - **Path**: The AWS Parameter Store path to deploy secrets to. + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/aws-parameter-store/aws-parameter-store-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Parameter Store when keys conflict. + - **Import Secrets (Prioritize AWS Parameter Store)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Parameter Store over Infisical when keys conflict. + - **KMS Key**: The AWS KMS key ID or alias to encrypt parameters with. + - **Tags**: Optional resource tags to add to parameters synced by Infisical. + - **Sync Secret Metadata as Resource Tags**: If enabled, metadata attached to secrets will be added as resource tags to parameters synced by Infisical. + + Manually configured tags from the **Tags** field will take precedence over secret metadata when tag keys conflict. + + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + 6. Configure the **Details** of your Parameter Store Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/aws-parameter-store/aws-parameter-store-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Parameter Store Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/aws-parameter-store/aws-parameter-store-review.png) + + 8. If enabled, your Parameter Store Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/aws-parameter-store/aws-parameter-store-created.png) + + + + To create an **AWS Parameter Store Sync**, make an API request to the [Create AWS + Parameter Store Sync](/api-reference/endpoints/secret-syncs/aws-parameter-store/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/aws-parameter-store \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-parameter-store-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "region": "us-east-1", + "path": "/my-aws/path/" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-parameter-store-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "aws", + "name": "my-aws-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "aws-parameter-store", + "destinationConfig": { + "region": "us-east-1", + "path": "/my-aws/path/" + } + } + } + ``` + + diff --git a/docs/integrations/secret-syncs/aws-secrets-manager.mdx b/docs/integrations/secret-syncs/aws-secrets-manager.mdx new file mode 100644 index 000000000..8ed85be25 --- /dev/null +++ b/docs/integrations/secret-syncs/aws-secrets-manager.mdx @@ -0,0 +1,149 @@ +--- +title: "AWS Secrets Manager Sync" +description: "Learn how to configure an AWS Secrets Manager Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create an [AWS Connection](/integrations/app-connections/aws) with the required **Secret Sync** permissions + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **AWS Secrets Manager** option. + ![Select AWS Secrets Manager](/images/secret-syncs/aws-secrets-manager/select-aws-secrets-manager-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-destination.png) + + - **AWS Connection**: The AWS Connection to authenticate with. + - **Region**: The AWS region to deploy secrets to. + - **Mapping Behavior**: Specify how Infisical should map secrets to AWS Secrets Manager: + - **One-To-One**: Each Infisical secret will be mapped to a separate AWS Secrets Manager secret. + - **Many-To-One**: All Infisical secrets will be mapped to a single AWS Secrets Manager secret. + - **Secret Name**: Specifies the name of the AWS Secret to map secrets to if **Many-To-One** mapping behavior is selected. + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict. + - **Import Secrets (Prioritize AWS Secrets Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict. + - **KMS Key**: The AWS KMS key ID or alias to encrypt secrets with. + - **Tags**: Optional tags to add to secrets synced by Infisical. + - **Sync Secret Metadata as Tags**: If enabled, metadata attached to secrets will be added as tags to secrets synced by Infisical. + + Manually configured tags from the **Tags** field will take precedence over secret metadata when tag keys conflict. + + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + 6. Configure the **Details** of your Secrets Manager Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Secrets Manager Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-review.png) + + 8. If enabled, your Secrets Manager Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-created.png) + + + + To create an **AWS Secrets Manager Sync**, make an API request to the [Create AWS + Secrets Manager Sync](/api-reference/endpoints/secret-syncs/aws-secrets-manager/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/aws-secrets-manager \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-secrets-manager-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "region": "us-east-1", + "mappingBehavior": "one-to-one" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-secrets-manager-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "aws", + "name": "my-aws-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "aws-secrets-manager", + "destinationConfig": { + "region": "us-east-1", + "mappingBehavior": "one-to-one" + } + } + } + ``` + + diff --git a/docs/integrations/secret-syncs/azure-app-configuration.mdx b/docs/integrations/secret-syncs/azure-app-configuration.mdx new file mode 100644 index 000000000..35a577872 --- /dev/null +++ b/docs/integrations/secret-syncs/azure-app-configuration.mdx @@ -0,0 +1,148 @@ +--- +title: "Azure App Configuration Sync" +description: "Learn how to configure an Azure App Configuration Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create an [Azure App Configuration Connection](/integrations/app-connections/azure-app-configuration) + + + The Azure App Configuration Secret Sync requires the following permissions to be set on the user / service principal + for Infisical to sync secrets to Azure App Configuration: `Read Key-Value`, `Write Key-Value`, `Delete Key-Value`. + + Any role with these permissions would work such as the **App Configuration Data Owner** role. Alternatively, you can use the **App Configuration Data Contributor** role for read/write access. + + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **Azure App Configuration** option. + ![Select Azure App Configuration](/images/secret-syncs/azure-app-configuration/select-app-config.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/azure-app-configuration/app-config-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/azure-app-configuration/app-config-destination.png) + + - **Azure Connection**: The Azure Connection to authenticate with. + - **Configuration URL**: The URL of your Azure App Configuration. + - **Label**: An optional label to attach to all secrets created by Infisical inside your Azure App Configuration. + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/azure-app-configuration/app-config-options.png) + + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict. + - **Import Secrets (Prioritize Azure App Configuration)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict. + + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + 6. Configure the **Details** of your Azure App Configuration Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/azure-app-configuration/app-config-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Azure App Configuration Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/azure-app-configuration/app-config-review.png) + + 8. If enabled, your Azure App Configuration Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/azure-app-configuration/app-config-synced.png) + + + + To create an **Azure App Configuration Sync**, make an API request to the [Create Azure App Configuration Sync](/api-reference/endpoints/secret-syncs/azure-app-configuration/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/azure-app-configuration \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-azure-app-configuration-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "configurationUrl": "https://my-azure-app-configuration.azconfig.io", + "label": "my-label" + } + }' + ``` + + ### Sample response + + ```json Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-azure-app-configuration-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "azure", + "name": "my-azure-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "azure-app-configuration", + "destinationConfig": { + "configurationUrl": "https://my-azure-app-configuration.azconfig.io", + "label": "my-label" + } + } + } + ``` + + diff --git a/docs/integrations/secret-syncs/azure-key-vault.mdx b/docs/integrations/secret-syncs/azure-key-vault.mdx new file mode 100644 index 000000000..5f55a73ae --- /dev/null +++ b/docs/integrations/secret-syncs/azure-key-vault.mdx @@ -0,0 +1,148 @@ +--- +title: "Azure Key Vault Sync" +description: "Learn how to configure a Azure Key Vault Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create an [Azure Key Vault Connection](/integrations/app-connections/azure-key-vault) + + + The Azure Key Vault Secret Sync requires the following secrets permissions to be set on the user / service principal + for Infisical to sync secrets to Azure Key Vault: `secrets/list`, `secrets/get`, `secrets/set`, `secrets/recover`. + + Any role with these permissions would work such as the **Key Vault Secrets Officer** role. + + + + Secrets in Infisical that contain an underscore (`_`) will be converted to a hyphen (`-`) when synced to Azure Key Vault. + + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **Azure Key Vault** option. + ![Select Key Vault](/images/secret-syncs/azure-key-vault/select-key-vault-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/azure-key-vault/vault-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/azure-key-vault/vault-destination.png) + + - **Azure Connection**: The Azure Connection to authenticate with. + - **Vault Base URL**: The URL of your Azure Key Vault. +

+ + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/azure-key-vault/vault-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict. + - **Import Secrets (Prioritize Azure Key Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + 6. Configure the **Details** of your Azure Key Vault Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/azure-key-vault/vault-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Azure Key Vault Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/azure-key-vault/vault-review.png) + + 8. If enabled, your Azure Key Vault Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/azure-key-vault/vault-synced.png) + + + + To create a **Azure Key Vault Sync**, make an API request to the [Create Key Vault Sync](/api-reference/endpoints/secret-syncs/azure-key-vault/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/azure-key-vault \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-key-vault-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "vaultBaseUrl": "https://my-key-vault.vault.azure.net" + } + }' + ``` + + ### Sample response + + ```json Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-key-vault-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "azure", + "name": "my-azure-key-vault-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "azure-key-vault", + "destinationConfig": { + "vaultBaseUrl": "https://my-key-vault.vault.azure.net" + } + } + } + ``` + + diff --git a/docs/integrations/secret-syncs/camunda.mdx b/docs/integrations/secret-syncs/camunda.mdx new file mode 100644 index 000000000..5ed2cd9ae --- /dev/null +++ b/docs/integrations/secret-syncs/camunda.mdx @@ -0,0 +1,139 @@ +--- +title: "Camunda Sync" +description: "Learn how to configure a Camunda Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create a [Camunda Connection](/integrations/app-connections/camunda) + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **Camunda** option. + ![Select Camunda](/images/secret-syncs/camunda/select-camunda-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/camunda/camunda-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/camunda/camunda-destination.png) + + - **Camunda Connection**: The Camunda Connection to authenticate with. + - **Cluster**: The Camunda cluster to sync connector secrets to. + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/camunda/camunda-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Camunda when keys conflict. + - **Import Secrets (Prioritize Camunda)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Camunda over Infisical when keys conflict. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + 6. Configure the **Details** of your Camunda Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/camunda/camunda-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Camunda Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/camunda/camunda-review.png) + + 8. If enabled, your Camunda Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/camunda/camunda-created.png) + + + + To create an **Camunda Sync**, make an API request to the [Create Camunda Sync](/api-reference/endpoints/secret-syncs/camunda/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/camunda \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-camunda-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "scope": "cluster", + "clusterUUID": "cc4c8dae-dce9-4f4c-9882-132b2bd65fa5" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-camunda-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "camunda", + "name": "my-camunda-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "camunda", + "destinationConfig": { + "scope": "cluster", + "clusterUUID": "cc4c8dae-dce9-4f4c-9882-132b2bd65fa5" + } + } + } + ``` + + + diff --git a/docs/integrations/secret-syncs/databricks.mdx b/docs/integrations/secret-syncs/databricks.mdx new file mode 100644 index 000000000..c9db5f88a --- /dev/null +++ b/docs/integrations/secret-syncs/databricks.mdx @@ -0,0 +1,143 @@ +--- +title: "Databricks Sync" +description: "Learn how to configure a Databricks Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create a [Databricks Connection](/integrations/app-connections/databricks) + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **Databricks** option. + ![Select Databricks](/images/secret-syncs/databricks/select-databricks-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/databricks/databricks-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/databricks/databricks-destination.png) + + - **Databricks Connection**: The Databricks Connection to authenticate with. + - **Scope**: The Databricks secret scope to sync secrets to. + + + You must create a secret scope in your Databricks workspace prior to configuration. Ensure your service principal has [Write permissions](https://docs.databricks.com/en/security/auth/access-control/index.html#secret-acls) for the specified secret scope. + + Infisical recommends creating a designated Databricks secret scope for your sync to prevent removal of secrets not managed by Infisical. + + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/databricks/databricks-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + + Databricks does not support importing secrets. + + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + 6. Configure the **Details** of your Databricks Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/databricks/databricks-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Databricks Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/databricks/databricks-review.png) + + 8. If enabled, your Databricks Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/databricks/databricks-created.png) + + + + To create an **Databricks Sync**, make an API request to the [Create Databricks Sync](/api-reference/endpoints/secret-syncs/databricks/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/databricks \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-databricks-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "scope": "my-scope" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-databricks-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "databricks", + "name": "my-databricks-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "databricks", + "destinationConfig": { + "scope": "my-scope" + } + } + } + ``` + + diff --git a/docs/integrations/secret-syncs/gcp-secret-manager.mdx b/docs/integrations/secret-syncs/gcp-secret-manager.mdx new file mode 100644 index 000000000..72c932116 --- /dev/null +++ b/docs/integrations/secret-syncs/gcp-secret-manager.mdx @@ -0,0 +1,142 @@ +--- +title: "GCP Secret Manager Sync" +description: "Learn how to configure a GCP Secret Manager Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create a [GCP Connection](/integrations/app-connections/gcp) with the required **Secret Sync** permissions + - Enable **Cloud Resource Manager API** and **Secret Manager API** on your GCP project + ![Secret Syncs Tab](/images/secret-syncs/gcp-secret-manager/enable-resource-manager-api.png) + ![Secret Syncs Tab](/images/secret-syncs/gcp-secret-manager/enable-secret-manager-api.png) + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **GCP Secret Manager** option. + ![Select GCP Secret Manager](/images/secret-syncs/gcp-secret-manager/select-gcp-secret-manager-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-destination.png) + + - **GCP Connection**: The GCP Connection to authenticate with. + - **Project**: The GCP project to sync with. + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over GCP Secret Manager when keys conflict. + - **Import Secrets (Prioritize GCP Secret Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from GCP Secret Manager over Infisical when keys conflict. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + 6. Configure the **Details** of your GCP Secret Manager Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Secret Manager Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-review.png) + + 8. If enabled, your GCP Secret Manager Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-created.png) + + + + To create a **GCP Secret Manager Sync**, make an API request to the [Create GCP + Secret Manager Sync](/api-reference/endpoints/secret-syncs/gcp-secret-manager/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/gcp-secret-manager \ + --header 'Content-Type: application/json' \ + --data '{ + "destinationConfig": { + "scope": "global", + "projectId": "infisical-test-playground" + }, + "name": "my-gcp-sync", + "description": "this is an example secret sync", + "secretPath": "/", + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "isAutoSyncEnabled": true, + "connectionId": "eec83609-5eb4-4d8d-9f6e-ded016984f0d", + "environment": "dev", + "projectId": "09eda1f8-85a3-47a9-8a6f-e27f133b2a36" + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "aee02c4a-4a5f-488c-82dd-0b3164772871", + "name": "my-gcp-sync", + "description": "this is an example secret sync", + "isAutoSyncEnabled": true, + "version": 1, + "projectId": "09eda1f8-85a3-47a9-8a6f-e27f133b2a36", + "folderId": "1447389e-16fb-49ba-96fd-361b5a2522af", + "connectionId": "eec83609-5eb4-4d8d-9f6e-ded016984f0d", + "createdAt": "2025-01-27T12:28:59.408Z", + "updatedAt": "2025-01-27T12:28:59.408Z", + "syncStatus": "pending", + "lastSyncJobId": null, + "lastSyncMessage": null, + "lastSyncedAt": null, + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "connection": { + "app": "gcp", + "name": "my-gcp-connection", + "id": "eec83609-5eb4-4d8d-9f6e-ded016984f0d" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "124e0392-4070-4b1c-900e-ced30cd55bf3" + }, + "folder": { + "id": "1447389e-16fb-49ba-96fd-361b5a2522af", + "path": "/" + }, + "destination": "gcp-secret-manager", + "destinationConfig": { + "projectId": "infisical-test-playground" + } + } + } + ``` + + + diff --git a/docs/integrations/secret-syncs/github.mdx b/docs/integrations/secret-syncs/github.mdx new file mode 100644 index 000000000..d55ec3d0b --- /dev/null +++ b/docs/integrations/secret-syncs/github.mdx @@ -0,0 +1,163 @@ +--- +title: "GitHub Sync" +description: "Learn how to configure a GitHub Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create a [GitHub Connection](/integrations/app-connections/github) + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **GitHub** option. + ![Select GitHub](/images/secret-syncs/github/select-github-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/github/github-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/github/github-destination.png) + + - **GitHub Connection**: The GitHub Connection to authenticate with. + - **Scope**: The GitHub secret scope to sync secrets to. + - **Organization**: Sync secrets to a specific organization. + - **Repository**: Sync secrets to a specific repository. + - **Repository Environment**: Sync secrets to a specific repository's environment. +

+ The remaining fields are determined by the selected **Scope**: + + + - **Organization**: The organization to deploy secrets to. + - **Visibility**: Determines which organization repositories can access deployed secrets. + - **All Repositories**: All repositories of the organization. (Public repositories if not a Pro/Team account) + - **Private Repositories**: All private repositories of the organization. (Requires Pro/Team account) + - **Selected Repositories**: Only the selected Repositories. + - **Selected Repositories**: The selected repositories if **Visibility** is set to **Selected Repositories**. + + + - **Repository**: The repository to deploy secrets to. + + + - **Repository**: The repository to deploy secrets to. + - **Environment**: The repository's environment to deploy secrets to. + + + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/github/github-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + + GitHub does not support importing secrets. + + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + 6. Configure the **Details** of your GitHub Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/github/github-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your GitHub Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/github/github-review.png) + + 8. If enabled, your GitHub Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/github/github-created.png) + + + + To create an **GitHub Sync**, make an API request to the [Create GitHub Sync](/api-reference/endpoints/secret-syncs/github/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/github \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-github-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "scope": "repository", + "owner": "my-github", + "repo": "my-repository" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-github-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "github", + "name": "my-github-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "github", + "destinationConfig": { + "scope": "repository", + "owner": "my-github", + "repo": "my-repository" + } + } + } + ``` + + diff --git a/docs/integrations/secret-syncs/humanitec.mdx b/docs/integrations/secret-syncs/humanitec.mdx new file mode 100644 index 000000000..e8cd7eafc --- /dev/null +++ b/docs/integrations/secret-syncs/humanitec.mdx @@ -0,0 +1,157 @@ +--- +title: "Humanitec Sync" +description: "Learn how to configure a Humanitec Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create a [Humanitec Connection](/integrations/app-connections/humanitec) + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **Humanitec** option. + ![Select Humanitec](/images/secret-syncs/humanitec/select-humanitec-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/humanitec/humanitec-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/humanitec/humanitec-destination.png) + + - **Humanitec Connection**: The Humanitec Connection to authenticate with. + - **Scope**: The Humanitec secret scope to sync secrets to. + - **Application**: Sync secrets to a specific application. + - **Environment**: Sync secrets to a specific environment of an application. +

+ The remaining fields are determined by the selected **Scope**: + + + - **Organization**: The organization to deploy secrets to. + - **App**: The application to deploy secrets to. + + + - **Organization**: The organization to deploy secrets to. + - **App**: The application to deploy secrets to. + - **Environment**: The environment to deploy secrets to. + + + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/humanitec/humanitec-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + + Humanitec does not support importing secrets. + + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + 6. Configure the **Details** of your Humanitec Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/humanitec/humanitec-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Humanitec Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/humanitec/humanitec-review.png) + + 8. If enabled, your Humanitec Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/humanitec/humanitec-created.png) + + + + To create an **Humanitec Sync**, make an API request to the [Create Humanitec Sync](/api-reference/endpoints/secret-syncs/humanitec/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/humanitec \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-humanitec-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "scope": "application", + "app": "my-app", + "environment": "development" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-humanitec-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "humanitec", + "name": "my-humanitec-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "humanitec", + "destinationConfig": { + "scope": "application", + "org": "my-organization", + "app": "my-app", + "env": "development" + } + } + } + ``` + + diff --git a/docs/integrations/secret-syncs/overview.mdx b/docs/integrations/secret-syncs/overview.mdx new file mode 100644 index 000000000..0df04cbb7 --- /dev/null +++ b/docs/integrations/secret-syncs/overview.mdx @@ -0,0 +1,96 @@ +--- +sidebarTitle: "Overview" +description: "Learn how to sync secrets to third-party services with Infisical." +--- + +Secret Syncs enable you to sync secrets from Infisical to third-party services using [App Connections](/integrations/app-connections/overview). + + + Secret Syncs will gradually replace Native Integrations as they become available. Native Integrations will be deprecated in the future, so opt for configuring a Secret Sync when available. + + +## Concept + +Secret Syncs are a project-level resource used to sync secrets, via an [App Connection](/integrations/app-connections/overview), from a particular project environment and folder path (source) +to a third-party service (destination). Changes to the source will automatically be propagated to the destination, ensuring +your secrets are always up-to-date. + +
+ +

+ + ```mermaid + %%{init: {'flowchart': {'curve': 'linear'} } }%% + graph LR + A[App Connection] + B[Secret Sync] + C[Secret 1] + D[Secret 2] + E[Secret 3] + F[Third-Party Service] + G[Secret 1] + H[Secret 2] + I[Secret 3] + J[Project Source] + + B --> A + C --> J + D --> J + E --> J + A --> F + F --> G + F --> H + F --> I + J --> B + + classDef default fill:#ffffff,stroke:#666,stroke-width:2px,rx:10px,color:black + classDef connection fill:#FFF2B2,stroke:#E6C34A,stroke-width:2px,color:black,rx:15px + classDef secret fill:#E6F4FF,stroke:#0096D6,stroke-width:2px,color:black,rx:15px + classDef sync fill:#F4FFE6,stroke:#96D600,stroke-width:2px,color:black,rx:15px + classDef service fill:#E6E6FF,stroke:#6B4E96,stroke-width:2px,color:black,rx:15px + classDef project fill:#FFE6E6,stroke:#D63F3F,stroke-width:2px,color:black,rx:15px + + class A connection + class B sync + class C,D,E,G,H,I secret + class F project + class J service + ``` + +
+ +## Workflow + +Configuring a Secret Sync requires three components: a source location to retrieve secrets from, +a destination endpoint to deploy secrets to, and configuration options to determine how your secrets +should be synced. Follow these steps to start syncing: + + + For step-by-step guides on syncing to a particular third-party service, refer to the Secret Syncs section in the Navigation Bar. + + +1. Create App Connection: If you have not already done so, create an [App Connection](/integrations/app-connections/overview) +via the UI or API for the third-party service you intend to sync secrets to. + +2. Create Secret Sync: Configure a Secret Sync in the desired project by specifying the following parameters via the UI or API: + - Source: The project environment and folder path you wish to retrieve secrets from. + - Destination: The App Connection to utilize and the destination endpoint to deploy secrets to. These can vary between services. + - Options: Customize how secrets should be synced, such as whether or not secrets should be imported from the destination on the initial sync. + + + Secret Syncs are the source of truth for connected third-party services. Any secret, + including associated data, not present or imported in Infisical before syncing will be + overwritten, and changes made directly in the connected service outside of infisical may also + be overwritten by future syncs. + + + + Some third-party services do not support importing secrets. + + +3. Utilize Sync: Any changes to the source location will now automatically be propagated to the destination endpoint. + + + Infisical is continuously expanding it's Secret Sync third-party service support. If the service you need isn't available, + you can still use our Native Integrations in the interim, or contact us at team@infisical.com to make a request . + \ No newline at end of file diff --git a/docs/integrations/secret-syncs/terraform-cloud.mdx b/docs/integrations/secret-syncs/terraform-cloud.mdx new file mode 100644 index 000000000..80a087d2b --- /dev/null +++ b/docs/integrations/secret-syncs/terraform-cloud.mdx @@ -0,0 +1,161 @@ +--- +title: "Terraform Cloud Sync" +description: "Learn how to configure a Terraform Cloud Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create a [Terraform Cloud Connection](/integrations/app-connections/terraform-cloud) + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **Terraform Cloud** option. + ![Select Terraform Cloud](/images/secret-syncs/terraform-cloud/terraform-cloud-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/terraform-cloud/terraform-cloud-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/terraform-cloud/terraform-cloud-destination.png) + + - **Terraform Cloud Connection**: The Terraform Cloud Connection to authenticate with. + - **Organization**: The Terraform Cloud organization to deploy secrets to. + - **Category**: The Terraform Cloud variable category to use on secrets syncs. Choose from: + - **Environment**: Sync secrets as environment variables. + - **Terraform**: Sync secrets as Terraform variables. + - **Scope**: The Terraform Cloud secret scope to sync secrets to. + - **Variable Set**: Sync secrets to a specific variable set. + - **Workspace**: Sync secrets to a specific workspace. +

+ The remaining fields are determined by the selected **Scope**: + + + - **Variable Set**: The variable set to deploy secrets to. + + + - **Workspace**: The workspace to deploy secrets to. + + + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/terraform-cloud/terraform-cloud-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + + Terraform Cloud does not support importing secrets. + + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + 6. Configure the **Details** of your Terraform Cloud Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/terraform-cloud/terraform-cloud-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Terraform Cloud Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/terraform-cloud/terraform-cloud-review.png) + + 8. If enabled, your Terraform Cloud Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/terraform-cloud/terraform-cloud-created.png) + + + + To create an **Terraform Cloud Sync**, make an API request to the [Create Terraform Cloud Sync](/api-reference/endpoints/secret-syncs/terraform-cloud/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/terraform-cloud \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-terraform-cloud-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "scope": "variable-set", + "variableSetId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "variableSetName": "my-variable-set", + "org": "my-organization-id", + "category": "env" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-terraform-cloud-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "terraform-cloud", + "name": "my-terraform-cloud-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "terraform-cloud", + "destinationConfig": { + "scope": "workspace", + "workspaceId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "workspaceName": "my-workspace", + "org": "my-organization-id", + "category": "terraform" + } + } + } + ``` + + diff --git a/docs/integrations/secret-syncs/vercel.mdx b/docs/integrations/secret-syncs/vercel.mdx new file mode 100644 index 000000000..593874dee --- /dev/null +++ b/docs/integrations/secret-syncs/vercel.mdx @@ -0,0 +1,148 @@ +--- +title: "Vercel Sync" +description: "Learn how to configure a Vercel Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create a [Vercel Connection](/integrations/app-connections/vercel) + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **Vercel** option. + ![Select Vercel](/images/secret-syncs/vercel/select-vercel-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/vercel/vercel-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/vercel/vercel-destination.png) + + - **Vercel Connection**: The Vercel Connection to authenticate with. + - **Vercel App**: The application to deploy secrets to. + - **Vercel App Environment**: The environment to deploy secrets to. + - **Vercel Preview Branch (Optional)**: Specify a branch for preview deployments if needed. + + After configuring these parameters, click the **Next** button to continue to the Sync Options step. + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/vercel/vercel-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Vercel when keys conflict. + - **Import Secrets (Prioritize Vercel)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Vercel over Infisical when keys conflict. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + 6. Configure the **Details** of your Vercel Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/vercel/vercel-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Vercel Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/vercel/vercel-review.png) + + 8. If enabled, your Vercel Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/vercel/vercel-created.png) + + + + To create an **Vercel Sync**, make an API request to the [Create Vercel Sync](/api-reference/endpoints/secret-syncs/vercel/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/vercel \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-vercel-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "app": "prj_bz7zgHvQETPvJWc5tmIr0tGRH9kE", + "env": "preview", + "branch": "test", + "appName": "nextjs-boilerplate", + "teamId": "team_0d444b5088888dd257" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-vercel-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "vercel", + "name": "my-vercel-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "vercel", + "destinationConfig": { + "app": "prj_bz7zgHvQETPvJWc5tmIr0tGRH9kE", + "env": "preview", + "branch": "test", + "appName": "nextjs-boilerplate", + "teamId": "team_0d444b5088888dd257" + } + } + } + ``` + + diff --git a/docs/integrations/secret-syncs/windmill.mdx b/docs/integrations/secret-syncs/windmill.mdx new file mode 100644 index 000000000..90d35f8b8 --- /dev/null +++ b/docs/integrations/secret-syncs/windmill.mdx @@ -0,0 +1,147 @@ +--- +title: "Windmill Sync" +description: "Learn how to configure a Windmill Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create a [Windmill Connection](/integrations/app-connections/windmill) with the required **Secret Sync** permissions + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **Windmill** option. + ![Select Windmill](/images/secret-syncs/windmill/select-windmill-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/windmill/windmill-sync-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/windmill/windmill-sync-destination.png) + + - **Windmill Connection**: The Windmill Connection to authenticate with. + - **Workspace**: The Windmill workspace to sync secrets to. + - **Path**: The workspace path to sync secrets to. + + + Workspace path must conform to Windmill's [owner path convention](https://www.windmill.dev/docs/core_concepts/roles_and_permissions#path). + + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/windmill/windmill-sync-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Windmill when keys conflict. + - **Import Secrets (Prioritize Windmill)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Windmill over Infisical when keys conflict. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + 6. Configure the **Details** of your Windmill Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/windmill/windmill-sync-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Windmill Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/windmill/windmill-sync-review.png) + + 8. If enabled, your Windmill Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/windmill/windmill-sync-created.png) + + + + To create an **Windmill Sync**, make an API request to the [Create Windmill Sync](/api-reference/endpoints/secret-syncs/windmill/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/windmill \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-windmill-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "workspace": "my-workspace", + "path": "f/folder/path/" + } + }' + ``` + + + Workspace path must conform to Windmill's [owner path convention](https://www.windmill.dev/docs/core_concepts/roles_and_permissions#path). + + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-windmill-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "windmill", + "name": "my-windmill-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "windmill", + "destinationConfig": { + "workspace": "my-workspace", + "path": "f/folder/path/" + } + } + } + ``` + + diff --git a/docs/internals/components.mdx b/docs/internals/components.mdx index 65506a500..bad4dc076 100644 --- a/docs/internals/components.mdx +++ b/docs/internals/components.mdx @@ -1,28 +1,34 @@ --- title: "Components" -description: "Infisical's components span multiple clients, an API, and a storage backend." +description: "Understand Infisical's core architectural components and how they work together." --- -## Infisical API +## Overview -The Infisical API (sometimes referred to as the **backend**) contains the core platform logic. +Infisical is architected around several key components that work in concert to provide a secure and streamlined secret management experience. These components span the client, API, and storage layers, ensuring that secrets are protected at every stage of their lifecycle. -## Storage backend +## 1. API (Backend) -Infisical relies on a storage backend to store data including users and secrets. Infisical's storage backend is Postgres. +Infisical exposes a well-documented [REST API](https://infisical.com/docs/api-reference/overview/introduction) that enables programmatic interaction with the platform, enabling a wide range of use cases. -## Redis +## 2. Storage Backend -Infisical uses [Redis](https://redis.com) to enable more complex workflows including a queuing system to manage long running asynchronous tasks, cron jobs, as well as reliable cache for frequently used resources. +Infisical relies on a robust storage backend to durably store secrets, users, and other platform data. Infisical's storage backend is [PostgreSQL](https://www.postgresql.org/). -## Infisical Web UI +## 3. Caching Layer -The Web UI is the browser-based portal that connects to the Infisical API. +Infisical uses [Redis](https://redis.com) to enable more complex workflows including a queuing system to manage long-running asynchronous tasks, cron jobs, as well as reliable cache for frequently used resources. -## Infisical clients +## 4. Clients -Clients are any application or infrastructure that connecting to the Infisical API using one of the below methods: -- Public API: Making API requests directly to the Infisical API. -- Client SDK: A platform-specific library with method abstractions for working with secrets. Currently, there are three official SDKs: [Node SDK](https://infisical.com/docs/sdks/languages/node), [Python SDK](https://infisical.com/docs/sdks/languages/python), and [Java SDK](https://infisical.com/docs/sdks/languages/java). -- CLI: A terminal-based interface for interacting with the Infisical API. -- Kubernetes Operator: This operator retrieves secrets from Infisical and securely store +Clients are interfaces through which users and applications interact with the Infisical API: + +- **Web UI**: A browser-based portal providing a user-friendly interface for managing secrets, configurations, and performing administrative tasks. + +- [**CLI**](https://infisical.com/docs/cli): A terminal-based tool for interacting with the Infisical API, enabling automation, scripting, and integration into CI/CD pipelines. + +- **SDKs (Software Development Kits)**: Platform-specific libraries with method abstractions for working with secrets. Supported languages include [Node.js](https://infisical.com/docs/sdks/languages/node), [Python](https://infisical.com/docs/sdks/languages/python), [Java](https://infisical.com/docs/sdks/languages/java), [Golang](https://infisical.com/docs/sdks/languages/go), [Ruby](https://infisical.com/docs/sdks/languages/ruby) and [.NET](https://infisical.com/docs/sdks/languages/csharp). + +- [**Kubernetes Operator**](https://infisical.com/docs/integrations/platforms/kubernetes): A Kubernetes-native component that facilitates the secure retrieval and management of secrets within a Kubernetes cluster. The operator supports multiple custom resource definitions (CRDs) for syncing secrets. + +- [**Infisical Agent**](https://infisical.com/docs/integrations/platforms/infisical-agent): Daemon that automatically fetches and manages access tokens and secrets to be used in various client resources. diff --git a/docs/internals/flows.mdx b/docs/internals/flows.mdx deleted file mode 100644 index 0da671d64..000000000 --- a/docs/internals/flows.mdx +++ /dev/null @@ -1,97 +0,0 @@ ---- -title: "Flows" -description: "Infisical's core flows have strong cryptographic underpinnings." ---- - -## Signup - -When a user signs up for an account using email/password, they verify their email by correctly entering the 6-digit OTP code sent to it. - -After this procedure, the user creates a password that is checked against strict requirements to ensure that it has sufficient entropy; this is critical because passwords have both authentication-related and cryptographic implications in Infisical. In accordance to the [secure remote password protocol (SRP)](https://en.wikipedia.org/wiki/Secure_Remote_Password_protocol), the password is used to generate a salt and X; this is kept handy on the client side. - -Next, a few user-associated symmetric keys are generated for subsequent use: - -- The password is transformed into a 256-bit symmetric key, called the generated key, using the [Argon2id](https://en.wikipedia.org/wiki/Argon2) key derivation function. -- A 256-bit symmetric key, called the protected key, is generated. -- A public-private key pair is generated. - -The symmetric keys are used in sequence to encrypt the user’s private key: - -- The protected key is used to encrypt the private key. -- The generated key is used to encrypt the protected key. - -Finally, the encrypted private key, the protected key, salt, and X are sent to the Infisical API to be stored in the storage backend. Note that the top-level secret used to secure the user’s account and private key is their password. Therefore, it must be unknown to the Infisical API and strong by nature. - -## Login - -When a user logs in, they enter their password to authenticate with Infisical via SRP. If successful, the encrypted protected key and encrypted private key are returned to the client side. - -The password is then used in reverse sequence to decrypt the private key: - -- The password is transformed back into the generated key. -- The generated key is used to decrypt the encrypted protected key. -- The protected key is used to decrypt the encrypted private key. - -The private key is stored on the client side and kept handy. - -## Single sign-on - -When a SSO authentication method like Google, GitHub, or SAML SSO is used to login or signup to Infisical, the process is identical to logging in with email/password except that it is contingent on first successfully logging in via the authentication provider. This means, for example, a user with Google SSO enabled must first log in with Google and then enter their password for Infisical to complete logging into the platform. - -This approach implies that the user’s password assumes only the role of a master decryption key or secret. It also ensures that the authentication provider does not know this top-level secret, keeping the platform zero-knowledge as intended. - -## Account recovery - -When a user signs up for Infisical, they are issued a backup PDF containing a symmetric key that can be used to recover their account by decrypting a copy of that user’s private key; using the backup PDF is the only way to recover a user’s account in the event of a lockout - this is intentional by design of Infisical’s zero-knowledge architecture. - -We strongly encourage all users to download, print, and keep their backup PDFs in a secure location. - -## Secrets - -In Infisical, secrets belong to environments in projects, and projects belong to organizations. Each project can be thought of as a vault and has its own symmetric key, called the project key. The project key is used to encrypt the secrets contained in that project. - -Similar to each user’s private key, the project key is sensitive and must remain unknown to the server to preserve the zero-knowledge aspect of Infisical; knowledge of the project key would allow the server to decrypt the secrets of that project which would be undesirable if the server is compromised. - -In order to preserve the zero-knowledge aspect of Infisical, each project key is encrypted on the client side before being sent to the server. More specifically, for each project, we make copies of its project key for each member of that project; each copy is encrypted under that member’s public key and only then sent off to the server for storage. A few relevant sequences: - -- The initial member of a project generates its project key, encrypts it under their public key, and uploads it to the server for storage. -- When a new member is added to the project, an existing member of the project (e.g. the initial member) fetches their copy of the project key, decrypts that copy, encrypts it under the public key of the new member, and uploads it to the server for storage. -- When a member is removed from a project, their copy of the project key is hard deleted from the storage backend. - -When dealing with secrets, this implies a specific sequence of decryption/encryption steps to fetch and create/update them. Assuming that we’re dealing with the Infisical Web UI, let’s start with fetching secrets which happens after the user logs in and selects a project: - -- The user fetches encrypted secrets back to the client side. -- The user also fetches the encrypted project key, encrypted under their public key, for these secrets. -- The encrypted project key is decrypted by the user’s private key which is kept handy on the client side. -- The project key is finally used to decrypt the secrets belonging to the project. -- The secrets are displayed to the user in the Infisical Web UI. - -Similarly, when a user creates/updates a secret, the reverse sequence is performed: - -- The user fetches the encrypted project key, encrypted under the user’s public key. -- The project key is decrypted by the user’s private key which is kept handy on the client side. -- The user encrypts the new/updated secret under the project key. -- The user sends the new/updated secret to the server for storage. - -These sequences are performed across various Infisical clients including the web UI, CLI, SDKs, and K8s operators when dealing with the Infisical API. They are also relevant in the implementations of Infisical’s versioning features like secret versions and snapshots. - -## Native integrations - -Previously, we mentioned that Infisical is zero-knowledge; this is partly true because Infisical can be used this way. Under certain circumstances, however, a user can explicitly share their copy of the project key with the server to enable more advanced features like native integrations. - -The way a project key is shared with Infisical is via an abstraction that we call a bot. Each project has a bot with a public-private key pair generated on the server; the private key of each bot is symmetrically encrypted by the root encryption key of the server. This implies a few things: - -- The server may partake in the sharing of project keys via its own public-private keys bound to each project bot. -- The server root encryption key must be kept secure. - -With that, let’s discuss native integrations. A native integrations is a connection between Infisical and a target platform like GitHub, GitLab, or Vercel that allows secrets to be synced from Infisical to the target platform using its API. Since native integrations require secrets to be sent over in plaintext, they require the server to have access to the secrets. The sequence for how integrations are implemented is fairly simple: - -- A user explicitly shares copy of the project key with the server via the Infisical Web UI. In this step, the user fetches the public key of the bot assigned to that project, encrypts the project key under that public key, and sends it back to the server. -- The user selects a target platform to integrate with their project and enters details such as the source environment within the project to send secrets from as well as the project and environment in the target platform to sync secrets to. -- The user creates the integration, triggering the first sync wherein Infisical decrypts the project’s key, uses it to decrypt the secrets of that project, and sends the secrets to the target platform. -- Finally, on any subsequent mutations applied to the source environment of an active integration, Infisical automatically triggers a re-sync to the target platform. This keeps Infisical as a ground source-of-truth for a team’s secrets. - -## Resources - -- For in depth details, consult the code. -- To get started with Infisical, try out the [Getting Started](https://infisical.com/docs/documentation/getting-started/introduction) overview. \ No newline at end of file diff --git a/docs/internals/overview.mdx b/docs/internals/overview.mdx index a80ebf294..510a6b06c 100644 --- a/docs/internals/overview.mdx +++ b/docs/internals/overview.mdx @@ -6,24 +6,23 @@ description: "Read how Infisical works under the hood." This section covers the internals of Infisical including its technical underpinnings, architecture, and security properties. - Knowledge of this section is recommended but not required to use Infisical. However, if you're operating Infisical, we recommend understanding the internals. + Knowledge of this section is recommended but not required to use Infisical. + However, if you're operating Infisical, we recommend understanding the + internals. ## Learn More - - Learn about the fundamental parts of Infisical. - - - Find out more about the structure of core user flows in Infisical. - + Learn about the fundamental parts of Infisical. + + Read about most common security-related topics and questions. - Learn best practices for utilizing Infisical service tokens. Please note that service tokens are now deprecated and will be removed entirely in the future. + Learn best practices for utilizing Infisical service tokens. Please note + that service tokens are now deprecated and will be removed entirely in the + future. diff --git a/docs/internals/permissions.mdx b/docs/internals/permissions.mdx deleted file mode 100644 index 1fce09def..000000000 --- a/docs/internals/permissions.mdx +++ /dev/null @@ -1,211 +0,0 @@ ---- -title: "Permissions" -description: "Infisical's permissions system provides granular access control." ---- - -## Overview - -The Infisical permissions system is based on a role-based access control (RBAC) model. The system allows you to define roles and assign them to users and machines. Each role has a set of permissions that define what actions a user can perform. - -Permissions are built on a subject-action-object model. The subject is the resource the permission is being applied to, the action is what the permission allows. -An example of a subject/action combination would be `secrets/read`. This permission allows the subject to read secrets. - -Refer to the table below for a list of subjects and the actions they support. - -## Subjects and Actions - - - - - - Not all actions are applicable to all subjects. As an example, the - `secrets-rollback` subject only supports `read`, and `create` as actions. - While `secrets` support `read`, `create`, `edit`, `delete`. - - -| Subject | Actions | -| ------------------------- | ----------------------------------------------------------------------------------------------------------- | -| `role` | `read`, `create`, `edit`, `delete` | -| `member` | `read`, `create`, `edit`, `delete` | -| `groups` | `read`, `create`, `edit`, `delete` | -| `settings` | `read`, `create`, `edit`, `delete` | -| `integrations` | `read`, `create`, `edit`, `delete` | -| `webhooks` | `read`, `create`, `edit`, `delete` | -| `service-tokens` | `read`, `create`, `edit`, `delete` | -| `environments` | `read`, `create`, `edit`, `delete` | -| `tags` | `read`, `create`, `edit`, `delete` | -| `audit-logs` | `read`, `create`, `edit`, `delete` | -| `ip-allowlist` | `read`, `create`, `edit`, `delete` | -| `workspace` | `edit`, `delete` | -| `secrets` | `read`, `create`, `edit`, `delete` | -| `secret-folders` | `read`, `create`, `edit`, `delete` | -| `secret-imports` | `read`, `create`, `edit`, `delete` | -| `dynamic-secrets` | `read-root-credential`, `create-root-credential`, `edit-root-credential`, `delete-root-credential`, `lease` | -| `secret-rollback` | `read`, `create` | -| `secret-approval` | `read`, `create`, `edit`, `delete` | -| `secret-rotation` | `read`, `create`, `edit`, `delete` | -| `identity` | `read`, `create`, `edit`, `delete` | -| `certificate-authorities` | `read`, `create`, `edit`, `delete` | -| `certificates` | `read`, `create`, `edit`, `delete` | -| `certificate-templates` | `read`, `create`, `edit`, `delete` | -| `pki-alerts` | `read`, `create`, `edit`, `delete` | -| `pki-collections` | `read`, `create`, `edit`, `delete` | -| `kms` | `edit` | -| `cmek` | `read`, `create`, `edit`, `delete`, `encrypt`, `decrypt` | - - - - - - - Not all actions are applicable to all subjects. As an example, the `workspace` - subject only supports `read`, and `create` as actions. While `member` support - `read`, `create`, `edit`, `delete`. - - -| Subject | Actions | -| ------------------ | ---------------------------------- | -| `workspace` | `read`, `create` | -| `role` | `read`, `create`, `edit`, `delete` | -| `member` | `read`, `create`, `edit`, `delete` | -| `secret-scanning` | `read`, `create`, `edit`, `delete` | -| `settings` | `read`, `create`, `edit`, `delete` | -| `incident-account` | `read`, `create`, `edit`, `delete` | -| `sso` | `read`, `create`, `edit`, `delete` | -| `scim` | `read`, `create`, `edit`, `delete` | -| `ldap` | `read`, `create`, `edit`, `delete` | -| `groups` | `read`, `create`, `edit`, `delete` | -| `billing` | `read`, `create`, `edit`, `delete` | -| `identity` | `read`, `create`, `edit`, `delete` | -| `kms` | `read` | - - - - -## Inversion - -Permission inversion allows you to explicitly deny actions instead of allowing them. This is supported for the following subjects: - -- secrets -- secret-folders -- secret-imports -- dynamic-secrets -- cmek - -When a permission is inverted, it changes from an "allow" rule to a "deny" rule. For example: - -```typescript -// Regular permission - allows reading secrets -{ - subject: "secrets", - action: ["read"] -} - -// Inverted permission - denies reading secrets -{ - subject: "secrets", - action: ["read"], - inverted: true -} -``` - -## Conditions - -Conditions allow you to create more granular permissions by specifying criteria that must be met for the permission to apply. This is supported for the following subjects: - -- secrets -- secret-folders -- secret-imports -- dynamic-secrets - -### Properties - -Conditions can be applied to the following properties: - -- `environment`: Control access based on environment slugs -- `secretPath`: Control access based on secret paths -- `secretName`: Control access based on secret names -- `secretTags`: Control access based on tags (only supports $in operator) - -### Operators - -The following operators are available for conditions: - -| Operator | Description | Example | -| -------- | ---------------------------------- | ----------------------------------------------------- | -| `$eq` | Equal | `{ environment: { $eq: "production" } }` | -| `$ne` | Not equal | `{ environment: { $ne: "development" } }` | -| `$in` | Matches any value in array | `{ environment: { $in: ["staging", "production"] } }` | -| `$glob` | Pattern matching using glob syntax | `{ secretPath: { $glob: "/app/\*" } }` | - -These details are especially useful if you're using the API to [create new project roles](../api-reference/endpoints/project-roles/create). -The rules outlined on this page, also apply when using our Terraform Provider to manage your Infisical project roles, or any other of our clients that manage project roles. - -## Migrating from permission V1 to permission V2 - -When upgrading to V2 permissions (i.e. when moving from using the `permissions` to `permissions_v2` field in your Terraform configurations, or upgrading to the V2 permission API), you'll need to update your permission structure as follows: - -Any permissions for `secrets` should be expanded to include equivalent permissions for: - -- `secret-imports` -- `secret-folders` (except for read permissions) -- `dynamic-secrets` - -For dynamic secrets, the actions need to be mapped differently: - -- `read` → `read-root-credential` -- `create` → `create-root-credential` -- `edit` → `edit-root-credential` (also adds `lease` permission) -- `delete` → `delete-root-credential` - -Example: - -```hcl -# Old V1 configuration -resource "infisical_project_role" "example" { - name = "example" - permissions = [ - { - subject = "secrets" - action = "read" - }, - { - subject = "secrets" - action = "edit" - } - ] -} - -# New V2 configuration -resource "infisical_project_role" "example" { - name = "example" - permissions_v2 = [ - # Original secrets permission - { - subject = "secrets" - action = ["read", "edit"] - inverted = false - }, - # Add equivalent secret-imports permission - { - subject = "secret-imports" - action = ["read", "edit"] - inverted = false - }, - # Add secret-folders permission (without read) - { - subject = "secret-folders" - action = ["edit"] - inverted = false - }, - # Add dynamic-secrets permission with mapped actions - { - subject = "dynamic-secrets" - action = ["read-root-credential", "edit-root-credential", "lease"] - inverted = false - } - ] -} -``` - -Note: When moving to V2 permissions, make sure to include all the necessary expanded permissions based on your original `secrets` permissions. diff --git a/docs/internals/permissions/migration.mdx b/docs/internals/permissions/migration.mdx new file mode 100644 index 000000000..c934f86e0 --- /dev/null +++ b/docs/internals/permissions/migration.mdx @@ -0,0 +1,118 @@ +--- +title: "Migration Guide" +description: "Guide for migrating permissions in Infisical" +--- + +# Migrating from Permission V1 to Permission V2 + +This guide provides instructions for upgrading from the legacy V1 permissions system to the more powerful V2 permissions system in Infisical. + +## Why Upgrade to V2? + +The V2 permissions system offers several advantages over V1: + +- **More granular control**: Separate permissions for different secret-related resources +- **Explicit deny rules**: Support for permission inversion +- **Conditional permissions**: Apply permissions based on specific criteria +- **Array-based actions**: Cleaner syntax for multiple actions + +## Migration Steps + +When upgrading to V2 permissions (i.e., when moving from using the `permissions` to `permissions_v2` field in your Terraform configurations, or upgrading to the V2 permission API), you'll need to update your permission structure as follows: + +### 1. Expand Secret Permissions + +Any permissions for `secrets` should be expanded to include equivalent permissions for: + +- `secret-imports` +- `secret-folders` (except for read permissions) +- `dynamic-secrets` + +### 2. Map Dynamic Secret Actions + +For dynamic secrets, the actions need to be mapped differently: + +| V1 Action | V2 Action | +| --------- | ----------------------------------------------------- | +| `read` | `read-root-credential` | +| `create` | `create-root-credential` | +| `edit` | `edit-root-credential` (also adds `lease` permission) | +| `delete` | `delete-root-credential` | + +### 3. Update Configuration Format + +V2 permissions use a different syntax, with actions stored in arrays and an optional `inverted` flag: + +```typescript +// V1 format (single action) +{ + subject: "secrets", + action: "read" +} + +// V2 format (array of actions) +{ + subject: "secrets", + action: ["read"], + inverted: false // Optional, defaults to false +} +``` + +## Example Migration + +Here's a complete example showing how to migrate a role from V1 to V2: + +```hcl +# Old V1 configuration +resource "infisical_project_role" "example" { + name = "example" + permissions = [ + { + subject = "secrets" + action = "read" + }, + { + subject = "secrets" + action = "edit" + } + ] +} + +# New V2 configuration +resource "infisical_project_role" "example" { + name = "example" + permissions_v2 = [ + # Original secrets permission + { + subject = "secrets" + action = ["read", "edit"] + inverted = false + }, + # Add equivalent secret-imports permission + { + subject = "secret-imports" + action = ["read", "edit"] + inverted = false + }, + # Add secret-folders permission (without read) + { + subject = "secret-folders" + action = ["edit"] + inverted = false + }, + # Add dynamic-secrets permission with mapped actions + { + subject = "dynamic-secrets" + action = ["read-root-credential", "edit-root-credential", "lease"] + inverted = false + } + ] +} +``` + +## Important Considerations + +- When moving to V2 permissions, make sure to include all the necessary expanded permissions based on your original `secrets` permissions. +- V2 permissions give you the ability to use conditions and inversion, which are not available in V1. +- During migration, review your existing roles and consider if more granular permissions would better fit your security requirements. +- Test your migrated permissions thoroughly in a non-production environment before deploying to production. diff --git a/docs/internals/permissions/organization-permissions.mdx b/docs/internals/permissions/organization-permissions.mdx new file mode 100644 index 000000000..c68d845e2 --- /dev/null +++ b/docs/internals/permissions/organization-permissions.mdx @@ -0,0 +1,220 @@ +--- +title: "Organization Permissions" +description: "Comprehensive guide to Infisical's organization-level permissions" +--- + +## Overview + +Infisical's organization permissions system follows a role-based access control (RBAC) model built on a subject-action-object framework. At the organization level, these permissions determine what actions users/machines can perform on various resources across the entire organization. + +Each permission consists of: + +- **Subject**: The resource the permission applies to (e.g., workspaces, members, billing) +- **Action**: The operation that can be performed (e.g., read, create, edit, delete) + +Some organization-level resources—specifically `app-connections`—support conditional permissions and permission inversion for more granular access control. + +## Available Organization Permissions + +Below is a comprehensive list of all available organization-level subjects and their supported actions, organized by functional area. + +### Workspace Management + +#### Subject: `workspace` + +| Action | Description | +| -------- | --------------------- | +| `create` | Create new workspaces | + +### Role Management + +#### Subject: `role` + +| Action | Description | +| -------- | ------------------------------------------------------ | +| `read` | View organization roles and their assigned permissions | +| `create` | Create new organization roles | +| `edit` | Modify existing organization roles | +| `delete` | Remove organization roles | + +### User Management + +#### Subject: `member` + +| Action | Description | +| -------- | ------------------------------------ | +| `read` | View organization members | +| `create` | Add new members to the organization | +| `edit` | Modify member details | +| `delete` | Remove members from the organization | + +#### Subject: `groups` + +| Action | Description | +| ------------------ | ------------------------------------------------ | +| `read` | View organization groups | +| `create` | Create new groups in the organization | +| `edit` | Modify existing groups | +| `delete` | Remove groups from the organization | +| `grant-privileges` | Change permission levels for organization groups | +| `add-members` | Add members to groups | +| `remove-members` | Remove members from groups | + +#### Subject: `identity` + +| Action | Description | +| ------------------ | --------------------------------------------------- | +| `read` | View organization identities | +| `create` | Add new identities to organization | +| `edit` | Modify organization identities | +| `delete` | Remove identities from organization | +| `grant-privileges` | Change permission levels of organization identities | +| `revoke-auth` | Revoke authentication for identities | +| `create-token` | Create new authentication tokens | +| `delete-token` | Delete authentication tokens | +| `get-token` | Retrieve authentication tokens | + +### Security & Compliance + +#### Subject: `secret-scanning` + +| Action | Description | +| -------- | ----------------------------------------- | +| `read` | View secret scanning results and settings | +| `create` | Configure secret scanning | +| `edit` | Modify secret scanning settings | +| `delete` | Remove secret scanning configuration | + +#### Subject: `settings` + +| Action | Description | +| -------- | ----------------------------------------- | +| `read` | View organization settings | +| `create` | Setup and configure organization settings | +| `edit` | Modify organization settings | +| `delete` | Remove organization settings | + +#### Subject: `incident-contact` + +| Action | Description | +| -------- | -------------------------------- | +| `read` | View incident contacts | +| `create` | Set up new incident contacts | +| `edit` | Modify incident contact settings | +| `delete` | Remove incident contacts | + +#### Subject: `audit-logs` + +| Action | Description | +| ------ | ---------------------------- | +| `read` | View organization audit logs | + +### Identity Provider Integration + +#### Subject: `sso` + +| Action | Description | +| -------- | ---------------------------------- | +| `read` | View Single Sign-On configurations | +| `create` | Set up new SSO integrations | +| `edit` | Modify existing SSO settings | +| `delete` | Remove SSO configurations | + +#### Subject: `scim` + +| Action | Description | +| -------- | ----------------------------- | +| `read` | View SCIM configurations | +| `create` | Set up new SCIM provisioning | +| `edit` | Modify existing SCIM settings | +| `delete` | Remove SCIM configurations | + +#### Subject: `ldap` + +| Action | Description | +| -------- | ----------------------------- | +| `read` | View LDAP configurations | +| `create` | Set up new LDAP integrations | +| `edit` | Modify existing LDAP settings | +| `delete` | Remove LDAP configurations | + +### Billing & Subscriptions + +#### Subject: `billing` + +| Action | Description | +| -------- | ------------------------------------------------ | +| `read` | View billing information and subscription status | +| `create` | Set up new payment methods or subscriptions | +| `edit` | Modify billing details or subscription plans | +| `delete` | Remove payment methods or cancel subscriptions | + +### Templates & Automation + +#### Subject: `project-templates` + +| Action | Description | +| -------- | --------------------------------- | +| `read` | View project templates | +| `create` | Create new project templates | +| `edit` | Modify existing project templates | +| `delete` | Remove project templates | + +### Integrations + +#### Subject: `app-connections` + +Supports conditions and permission inversion + +| Action | Description | +| --------- | ---------------------------------- | +| `read` | View app connection configurations | +| `create` | Create new app connections | +| `edit` | Modify existing app connections | +| `delete` | Remove app connections | +| `connect` | Use app connections | + +### Key Management + +#### Subject: `kms` + +| Action | Description | +| -------- | ------------------------------------ | +| `read` | View organization KMS configurations | +| `create` | Set up new KMS configurations | +| `edit` | Modify KMS settings | +| `delete` | Remove KMS configurations | + +#### Subject: `kmip` + +| Action | Description | +| ------- | ---------------------------------- | +| `setup` | Configure KMIP server settings | +| `proxy` | Act as a proxy for KMIP operations | + +### Admin Tools + +#### Subject: `organization-admin-console` + +| Action | Description | +| --------------------- | ------------------------------------------- | +| `access-all-projects` | Access all projects within the organization | + +### Secure Share + +#### Subject: `secret-share` + +| Action | Description | +| ----------------- | ---------------------------- | +| `manage-settings` | Manage secret share settings | + +### Gateway Management + +#### Subject: `gateway` + +| Action | Description | +| ----------------- | --------------------------------- | +| `list-gateways` | View all organization gateways | +| `create-gateways` | Add new gateways to organization | +| `edit-gateways` | Modify existing gateway settings | +| `delete-gateways` | Remove gateways from organization | diff --git a/docs/internals/permissions/overview.mdx b/docs/internals/permissions/overview.mdx new file mode 100644 index 000000000..2fb4dc28d --- /dev/null +++ b/docs/internals/permissions/overview.mdx @@ -0,0 +1,89 @@ +--- +title: "Overview" +description: "Infisical's permissions system provides granular access control." +--- + +## Overview + +The Infisical permissions system is based on a role-based access control (RBAC) model. The system allows you to define roles and assign them to users and machines. Each role has a set of permissions that define what actions a user can perform. + +Permissions are built on a subject-action-object model. The subject is the resource the permission is being applied to, the action is what the permission allows. +An example of a subject/action combination would be `secrets/read`. This permission allows the subject to read secrets. + +## Permission Scope Levels + +Infisical's permission system operates at two distinct levels, providing comprehensive and flexible access control across your entire security infrastructure: + +### Project Permissions + +Project permissions control access to resources within a specific project, including secrets management, PKI, KMS, and SSH certificate functionality. + +For a comprehensive list of all project-level subjects, actions, and detailed descriptions, please refer to the [Project Permissions](/internals/permissions/project-permissions) documentation. + +### Organization Permissions + +Organization permissions control access to organization-wide resources and settings such as workspaces, billing, identity providers, and more. + +For a comprehensive list of all organization-level subjects, actions, and detailed descriptions, please refer to the [Organization Permissions](/internals/permissions/organization-permissions) documentation. + +## Inversion + +Permission inversion allows you to explicitly deny actions instead of allowing them. This is supported for the following subjects: + +- secrets +- secret-folders +- secret-imports +- dynamic-secrets + +When a permission is inverted, it changes from an "allow" rule to a "deny" rule. For example: + +```typescript +// Regular permission - allows reading secrets +{ + subject: "secrets", + action: ["read"] +} + +// Inverted permission - denies reading secrets +{ + subject: "secrets", + action: ["read"], + inverted: true +} +``` + +**Important:** The order of permissions matters when using inversion. For inverted (deny) permissions to be effective, there +typically needs to be a corresponding allow permission somewhere in the chain. Permissions are evaluated in sequence, +so the relative positioning of allow and deny rules determines the final access outcome. + +## Conditions + +Conditions allow you to create more granular permissions by specifying criteria that must be met for the permission to apply. This is supported for the following subjects: + +- secrets +- secret-folders +- secret-imports +- dynamic-secrets + +### Properties + +Conditions can be applied to the following properties: + +- `environment`: Control access based on environment slugs +- `secretPath`: Control access based on secret paths +- `secretName`: Control access based on secret names +- `secretTags`: Control access based on tags (only supports $in operator) + +### Operators + +The following operators are available for conditions: + +| Operator | Description | Example | +| -------- | ---------------------------------- | ----------------------------------------------------- | +| `$eq` | Equal | `{ environment: { $eq: "production" } }` | +| `$ne` | Not equal | `{ environment: { $ne: "development" } }` | +| `$in` | Matches any value in array | `{ environment: { $in: ["staging", "production"] } }` | +| `$glob` | Pattern matching using glob syntax | `{ secretPath: { $glob: "/app/\*" } }` | + +These details are especially useful if you're using the API to [create new project roles](../api-reference/endpoints/project-roles/create). +The rules outlined on this page, also apply when using our Terraform Provider to manage your Infisical project roles, or any other of our clients that manage project roles. diff --git a/docs/internals/permissions/project-permissions.mdx b/docs/internals/permissions/project-permissions.mdx new file mode 100644 index 000000000..4e0c592cb --- /dev/null +++ b/docs/internals/permissions/project-permissions.mdx @@ -0,0 +1,315 @@ +--- +title: "Project Permissions" +description: "Comprehensive guide to Infisical's project-level permissions" +--- + +## Overview + +Infisical's project permissions system follows a role-based access control (RBAC) model built on a subject-action-object framework. At the project level, these permissions determine what actions users/machines can perform on various resources within a specific project. + +Each permission consists of: + +- **Subject**: The resource the permission applies to (e.g., secrets, members, settings) +- **Action**: The operation that can be performed (e.g., read, create, edit, delete) + +Some project-level resources—specifically `secrets`, `secret-folders`, `secret-imports`, and `dynamic-secrets`—support conditional permissions and permission inversion for more granular access control. Conditions allow you to specify criteria (like environment, secret path, or tags) that must be met for the permission to apply. + +## Available Project Permissions + +Below is a comprehensive list of all available project-level subjects and their supported actions. + +### Core Platform & Access Control + +#### Subject: `role` + +| Action | Description | +| -------- | ------------------------------------------------- | +| `read` | View project roles and their assigned permissions | +| `create` | Create new project roles | +| `edit` | Modify existing project roles | +| `delete` | Remove project roles | + +#### Subject: `member` + +| Action | Description | +| ------------------ | ------------------------------------------- | +| `read` | View project members | +| `create` | Add new members to the project | +| `edit` | Modify member details | +| `delete` | Remove members from the project | +| `grant-privileges` | Change permission levels of project members | + +#### Subject: `groups` + +| Action | Description | +| ------------------ | ------------------------------------------ | +| `read` | View project groups | +| `create` | Create new groups within the project | +| `edit` | Modify existing groups | +| `delete` | Remove groups from the project | +| `grant-privileges` | Change permission levels of project groups | + +#### Subject: `identity` + +| Action | Description | +| ------------------ | ---------------------------------------------- | +| `read` | View project identities | +| `create` | Add new identities to project | +| `edit` | Modify project identities | +| `delete` | Remove identities from project | +| `grant-privileges` | Change permission levels of project identities | + +#### Subject: `settings` + +| Action | Description | +| -------- | -------------------------------------- | +| `read` | View project settings | +| `create` | Add new project configuration settings | +| `edit` | Modify project settings | +| `delete` | Remove project settings | + +#### Subject: `environments` + +| Action | Description | +| -------- | ------------------------------------ | +| `read` | View project environments | +| `create` | Add new environments to the project | +| `edit` | Modify existing environments | +| `delete` | Remove environments from the project | + +#### Subject: `tags` + +| Action | Description | +| -------- | ---------------------------------------- | +| `read` | View project tags | +| `create` | Create new tags for organizing resources | +| `edit` | Modify existing tags | +| `delete` | Remove tags from the project | + +#### Subject: `workspace` + +| Action | Description | +| -------- | ------------------------- | +| `edit` | Modify workspace settings | +| `delete` | Delete the workspace | + +#### Subject: `ip-allowlist` + +| Action | Description | +| -------- | -------------------------------------------- | +| `read` | View IP allowlists | +| `create` | Add new IP addresses or ranges to allowlists | +| `edit` | Modify existing IP allowlist entries | +| `delete` | Remove IP addresses from allowlists | + +#### Subject: `audit-logs` + +| Action | Description | +| ------ | ------------------------------------------------------- | +| `read` | View audit logs of actions performed within the project | + +#### Subject: `integrations` + +| Action | Description | +| -------- | -------------------------------- | +| `read` | View configured integrations | +| `create` | Add new third-party integrations | +| `edit` | Modify integration settings | +| `delete` | Remove integrations | + +#### Subject: `webhooks` + +| Action | Description | +| -------- | ------------------------------------ | +| `read` | View webhook configurations | +| `create` | Add new webhooks | +| `edit` | Modify webhook endpoints or triggers | +| `delete` | Remove webhooks | + +#### Subject: `service-tokens` + +| Action | Description | +| -------- | ---------------------------------------- | +| `read` | View service tokens | +| `create` | Create new service tokens for API access | +| `edit` | Modify token properties | +| `delete` | Revoke or remove service tokens | + +### Secrets Management + +#### Subject: `secrets` + +Supports conditions and permission inversion +| Action | Description | Notes | +| -------- | ------------------------------- | ----- | +| `read` | View secrets and their values | This action is the equivalent of granting both `describeSecret` and `readValue`. The `read` action is considered **legacy**. You should use the `describeSecret` and/or `readValue` actions instead. | +| `describeSecret` | View secret details such as key, path, metadata, tags, and more | If you are using the API, you can pass `viewSecretValue: false` to the API call to retrieve secrets without their values. | +| `readValue` | View the value of a secret.| In order to read secret values, the `describeSecret` action must also be granted. | +| `create` | Add new secrets to the project | | +| `edit` | Modify existing secret values | | +| `delete` | Remove secrets from the project | | + +#### Subject: `secret-folders` + +Supports conditions and permission inversion +| Action | Description | +| -------- | ------------------------ | +| `read` | View secret folders | +| `create` | Create new folders | +| `edit` | Modify folder properties | +| `delete` | Remove secret folders | + +#### Subject: `secret-imports` + +Supports conditions and permission inversion +| Action | Description | +| -------- | --------------------- | +| `read` | View secret imports | +| `create` | Create secret imports | +| `edit` | Modify secret imports | +| `delete` | Remove secret imports | + +#### Subject: `secret-rollback` + +| Action | Description | +| -------- | ---------------------------------- | +| `read` | View secret versions and snapshots | +| `create` | Roll back secrets to snapshots | + +#### Subject: `secret-approval` + +| Action | Description | +| -------- | ----------------------------------- | +| `read` | View approval policies and requests | +| `create` | Create new approval policies | +| `edit` | Modify approval policies | +| `delete` | Remove approval policies | + +#### Subject: `secret-rotation` + +Supports conditions and permission inversion +| Action | Description | +| ------------------------------ | ---------------------------------------------- | +| `read` | View secret rotation configurations | +| `read-generated-credentials` | View the generated credentials of a rotation | +| `create` | Set up secret rotation configurations | +| `edit` | Modify secret rotation configurations | +| `rotate-secrets` | Rotate the generated credentials of a rotation | +| `delete` | Remove secret rotation configurations | + +#### Subject: `secret-syncs` + +| Action | Description | +| ---------------- | -------------------------------------------------- | +| `read` | View secret synchronization configurations | +| `create` | Create new sync configurations | +| `edit` | Modify existing sync settings | +| `delete` | Remove sync configurations | +| `sync-secrets` | Execute synchronization of secrets between systems | +| `import-secrets` | Import secrets from sync sources | +| `remove-secrets` | Remove secrets from sync destinations | + +#### Subject: `dynamic-secrets` + +Supports conditions and permission inversion +| Action | Description | +| ------------------------ | ---------------------------------- | +| `read-root-credential` | View dynamic secret configurations | +| `create-root-credential` | Create dynamic secrets | +| `edit-root-credential` | Edit dynamic secrets | +| `delete-root-credential` | Remove dynamic secrets | +| `lease` | Create dynamic secret leases | + +### Key Management Service (KMS) + +#### Subject: `kms` + +| Action | Description | +| ------ | --------------------------- | +| `edit` | Modify project KMS settings | + +#### Subject: `cmek` + +| Action | Description | +| --------- | ------------------------------------- | +| `read` | View Customer-Managed Encryption Keys | +| `create` | Add new encryption keys | +| `edit` | Modify key properties | +| `delete` | Remove encryption keys | +| `encrypt` | Use keys for encryption operations | +| `decrypt` | Use keys for decryption operations | + +### Public Key Infrastructure (PKI) + +#### Subject: `certificate-authorities` + +| Action | Description | +| -------- | ---------------------------------- | +| `read` | View certificate authorities | +| `create` | Create new certificate authorities | +| `edit` | Modify CA configurations | +| `delete` | Remove certificate authorities | + +#### Subject: `certificates` + +| Action | Description | +| -------- | ----------------------------- | +| `read` | View certificates | +| `create` | Issue new certificates | +| `delete` | Revoke or remove certificates | + +#### Subject: `certificate-templates` + +| Action | Description | +| -------- | -------------------------------- | +| `read` | View certificate templates | +| `create` | Create new certificate templates | +| `edit` | Modify template configurations | +| `delete` | Remove certificate templates | + +#### Subject: `pki-alerts` + +| Action | Description | +| -------- | ------------------------------------------------------------ | +| `read` | View PKI alert configurations | +| `create` | Create new alerts for certificate expiry or other PKI events | +| `edit` | Modify alert settings | +| `delete` | Remove PKI alerts | + +#### Subject: `pki-collections` + +| Action | Description | +| -------- | --------------------------------------------------- | +| `read` | View PKI resource collections | +| `create` | Create new collections for organizing PKI resources | +| `edit` | Modify collection properties | +| `delete` | Remove PKI collections | + +### SSH Certificate Management + +#### Subject: `ssh-certificate-authorities` + +| Action | Description | +| -------- | -------------------------------------- | +| `read` | View SSH certificate authorities | +| `create` | Create new SSH certificate authorities | +| `edit` | Modify SSH CA configurations | +| `delete` | Remove SSH certificate authorities | + +#### Subject: `ssh-certificates` + +| Action | Description | +| -------- | --------------------------------- | +| `read` | View SSH certificates | +| `create` | Issue new SSH certificates | +| `edit` | Modify SSH certificate properties | +| `delete` | Revoke or remove SSH certificates | + +#### Subject: `ssh-certificate-templates` + +| Action | Description | +| -------- | ------------------------------------ | +| `read` | View SSH certificate templates | +| `create` | Create new SSH certificate templates | +| `edit` | Modify SSH template configurations | +| `delete` | Remove SSH certificate templates | diff --git a/docs/internals/security.mdx b/docs/internals/security.mdx index 02b6fd9fb..17daf88cb 100644 --- a/docs/internals/security.mdx +++ b/docs/internals/security.mdx @@ -3,25 +3,25 @@ title: "Security" description: "Infisical's security model includes many considerations and initiatives." --- -Given that Infisical is a secret management platform that manages sensitive data, the Infisical security model is very important. -The goal of Infisical's security model is to ensure the security and integrity of all of its managed data as well as all associated operations. +As a security infrastructure platform dealing with highly-sensitive data, Infisical follows a robust security model with the goal of ensuring the security and integrity of all its managed data and associated components. -This means that data at rest and in transit must be secure from eavesdropping or tampering. All clients must be authenticated and authorized to access data. Additionally, all interactions must be auditable and traced uniquely back to their source. +As part of the security model, data at rest and in transit must be secure from eavesdropping or tampering, clients must be authenticated and authorized to access data, and all operations in the platform are audited and can be traced back to their source. + +This page documents security measures used by [Infisical](https://github.com/Infisical/infisical), the software, and [Infisical Cloud](https://infisical.com/), a separate managed service offering for the software. ## Threat model -Infisical’s threat model spans communication, storage, response mechanisms, failover strategies, and more. +Infisical’s (the software) threat model spans communication, storage, response mechanisms, and more. -- Eavesdropping on communications: Infisical ensures end-to-end encryption for all client interactions with the Infisical API. +- Eavesdropping on communications: Infisical secures client communication with the server and from the server to the storage backend. - Tampering with data (at rest or in transit): Infisical implements data integrity checks to detect tampering. If inconsistencies are found, Infisical aborts transactions and raises alerts. -- Unauthorized access (lacking authentication/authorization): Infisical mandates rigorous authentication and authorization checks for all inbound requests; it also offers multi-factor authentication and role-based access controls. -- Actions without accountability: Infisical logs all project-level events, including policy updates, queries/mutations applied to secrets, and more. Every event is timestamped and information about actor, source (i.e. IP address, user-agent, etc.), and relevant metadata is included. -- Breach of data storage confidentiality: Infisical encrypts all stored secrets using proven cryptographic techniques such as AES-256-GCM for symmetric encryption. -- Loss of service availability or secret data due to failures: Infisical leverages the robust container orchestration capabilities of Kubernetes and the inherent high availability features of Bitnami MongoDB to ensure resilience and fault tolerance. By deploying multiple replicas of Infisical application on Kubernetes, operations can continue even if a single instance fails. +- Unauthorized access (lacking authentication/authorization): Infisical mandates rigorous authentication and authorization checks for all inbound requests; it also offers multi-factor authentication and role/attribute-based access controls. +- Actions without accountability: Infisical logs events, including policy updates, queries/mutations applied to secrets, certificates, and more. Every event is timestamped and information about actor, source (i.e. IP address, user-agent, etc.), and relevant metadata is included. +- Breach of data storage confidentiality: Infisical encrypts all stored secrets using proven cryptographic techniques for symmetric encryption. - Unrecognized suspicious activities: Infisical monitors for any anomalous activities such as authentication attempts from previously unseen sources. -- Unidentified system vulnerabilities: Infisical undergoes penetration tests and vulnerability assessments twice a year; we act on findings to bolster the system's defense mechanisms. +- Unidentified system vulnerabilities: Infisical undergoes penetration tests and vulnerability assessments twice a year; we act on findings to bolster the system’s defense mechanisms. -That said, Infisical does not consider the following as part of its threat model: +Infisical (the software) does not consider the following as part of its threat model: - Uncontrolled access to the storage mechanism: An attacker with unfettered access to the storage system can manipulate data in unpredictable ways, including erasing or tampering with stored secrets. Furthermore, the attacker could potentially implement state rollbacks to favor their objectives. - Disclosure of secret presence: If an adversary gains read access to the storage backend, they might discern the existence of certain secrets, even if the actual contents remain encrypted and concealed. @@ -30,143 +30,80 @@ That said, Infisical does not consider the following as part of its threat model - Breaches via compromised clients: If a system or application accessing Infisical is compromised, and its credentials to the platform are exposed, an attacker might gain access at the privilege level of that compromised entity. - Configuration tampering by administrators: Any configuration data, whether supplied through admin interfaces or configuration files, needs scrutiny. If an attacker can manipulate these configurations, it poses risks to data confidentiality and integrity. - Physical access to deployment infrastructure: An attacker with physical access to the servers or infrastructure where Infisical is deployed can potentially compromise the system in ways that are challenging to guard against, such as direct hardware tampering or booting from malicious media. -- Social engineering attacks on personnel: Attacks that target personnel, tricking them into divulging sensitive information or performing compromising actions, fall outside the platform's direct defensive purview. +- Social engineering attacks on personnel: Attacks that target personnel, tricking them into divulging sensitive information or performing compromising actions, fall outside the platform’s direct defensive purview. -It's essential to note that while these points fall outside the platform's direct threat model, they still form crucial considerations for an overarching security strategy. +Note that while these points fall outside the Infisical’s threat model, they remain considerations in the broader platform architecture. ## External threat overview -Infisical's architecture consists of various systems: +Infisical’s architecture consists of various systems which together we refer to as the Infisical platform: -- Infisical API -- Storage backend -- Redis -- Infisical Web UI -- Infisical clients +- Server: The Infisical API that serves requests. +- Clients: The Web UI and other applications that send requests to the server. +- Storage backend: PostgreSQL used by the server to persist data. +- Redis: Used by Infisical for caching, queueing and cron job scheduling. -The Infisical API requires that the Infisical Web UI and all Infisical clients are authenticated and authorized for every inbound request. If using [Infisical Cloud](https://app.infisical.com), all traffic is routed through [Cloudflare](https://www.cloudflare.com) which enforces TLS and requires a minimum of TLS 1.2. +The server requires clients to be authenticated and authorized for every inbound request. If using [Infisical Cloud](https://infisical.com/), all traffic is routed through [Cloudflare](https://www.cloudflare.com/) which enforces TLS and requires a minimum of TLS 1.2. -The Infisical API is untrusted by design when dealing with secrets. All secrets are encrypted/decrypted on the client-side before reaching the Infisical API by default; granting Infisical access to secrets afterward is optional and up to your organization. +The server mandates that each request includes a valid token (issued for a user or machine identity) used to identify the client before performing any actions on the platform. Clients without a valid token can only access login endpoints with the exception of a few intentionally unauthenticated endpoints. For tokens issued for machine identities, Infisical provides significant configuration, including support for native authentication methods (e.g. [AWS](https://infisical.com/docs/documentation/platform/identities/aws-auth), [Azure](https://infisical.com/docs/documentation/platform/identities/azure-auth), [Kubernetes](https://infisical.com/docs/documentation/platform/identities/kubernetes-auth), etc.); custom TTLs to restrict token lifespan; IP restrictions to enforce network-based access controls; and usage caps to limit the maximum number of times that a token can be used. -The storage backend used by Infisical is also untrusted by design. All sensitive data is encrypted either symmetrically with AES-256-GCM or asymmetrically with x25519-xsalsa20-poly1305 prior to entering the storage backend, depending on the context either on the client-side or server-side. Moreover, Infisical communicates with the storage backend over TLS to provide an added layer of security. +When accessing Infisical via web browser, JWT tokens are stored in browser memory and appended to outbound requests requiring authentication; refresh tokens are stored in HttpOnly cookies and included in requests as part of token renewal. Note also that Infisical utilizes the latest HTTP security headers and employs a strict Content-Security-Policy to mitigate XSS. + +To mitigate abuse and enhance system stability, the server enforces configurable rate limiting on read, write, and secrets operations. This prevents excessive API requests from impacting system performance while ensuring fair usage across clients. + +Once traffic enters the server, any sensitive data (e.g. secrets, certificates entering the server), where applicable, is encrypted using a 256-bit [Advanced Encryption Standard (AES)](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) cipher in the [Galois Counter Mode (GCM)](https://en.wikipedia.org/wiki/Galois/Counter_Mode) with 96-bit nonces prior to being persisted in the storage backend. Encryption is an integral part of Infisical’s platform-wide cryptographic architecture, which also supports seal-wrapping with external KMS and HSMs. Before responding to a client request, the server securely retrieves and decrypts requested data from the storage backend. Each decryption operation includes integrity verification to ensure data has not been altered or tampered with. ## Internal threat overview -Within Infisical, a critical security concern is an attacker gaining access to sensitive data that they are not permitted to, especially if they already has some degree of access to the system. There are currently two authentication methods categories used by clients for where we apply robust authentication and authorization logic. +Within Infisical, an internal threat and critical security concern is an attacker gaining access to sensitive data that they are not permitted to, especially if they are able to authenticate with some degree of access to the system. -### JWT / API Key +Before a client can perform any actions on the platform, it must authenticate with the server using a supported authentication method such as username-password, SAML, SSO, LDAP, AWS/GCP/Azure, OIDC, or Kubernetes authentication. A successful authentication results in the issuance of a client (JWT) token containing a reference to the user or machine identity bound to it. -This token category is used by users and included in requests made from the Infisical Web UI or elsewhere to the Infisical API. +When a client uses the token to make authenticated requests against the server, Infisical validates the token and maps the bound-identity to access control policies that exist at the organization and project level, both types of namespaces within the platform. The access control policies are configured by operators of Infisical ahead of time and may involve role-based, attribute-based, and one-off “additional privilege” resource constraints. Given the robustness of the access control system, we recommend reading the full documentation for it. -Each token is authenticated against the API and mapped to an existing user in Infisical. If no existing user is found for the token, the request is rejected by the API. Each token assumes the permission set of the user that it is mapped to. For example, if a user corresponding to a token is not allowed access to a certain organization or project, then the token is also not be valid for any requests concerning those specific resources. +For example, an operator of Infisical may define the following constraints to restrict client access to particular resources: -In the event of compromise, an attacker could use the token to impersonate the associated user and perform actions within the permission set of that user. While they could retrieve secrets for a project that the user is part of, they could not, however, decrypt secrets if the project follows Infisical's default zero-knowledge architecture. In any case, it would be critical for the user to invalidate this token and change their password immediately to prevent further unintended actions and consequences. - -### Service token - -This token category is provisioned by users for applications and infrastructure to perform secret operations against the Infisical API. - -Each token is scoped to a project in Infisical and configurable with an expiration date and permission set (also known as **scopes**) for specific environment(s) and path(s) within them. For example, you may provision an application a service token to authenticate against the Infisical API and retrieve secrets from some `/environment-variables` path in the production environment of a project. If the token is tried for another project, environment, or path outside of its permission set, then it is rejected by the API. - -It should also be noted that projects in Infisical can be configured to restrict service token access to specific IP addresses or CIDR ranges; this can be useful for limiting access to traffic coming from corporate networks. - -In the event of compromise, an attacker could use a service token to access the secrets that it is provisioned for. It would be critical here for project administrator(s) to revoke the token immediately to prevent further unintended access to resources; it would also be advisable currently to transfer secrets to a new project where a new project key is created on the client-side. +- Read and write access to a secret resource via an additional privilege attached to the bound-identity. +- Read-only access to a secret resource via one or multiple roles attached to the bound-identity. +- Read-only access to a secret resource via a group membership for which the associated bound-identity is part of; the group itself is assigned one or multiple roles with access to the secret resource. ## Cryptography -Infisical uses AES-256-GCM for symmetric encryption and x25519-xsalsa20-poly1305 for asymmetric encryption operations; asymmetric algorithms are implemented with the [TweetNaCl.js](https://tweetnacl.js.org/#/) library which has been well-audited and recommended for use by cybersecurity firm Cure53. Lastly, the secure remote password (SRP) implementation uses [jsrp](https://github.com/alax/jsrp) package for user authentication. +All symmetric encryption operations, with the exception of those proxied through external KMS and HSM systems, in Infisical use a software-backed, 256-bit Advanced Encryption Standard (AES) cipher in the Galois Counter Mode (GCM) with 96-bit nonces — AES-256-GCM. -By default, Infisical employs a zero-knowledge-first approach to securely storing and sharing secrets. +Infisical employs a multilayer approach to its encryption architecture with components that can be optionally linked to external KMS or HSM systems. At a high-level, a master key, backed by an operator-provided key, is used to encrypt (internal) “KMS” keys that are used to then encrypt data keys; the data keys are used to protect sensitive data stored in Infisical. The keys in the architecture are stored encrypted in the storage backend, retrieved, decrypted, and only then used as part of server operations when needed. Since server configuration is needed to decrypt any keys as part of the encryption architecture, accessing any sensitive data in Infisical requires access to both server configuration and data in the storage backend. Note that the platform’s encryption architecture has components that can be linked to external KMS and HSM systems; opting for these make the use of the software more FIPS aligned. -- Each secret belongs to a project and is symmetrically encrypted by that project's unique key. Each member of a project is shared a copy of the project key, encrypted under their public key, when they are first invited to join the project. - Since these encryption operations occur on the client-side, the Infisical API is not able to view the value of any secret and the default zero-knowledge property of Infisical is retained; as you'd expect, it follows that decryption operations also occur on the client-side. -- An exception to the zero-knowledge property occurs when a member of a project explicitly shares that project's unique key with Infisical. It is often necessary to share the project key with Infisical in order to use features like native integrations and secret rotation that wouldn't be possible to offer otherwise. +To be specific: -## Infrastructure +- The architecture starts with a 256-bit master key that can be secured by a root key which can either be a 128-bit key, passed into the server by an operator of Infisical as an environment variable, or an external key from an HSM module such as [Thales Luna HSM](https://cpl.thalesgroup.com/encryption/data-protection-on-demand/services/luna-cloud-hsm) or [AWS Cloud HSM](https://aws.amazon.com/cloudhsm/) linked via specified configuration parameters. +- The master key secures 256-bit keys in Infisical henceforth referred to as KMS keys. +- Each organization in Infisical has its own KMS key and a separate data key; the KMS key is used to secure the data key which encrypts organization-level data. +- Each project in Infisical has a designated KMS key and a separate data key; the KMS key is used to secure the data key which encrypts project-level data. Note that a project KMS key can be substituted for an external key from a KMS such as [AWS KMS](https://infisical.com/docs/documentation/platform/kms-configuration/aws-kms), [AWS Cloud HSM](https://infisical.com/docs/documentation/platform/kms-configuration/aws-hsm), and [GCP KMS](https://infisical.com/docs/documentation/platform/kms-configuration/gcp-kms). We recommend reading the fuller [documentation](https://infisical.com/docs/documentation/platform/kms-configuration/overview) or integrating with an external KMS -### High availability +## Infrastructure & High availability (Infisical Cloud) + +Infisical Cloud uses a number of strategies to keep services running smoothly and ensure data stays available, even during failures; we document these strategies below: + +- Multi-AZ AWS RDS: Infisical Cloud runs AWS Relational Database Service (RDS) with Multi-AZ deployments to improve availability and durability. This setup keeps a standby replica in a different Availability Zone (AZ) and automatically fails over if the primary instance goes down. Continuous backups and replication help protect data and minimize interruptions. +- Multi-AZ ElastiCache (Redis): For caching, Infisical Cloud runs Amazon ElastiCache (Redis) in a Multi-AZ setup. This means data is replicated across different AZs, so if one goes down, the system can automatically fail over to a healthy node. This helps keep response times low and ensures caching stays reliable. +- Multi-AZ ECS for Container Orchestration: Infisical Cloud runs on Amazon Elastic Container Service (ECS) across multiple availability zones, making sure containers stay available even if an AZ fails. If one zone has an issue, traffic automatically shifts to healthy instances in other zones, keeping downtime to a minimum. Infisical Cloud utilizes several strategies to ensure high availability, leveraging AWS services to maintain continuous operation and data integrity. -#### Multi-AZ AWS RDS +## Cross-Region Replication for Disaster Recovery (Infisical Cloud) -Infisical Cloud uses AWS Relational Database Service (RDS) with Multi-AZ deployments. -This configuration ensures that the database service is highly available and durable. -AWS RDS automatically provisions and maintains a synchronous standby replica of the database in a different Availability Zone (AZ). -This setup facilitates immediate failover to the standby in the event of an AZ failure, thereby ensuring that database operations can continue with minimal interruption. -The continuous backup and replication to the standby instance safeguard data against loss and ensure its availability even during system failures. +To handle regional failures, Infisical Cloud keeps standby regions updated and ready to take over when needed. -#### Multi-AZ ECS for Container Orchestration +- ElastiCache (Redis): Data is replicated across regions using AWS Global Datastore, keeping cached data consistent and available even if a primary region goes down. +- RDS (PostgreSQL): Cross-region read replicas ensure database data is available in multiple locations, allowing for failover in case of a regional outage. -Infisical Cloud leverages Amazon Elastic Container Service (ECS) in a Multi-AZ configuration for container orchestration. -This arrangement enables the management and operation of containers across multiple availability zones, increasing the application's fault tolerance. -Should there be an AZ failure, load is seamlessly sent to an operational AZ, thus minimizing downtime and preserving service availability. - -#### Standby Regions for Regional Failover - -To fight regional outages, secondary regions are always in standby mode and maintained with up-to-date configurations and data, ready to take over in case the primary region fails. -The standby regions enable a rapid transition and service continuity with minimal disruption in the event of a complete regional failure, ensuring that Infisical Cloud services remain accessible. - -### Snapshots - -A snapshot is a complete copy of data in the storage backend at a point in time. - -If using [Infisical Cloud](https://app.infisical.com), snapshots of MongoDB databases are taken regularly; this can be enabled on your own storage backend as well. - -### Offline usage - -Many teams and organizations use the [Infisical CLI](https://infisical.com/docs/cli/overview) to fetch and inject secrets back from Infisical into their applications and infrastructure locally; the CLI has offline fallback capabilities. - -If you have previously retrieved secrets for a specific project and environment, the `run/secret` command will utilize the saved secrets, even when offline, on subsequent fetch attempts to ensure that you always have access to secrets. - -## Platform - -### Web application - -Infisical utilizes the latest HTTP security headers and employs a strict Content-Security-Policy to mitigate XSS. - -JWT tokens are stored in browser memory and appended to outbound requests requiring authentication; refresh tokens are stored in `HttpOnly` cookies and included in future requests to `/api/token` for JWT token renewal. - -### User authentication - -Infisical supports several authentication methods including email/password, Google SSO, GitHub SSO, SAML 2.0 (Okta, Azure, JumpCloud), and OpenID Connect; Infisical also currently offers email-based 2FA with authenticator app methods coming in Q1 2024. - -Infisical uses the [secure remote password protocol](https://en.wikipedia.org/wiki/Secure_Remote_Password_protocol#:~:text=The%20SRP%20protocol%20has%20a,the%20user%20to%20the%20server), commonly found in other zero-knowledge platform architectures, for authentication. -Put simply, the protocol enables Infisical to validate a user's knowledge of their password without ever seeing it by constructing a mutual secret; we use this protocol because each user's password is used to seed the generation of a master encryption/decryption key via KDF for that user which the platform -should not see. - -Lastly, Infisical enforces strong password requirements according to the guidance set forth in [NIST Special Publication 800–63B](https://pages.nist.gov/800-63-3/sp800-63b.html#appA). Since passwords in Infisical also has cryptographic implications, Infisical validates each password on client-side to meet minimum length and entropy requirements; Infisical also considers each password against the [Have I Been Pwned (HIBP) API](https://haveibeenpwned.com), which checks the password against around 700M breached passwords, in a privacy-preserving way. - - - Since Infisical's unique zero-knowledge architecture requires a master decryption key for every user account, users with Google SSO, GitHub SSO, or SAML 2.0 enabled must still enter a secret after the - authentication step to access their secrets in Infisical. In practice, this implies stronger security since users must successfully authenticate with a single sign-on provider and provide a master decryption key - to access the platform. - - We strongly encourage users to generate and store their passwords / master decryption key in a password manager, such as 1Password, Bitwarden, or Dashlane. - - - -## Role-based access control (RBAC) - -[Infisical's RBAC](https://infisical.com/docs/documentation/platform/role-based-access-controls) feature enables organization owners and administrators to manage fine-grained access policies for members of their organization in Infisical; with RBAC, administrators can define custom roles with permission sets to be conveniently assigned to other members. - -For example, you can define a role provisioning access to secrets in a specific project and environment in it with read-only permissions; the role can be assigned to members of an organization in Infisical. - -### Audit logging - -Infisical's audit logging feature spans 25+ events, tracking everything from permission changes to queries and mutations applied to secrets, for security and compliance teams at enterprises to monitor information access in the event of any suspicious activity or incident review. Every event is timestamped and information about actor, source (i.e. IP address, user-agent, etc.), and relevant metadata is included. - -### IP allowlisting - -Infisical's IP allowlisting feature can be configured to restrict client access to specific IP addresses or CIDR ranges. This applies to any client using service tokens and can be useful, for example, for limiting access to traffic coming from corporate networks. - -By default, each project is initialized with the `0.0.0.0/0` entry, representing all possible IPv4 addresses. For enhanced security, we strongly recommend replacing the default entry with your client IPs to tighten access to your secrets. +With standby regions and automated failovers in place, Infisical Cloud faces minimal service disruptions even during large-scale outages. ## Penetration testing Infisical hires external third parties to perform regular security assessment and penetration testing of the platform. -Most recently, Infisical commissioned cybersecurity firm [Oneleet](https://www.oneleet.com) to perform a full-coverage, gray box penetration test against the platform's entire attack surface to identify vulnerabilities according to industry standards (OWASP, ASVS, WSTG, TOP-10, etc.). +Most recently, Infisical commissioned cybersecurity firm [Cure53](https://cure53.de/) to perform a full-coverage, gray box penetration test against the platform's entire attack surface to identify vulnerabilities according to industry standards (OWASP, ASVS, WSTG, TOP-10, etc.). Please email security@infisical.com to request any reports including a letter of attestation for the conducted penetration test. @@ -179,7 +116,7 @@ Whether or not Infisical or your employees can access data in the Infisical inst It should be noted that, even on Infisical Cloud, it is physically impossible for employees of Infisical to view the values of secrets if users have not explicitly granted Infisical access to their project (i.e. opted out of zero-knowledge). -Please email security@infisical.com if you have any specific inquiries about employee data access policies. +Please email security@infisical.com if you have any specific inquiries about employee data and security policies. ## Get in touch diff --git a/docs/mint.json b/docs/mint.json index 5c0b4a493..3bdad3fe0 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -85,6 +85,10 @@ "documentation/guides/microsoft-power-apps", "documentation/guides/organization-structure" ] + }, + { + "group": "Setup", + "pages": ["documentation/setup/networking"] } ] }, @@ -114,11 +118,14 @@ "documentation/platform/pki/alerting" ] }, + "documentation/platform/ssh", { "group": "Key Management (KMS)", "pages": [ "documentation/platform/kms/overview", - "documentation/platform/kms/kubernetes-encryption" + "documentation/platform/kms/hsm-integration", + "documentation/platform/kms/kubernetes-encryption", + "documentation/platform/kms/kmip" ] }, { @@ -126,7 +133,8 @@ "pages": [ "documentation/platform/kms-configuration/overview", "documentation/platform/kms-configuration/aws-kms", - "documentation/platform/kms-configuration/aws-hsm" + "documentation/platform/kms-configuration/aws-hsm", + "documentation/platform/kms-configuration/gcp-kms" ] }, { @@ -142,10 +150,18 @@ "pages": [ "documentation/platform/access-controls/overview", "documentation/platform/access-controls/role-based-access-controls", - "documentation/platform/access-controls/attribute-based-access-controls", + { + "group": "Attribute based access controls", + "pages": [ + "documentation/platform/access-controls/abac/overview", + "documentation/platform/access-controls/abac/managing-user-metadata", + "documentation/platform/access-controls/abac/managing-machine-identity-attributes" + ] + }, "documentation/platform/access-controls/additional-privileges", "documentation/platform/access-controls/temporary-access", "documentation/platform/access-controls/access-requests", + "documentation/platform/access-controls/project-access-requests", "documentation/platform/pr-workflows", "documentation/platform/groups" ] @@ -162,11 +178,9 @@ "group": "Secret Rotation", "pages": [ "documentation/platform/secret-rotation/overview", - "documentation/platform/secret-rotation/sendgrid", - "documentation/platform/secret-rotation/postgres", - "documentation/platform/secret-rotation/mysql", - "documentation/platform/secret-rotation/mssql", - "documentation/platform/secret-rotation/aws-iam" + "documentation/platform/secret-rotation/auth0-client-secret", + "documentation/platform/secret-rotation/postgres-credentials", + "documentation/platform/secret-rotation/mssql-credentials" ] }, { @@ -187,8 +201,17 @@ "documentation/platform/dynamic-secrets/mongo-db", "documentation/platform/dynamic-secrets/azure-entra-id", "documentation/platform/dynamic-secrets/ldap", + "documentation/platform/dynamic-secrets/sap-ase", "documentation/platform/dynamic-secrets/sap-hana", - "documentation/platform/dynamic-secrets/snowflake" + "documentation/platform/dynamic-secrets/snowflake", + "documentation/platform/dynamic-secrets/totp" + ] + }, + { + "group": "Gateway", + "pages": [ + "documentation/platform/gateways/overview", + "documentation/platform/gateways/gateway-security" ] }, "documentation/platform/project-templates", @@ -206,7 +229,8 @@ "documentation/platform/admin-panel/org-admin-console" ] }, - "documentation/platform/secret-sharing" + "documentation/platform/secret-sharing", + "documentation/platform/secret-scanning" ] }, { @@ -220,12 +244,15 @@ "documentation/platform/identities/gcp-auth", "documentation/platform/identities/azure-auth", "documentation/platform/identities/aws-auth", + "documentation/platform/identities/jwt-auth", { "group": "OIDC Auth", "pages": [ "documentation/platform/identities/oidc-auth/general", "documentation/platform/identities/oidc-auth/github", - "documentation/platform/identities/oidc-auth/circleci" + "documentation/platform/identities/oidc-auth/circleci", + "documentation/platform/identities/oidc-auth/gitlab", + "documentation/platform/identities/oidc-auth/terraform-cloud" ] }, "documentation/platform/mfa", @@ -241,7 +268,14 @@ "documentation/platform/sso/jumpcloud", "documentation/platform/sso/keycloak-saml", "documentation/platform/sso/google-saml", - "documentation/platform/sso/keycloak-oidc", + "documentation/platform/sso/auth0-saml", + { + "group": "Keycloak OIDC", + "pages": [ + "documentation/platform/sso/keycloak-oidc/overview", + "documentation/platform/sso/keycloak-oidc/group-membership-mapping" + ] + }, "documentation/platform/sso/auth0-oidc", "documentation/platform/sso/general-oidc" ] @@ -271,7 +305,7 @@ "pages": [ "self-hosting/overview", { - "group": "Containerized installation methods", + "group": "Installation methods", "pages": [ "self-hosting/deployment-options/standalone-infisical", "self-hosting/deployment-options/docker-swarm", @@ -286,19 +320,25 @@ "self-hosting/deployment-options/native/linux-package/commands-configuration" ] }, + "self-hosting/guides/upgrading-infisical", "self-hosting/configuration/envars", "self-hosting/configuration/requirements", { "group": "Guides", "pages": [ - "self-hosting/configuration/schema-migrations", "self-hosting/guides/mongo-to-postgres", - "self-hosting/guides/custom-certificates" + "self-hosting/guides/custom-certificates", + "self-hosting/guides/automated-bootstrapping" ] }, { "group": "Reference architectures", - "pages": ["self-hosting/reference-architectures/aws-ecs"] + "pages": [ + "self-hosting/reference-architectures/aws-ecs", + "self-hosting/reference-architectures/linux-deployment-ha", + "self-hosting/reference-architectures/on-prem-k8s-ha", + "self-hosting/reference-architectures/google-cloud-run" + ] }, "self-hosting/ee", "self-hosting/faq" @@ -316,6 +356,10 @@ "cli/commands/init", "cli/commands/run", "cli/commands/secrets", + "cli/commands/dynamic-secrets", + "cli/commands/ssh", + "cli/commands/gateway", + "cli/commands/bootstrap", "cli/commands/export", "cli/commands/token", "cli/commands/service-token", @@ -343,7 +387,16 @@ { "group": "Container orchestrators", "pages": [ - "integrations/platforms/kubernetes", + { + "group": "Kubernetes", + "pages": [ + "integrations/platforms/kubernetes/overview", + "integrations/platforms/kubernetes/infisical-secret-crd", + "integrations/platforms/kubernetes/infisical-push-secret-crd", + "integrations/platforms/kubernetes/infisical-dynamic-secret-crd" + ] + }, + "integrations/platforms/kubernetes-csi", "integrations/platforms/docker-swarm-with-agent", "integrations/platforms/ecs-with-agent" ] @@ -359,7 +412,56 @@ ] }, "integrations/frameworks/terraform", - "integrations/platforms/ansible" + "integrations/platforms/ansible", + "integrations/platforms/apache-airflow" + ] + }, + { + "group": "App Connections", + "pages": [ + "integrations/app-connections/overview", + { + "group": "Connections", + "pages": [ + "integrations/app-connections/auth0", + "integrations/app-connections/aws", + "integrations/app-connections/azure-app-configuration", + "integrations/app-connections/azure-key-vault", + "integrations/app-connections/camunda", + "integrations/app-connections/databricks", + "integrations/app-connections/gcp", + "integrations/app-connections/github", + "integrations/app-connections/humanitec", + "integrations/app-connections/mssql", + "integrations/app-connections/postgres", + "integrations/app-connections/terraform-cloud", + "integrations/app-connections/vercel", + "integrations/app-connections/windmill" + ] + } + ] + }, + { + "group": "Secret Syncs", + "pages": [ + "integrations/secret-syncs/overview", + { + "group": "Syncs", + "pages": [ + "integrations/secret-syncs/aws-parameter-store", + "integrations/secret-syncs/aws-secrets-manager", + "integrations/secret-syncs/azure-app-configuration", + "integrations/secret-syncs/azure-key-vault", + "integrations/secret-syncs/camunda", + "integrations/secret-syncs/databricks", + "integrations/secret-syncs/gcp-secret-manager", + "integrations/secret-syncs/github", + "integrations/secret-syncs/humanitec", + "integrations/secret-syncs/terraform-cloud", + "integrations/secret-syncs/vercel", + "integrations/secret-syncs/windmill" + ] + } ] }, { @@ -423,7 +525,8 @@ "integrations/cicd/travisci", "integrations/cicd/rundeck", "integrations/cicd/codefresh", - "integrations/cloud/checkly" + "integrations/cloud/checkly", + "integrations/cicd/octopus-deploy" ] } ] @@ -451,7 +554,8 @@ "integrations/frameworks/laravel", "integrations/frameworks/rails", "integrations/frameworks/dotnet", - "integrations/platforms/pm2" + "integrations/platforms/pm2", + "integrations/frameworks/ab-initio" ] } ] @@ -460,6 +564,12 @@ "group": "Build Tool Integrations", "pages": ["integrations/build-tools/gradle"] }, + { + "group": "Others", + "pages": [ + "integrations/external/backstage" + ] + }, { "group": "", "pages": ["sdks/overview"] @@ -469,9 +579,9 @@ "pages": [ "sdks/languages/node", "sdks/languages/python", + "sdks/languages/java", "sdks/languages/go", "sdks/languages/ruby", - "sdks/languages/java", "sdks/languages/csharp" ] }, @@ -496,7 +606,8 @@ "api-reference/endpoints/identities/update", "api-reference/endpoints/identities/delete", "api-reference/endpoints/identities/get-by-id", - "api-reference/endpoints/identities/list" + "api-reference/endpoints/identities/list", + "api-reference/endpoints/identities/search" ] }, { @@ -578,6 +689,16 @@ "api-reference/endpoints/oidc-auth/revoke" ] }, + { + "group": "JWT Auth", + "pages": [ + "api-reference/endpoints/jwt-auth/login", + "api-reference/endpoints/jwt-auth/attach", + "api-reference/endpoints/jwt-auth/retrieve", + "api-reference/endpoints/jwt-auth/update", + "api-reference/endpoints/jwt-auth/revoke" + ] + }, { "group": "Groups", "pages": [ @@ -730,6 +851,52 @@ "api-reference/endpoints/secret-imports/delete" ] }, + { + "group": "Secret Rotations", + "pages": [ + "api-reference/endpoints/secret-rotations/list", + "api-reference/endpoints/secret-rotations/options", + { + "group": "Auth0 Client Secret", + "pages": [ + "api-reference/endpoints/secret-rotations/auth0-client-secret/create", + "api-reference/endpoints/secret-rotations/auth0-client-secret/delete", + "api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-id", + "api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-name", + "api-reference/endpoints/secret-rotations/auth0-client-secret/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/auth0-client-secret/list", + "api-reference/endpoints/secret-rotations/auth0-client-secret/rotate-secrets", + "api-reference/endpoints/secret-rotations/auth0-client-secret/update" + ] + }, + { + "group": "Microsoft SQL Server Credentials", + "pages": [ + "api-reference/endpoints/secret-rotations/mssql-credentials/create", + "api-reference/endpoints/secret-rotations/mssql-credentials/delete", + "api-reference/endpoints/secret-rotations/mssql-credentials/get-by-id", + "api-reference/endpoints/secret-rotations/mssql-credentials/get-by-name", + "api-reference/endpoints/secret-rotations/mssql-credentials/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/mssql-credentials/list", + "api-reference/endpoints/secret-rotations/mssql-credentials/rotate-secrets", + "api-reference/endpoints/secret-rotations/mssql-credentials/update" + ] + }, + { + "group": "PostgreSQL Credentials", + "pages": [ + "api-reference/endpoints/secret-rotations/postgres-credentials/create", + "api-reference/endpoints/secret-rotations/postgres-credentials/delete", + "api-reference/endpoints/secret-rotations/postgres-credentials/get-by-id", + "api-reference/endpoints/secret-rotations/postgres-credentials/get-by-name", + "api-reference/endpoints/secret-rotations/postgres-credentials/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/postgres-credentials/list", + "api-reference/endpoints/secret-rotations/postgres-credentials/rotate-secrets", + "api-reference/endpoints/secret-rotations/postgres-credentials/update" + ] + } + ] + }, { "group": "Identity Specific Privilege", "pages": [ @@ -741,6 +908,351 @@ "api-reference/endpoints/identity-specific-privilege/list" ] }, + { + "group": "App Connections", + "pages": [ + "api-reference/endpoints/app-connections/list", + "api-reference/endpoints/app-connections/options", + { + "group": "Auth0", + "pages": [ + "api-reference/endpoints/app-connections/auth0/list", + "api-reference/endpoints/app-connections/auth0/available", + "api-reference/endpoints/app-connections/auth0/get-by-id", + "api-reference/endpoints/app-connections/auth0/get-by-name", + "api-reference/endpoints/app-connections/auth0/create", + "api-reference/endpoints/app-connections/auth0/update", + "api-reference/endpoints/app-connections/auth0/delete" + ] + }, + { + "group": "AWS", + "pages": [ + "api-reference/endpoints/app-connections/aws/list", + "api-reference/endpoints/app-connections/aws/available", + "api-reference/endpoints/app-connections/aws/get-by-id", + "api-reference/endpoints/app-connections/aws/get-by-name", + "api-reference/endpoints/app-connections/aws/create", + "api-reference/endpoints/app-connections/aws/update", + "api-reference/endpoints/app-connections/aws/delete" + ] + }, + { + "group": "Azure App Configuration", + "pages": [ + "api-reference/endpoints/app-connections/azure-app-configuration/list", + "api-reference/endpoints/app-connections/azure-app-configuration/available", + "api-reference/endpoints/app-connections/azure-app-configuration/get-by-id", + "api-reference/endpoints/app-connections/azure-app-configuration/get-by-name", + "api-reference/endpoints/app-connections/azure-app-configuration/create", + "api-reference/endpoints/app-connections/azure-app-configuration/update", + "api-reference/endpoints/app-connections/azure-app-configuration/delete" + ] + }, + { + "group": "Azure Key Vault", + "pages": [ + "api-reference/endpoints/app-connections/azure-key-vault/list", + "api-reference/endpoints/app-connections/azure-key-vault/available", + "api-reference/endpoints/app-connections/azure-key-vault/get-by-id", + "api-reference/endpoints/app-connections/azure-key-vault/get-by-name", + "api-reference/endpoints/app-connections/azure-key-vault/create", + "api-reference/endpoints/app-connections/azure-key-vault/update", + "api-reference/endpoints/app-connections/azure-key-vault/delete" + ] + }, + { + "group": "Camunda", + "pages": [ + "api-reference/endpoints/app-connections/camunda/list", + "api-reference/endpoints/app-connections/camunda/available", + "api-reference/endpoints/app-connections/camunda/get-by-id", + "api-reference/endpoints/app-connections/camunda/get-by-name", + "api-reference/endpoints/app-connections/camunda/create", + "api-reference/endpoints/app-connections/camunda/update", + "api-reference/endpoints/app-connections/camunda/delete" + ] + }, + { + "group": "Databricks", + "pages": [ + "api-reference/endpoints/app-connections/databricks/list", + "api-reference/endpoints/app-connections/databricks/available", + "api-reference/endpoints/app-connections/databricks/get-by-id", + "api-reference/endpoints/app-connections/databricks/get-by-name", + "api-reference/endpoints/app-connections/databricks/create", + "api-reference/endpoints/app-connections/databricks/update", + "api-reference/endpoints/app-connections/databricks/delete" + ] + }, + { + "group": "GCP", + "pages": [ + "api-reference/endpoints/app-connections/gcp/list", + "api-reference/endpoints/app-connections/gcp/available", + "api-reference/endpoints/app-connections/gcp/get-by-id", + "api-reference/endpoints/app-connections/gcp/get-by-name", + "api-reference/endpoints/app-connections/gcp/create", + "api-reference/endpoints/app-connections/gcp/update", + "api-reference/endpoints/app-connections/gcp/delete" + ] + }, + { + "group": "GitHub", + "pages": [ + "api-reference/endpoints/app-connections/github/list", + "api-reference/endpoints/app-connections/github/available", + "api-reference/endpoints/app-connections/github/get-by-id", + "api-reference/endpoints/app-connections/github/get-by-name", + "api-reference/endpoints/app-connections/github/create", + "api-reference/endpoints/app-connections/github/update", + "api-reference/endpoints/app-connections/github/delete" + ] + }, + { + "group": "Humanitec", + "pages": [ + "api-reference/endpoints/app-connections/humanitec/list", + "api-reference/endpoints/app-connections/humanitec/available", + "api-reference/endpoints/app-connections/humanitec/get-by-id", + "api-reference/endpoints/app-connections/humanitec/get-by-name", + "api-reference/endpoints/app-connections/humanitec/create", + "api-reference/endpoints/app-connections/humanitec/update", + "api-reference/endpoints/app-connections/humanitec/delete" + ] + }, + { + "group": "Microsoft SQL Server", + "pages": [ + "api-reference/endpoints/app-connections/mssql/list", + "api-reference/endpoints/app-connections/mssql/available", + "api-reference/endpoints/app-connections/mssql/get-by-id", + "api-reference/endpoints/app-connections/mssql/get-by-name", + "api-reference/endpoints/app-connections/mssql/create", + "api-reference/endpoints/app-connections/mssql/update", + "api-reference/endpoints/app-connections/mssql/delete" + ] + }, + { + "group": "PostgreSQL", + "pages": [ + "api-reference/endpoints/app-connections/postgres/list", + "api-reference/endpoints/app-connections/postgres/available", + "api-reference/endpoints/app-connections/postgres/get-by-id", + "api-reference/endpoints/app-connections/postgres/get-by-name", + "api-reference/endpoints/app-connections/postgres/create", + "api-reference/endpoints/app-connections/postgres/update", + "api-reference/endpoints/app-connections/postgres/delete" + ] + }, + { + "group": "Terraform Cloud", + "pages": [ + "api-reference/endpoints/app-connections/terraform-cloud/list", + "api-reference/endpoints/app-connections/terraform-cloud/available", + "api-reference/endpoints/app-connections/terraform-cloud/get-by-id", + "api-reference/endpoints/app-connections/terraform-cloud/get-by-name", + "api-reference/endpoints/app-connections/terraform-cloud/create", + "api-reference/endpoints/app-connections/terraform-cloud/update", + "api-reference/endpoints/app-connections/terraform-cloud/delete" + ] + }, + { + "group": "Vercel", + "pages": [ + "api-reference/endpoints/app-connections/vercel/list", + "api-reference/endpoints/app-connections/vercel/available", + "api-reference/endpoints/app-connections/vercel/get-by-id", + "api-reference/endpoints/app-connections/vercel/get-by-name", + "api-reference/endpoints/app-connections/vercel/create", + "api-reference/endpoints/app-connections/vercel/update", + "api-reference/endpoints/app-connections/vercel/delete" + ] + }, + { + "group": "Windmill", + "pages": [ + "api-reference/endpoints/app-connections/windmill/list", + "api-reference/endpoints/app-connections/windmill/available", + "api-reference/endpoints/app-connections/windmill/get-by-id", + "api-reference/endpoints/app-connections/windmill/get-by-name", + "api-reference/endpoints/app-connections/windmill/create", + "api-reference/endpoints/app-connections/windmill/update", + "api-reference/endpoints/app-connections/windmill/delete" + ] + } + ] + }, + { + "group": "Secret Syncs", + "pages": [ + "api-reference/endpoints/secret-syncs/list", + "api-reference/endpoints/secret-syncs/options", + { + "group": "AWS Parameter Store", + "pages": [ + "api-reference/endpoints/secret-syncs/aws-parameter-store/list", + "api-reference/endpoints/secret-syncs/aws-parameter-store/get-by-id", + "api-reference/endpoints/secret-syncs/aws-parameter-store/get-by-name", + "api-reference/endpoints/secret-syncs/aws-parameter-store/create", + "api-reference/endpoints/secret-syncs/aws-parameter-store/update", + "api-reference/endpoints/secret-syncs/aws-parameter-store/delete", + "api-reference/endpoints/secret-syncs/aws-parameter-store/sync-secrets", + "api-reference/endpoints/secret-syncs/aws-parameter-store/import-secrets", + "api-reference/endpoints/secret-syncs/aws-parameter-store/remove-secrets" + ] + }, + { + "group": "AWS Secrets Manager", + "pages": [ + "api-reference/endpoints/secret-syncs/aws-secrets-manager/list", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-id", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-name", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/create", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/update", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/delete", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/sync-secrets", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/import-secrets", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/remove-secrets" + ] + }, + { + "group": "Azure App Configuration", + "pages": [ + "api-reference/endpoints/secret-syncs/azure-app-configuration/list", + "api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-id", + "api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-name", + "api-reference/endpoints/secret-syncs/azure-app-configuration/create", + "api-reference/endpoints/secret-syncs/azure-app-configuration/update", + "api-reference/endpoints/secret-syncs/azure-app-configuration/delete", + "api-reference/endpoints/secret-syncs/azure-app-configuration/sync-secrets", + "api-reference/endpoints/secret-syncs/azure-app-configuration/import-secrets", + "api-reference/endpoints/secret-syncs/azure-app-configuration/remove-secrets" + ] + }, + { + "group": "Azure Key Vault", + "pages": [ + "api-reference/endpoints/secret-syncs/azure-key-vault/list", + "api-reference/endpoints/secret-syncs/azure-key-vault/get-by-id", + "api-reference/endpoints/secret-syncs/azure-key-vault/get-by-name", + "api-reference/endpoints/secret-syncs/azure-key-vault/create", + "api-reference/endpoints/secret-syncs/azure-key-vault/update", + "api-reference/endpoints/secret-syncs/azure-key-vault/delete", + "api-reference/endpoints/secret-syncs/azure-key-vault/sync-secrets", + "api-reference/endpoints/secret-syncs/azure-key-vault/import-secrets", + "api-reference/endpoints/secret-syncs/azure-key-vault/remove-secrets" + ] + }, + { + "group": "Camunda", + "pages": [ + "api-reference/endpoints/secret-syncs/camunda/list", + "api-reference/endpoints/secret-syncs/camunda/get-by-id", + "api-reference/endpoints/secret-syncs/camunda/get-by-name", + "api-reference/endpoints/secret-syncs/camunda/create", + "api-reference/endpoints/secret-syncs/camunda/update", + "api-reference/endpoints/secret-syncs/camunda/delete", + "api-reference/endpoints/secret-syncs/camunda/sync-secrets", + "api-reference/endpoints/secret-syncs/camunda/remove-secrets" + ] + }, + { + "group": "Databricks", + "pages": [ + "api-reference/endpoints/secret-syncs/databricks/list", + "api-reference/endpoints/secret-syncs/databricks/get-by-id", + "api-reference/endpoints/secret-syncs/databricks/get-by-name", + "api-reference/endpoints/secret-syncs/databricks/create", + "api-reference/endpoints/secret-syncs/databricks/update", + "api-reference/endpoints/secret-syncs/databricks/delete", + "api-reference/endpoints/secret-syncs/databricks/sync-secrets", + "api-reference/endpoints/secret-syncs/databricks/remove-secrets" + ] + }, + { + "group": "GCP Secret Manager", + "pages": [ + "api-reference/endpoints/secret-syncs/gcp-secret-manager/list", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-id", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-name", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/create", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/update", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/delete", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/sync-secrets", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/import-secrets", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/remove-secrets" + ] + }, + { + "group": "GitHub", + "pages": [ + "api-reference/endpoints/secret-syncs/github/list", + "api-reference/endpoints/secret-syncs/github/get-by-id", + "api-reference/endpoints/secret-syncs/github/get-by-name", + "api-reference/endpoints/secret-syncs/github/create", + "api-reference/endpoints/secret-syncs/github/update", + "api-reference/endpoints/secret-syncs/github/delete", + "api-reference/endpoints/secret-syncs/github/sync-secrets", + "api-reference/endpoints/secret-syncs/github/remove-secrets" + ] + }, + { + "group": "Humanitec", + "pages": [ + "api-reference/endpoints/secret-syncs/humanitec/list", + "api-reference/endpoints/secret-syncs/humanitec/get-by-id", + "api-reference/endpoints/secret-syncs/humanitec/get-by-name", + "api-reference/endpoints/secret-syncs/humanitec/create", + "api-reference/endpoints/secret-syncs/humanitec/update", + "api-reference/endpoints/secret-syncs/humanitec/delete", + "api-reference/endpoints/secret-syncs/humanitec/sync-secrets", + "api-reference/endpoints/secret-syncs/humanitec/remove-secrets" + ] + }, + { + "group": "Terraform Cloud", + "pages": [ + "api-reference/endpoints/secret-syncs/terraform-cloud/list", + "api-reference/endpoints/secret-syncs/terraform-cloud/get-by-id", + "api-reference/endpoints/secret-syncs/terraform-cloud/get-by-name", + "api-reference/endpoints/secret-syncs/terraform-cloud/create", + "api-reference/endpoints/secret-syncs/terraform-cloud/update", + "api-reference/endpoints/secret-syncs/terraform-cloud/delete", + "api-reference/endpoints/secret-syncs/terraform-cloud/sync-secrets", + "api-reference/endpoints/secret-syncs/terraform-cloud/remove-secrets" + ] + }, + { + "group": "Vercel", + "pages": [ + "api-reference/endpoints/secret-syncs/vercel/list", + "api-reference/endpoints/secret-syncs/vercel/get-by-id", + "api-reference/endpoints/secret-syncs/vercel/get-by-name", + "api-reference/endpoints/secret-syncs/vercel/create", + "api-reference/endpoints/secret-syncs/vercel/update", + "api-reference/endpoints/secret-syncs/vercel/delete", + "api-reference/endpoints/secret-syncs/vercel/sync-secrets", + "api-reference/endpoints/secret-syncs/vercel/import-secrets", + "api-reference/endpoints/secret-syncs/vercel/remove-secrets" + ] + }, + { + "group": "Windmill", + "pages": [ + "api-reference/endpoints/secret-syncs/windmill/list", + "api-reference/endpoints/secret-syncs/windmill/get-by-id", + "api-reference/endpoints/secret-syncs/windmill/get-by-name", + "api-reference/endpoints/secret-syncs/windmill/create", + "api-reference/endpoints/secret-syncs/windmill/update", + "api-reference/endpoints/secret-syncs/windmill/delete", + "api-reference/endpoints/secret-syncs/windmill/sync-secrets", + "api-reference/endpoints/secret-syncs/windmill/import-secrets", + "api-reference/endpoints/secret-syncs/windmill/remove-secrets" + ] + } + ] + }, { "group": "Integrations", "pages": [ @@ -831,6 +1343,40 @@ } ] }, + { + "group": "Infisical SSH", + "pages": [ + { + "group": "Certificates", + "pages": [ + "api-reference/endpoints/ssh/certificates/issue-credentials", + "api-reference/endpoints/ssh/certificates/sign-key" + ] + }, + { + "group": "Certificate Authorities", + "pages": [ + "api-reference/endpoints/ssh/ca/list", + "api-reference/endpoints/ssh/ca/create", + "api-reference/endpoints/ssh/ca/read", + "api-reference/endpoints/ssh/ca/update", + "api-reference/endpoints/ssh/ca/delete", + "api-reference/endpoints/ssh/ca/public-key", + "api-reference/endpoints/ssh/ca/list-certificate-templates" + ] + }, + { + "group": "Certificate Templates", + "pages": [ + "api-reference/endpoints/ssh/certificate-templates/list", + "api-reference/endpoints/ssh/certificate-templates/create", + "api-reference/endpoints/ssh/certificate-templates/read", + "api-reference/endpoints/ssh/certificate-templates/update", + "api-reference/endpoints/ssh/certificate-templates/delete" + ] + } + ] + }, { "group": "Infisical KMS", "pages": [ @@ -838,11 +1384,27 @@ "group": "Keys", "pages": [ "api-reference/endpoints/kms/keys/list", + "api-reference/endpoints/kms/keys/get-by-id", + "api-reference/endpoints/kms/keys/get-by-name", "api-reference/endpoints/kms/keys/create", "api-reference/endpoints/kms/keys/update", - "api-reference/endpoints/kms/keys/delete", - "api-reference/endpoints/kms/keys/encrypt", - "api-reference/endpoints/kms/keys/decrypt" + "api-reference/endpoints/kms/keys/delete" + ] + }, + { + "group": "Encryption", + "pages": [ + "api-reference/endpoints/kms/encryption/encrypt", + "api-reference/endpoints/kms/encryption/decrypt" + ] + }, + { + "group": "Signing", + "pages": [ + "api-reference/endpoints/kms/signing/sign", + "api-reference/endpoints/kms/signing/verify", + "api-reference/endpoints/kms/signing/public-key", + "api-reference/endpoints/kms/signing/signing-algorithms" ] } ] @@ -851,9 +1413,16 @@ "group": "Internals", "pages": [ "internals/overview", - "internals/permissions", + { + "group": "Permissions", + "pages": [ + "internals/permissions/overview", + "internals/permissions/project-permissions", + "internals/permissions/organization-permissions", + "internals/permissions/migration" + ] + }, "internals/components", - "internals/flows", "internals/security", "internals/service-tokens" ] @@ -889,9 +1458,6 @@ ] } ], - "integrations": { - "intercom": "hsg644ru" - }, "analytics": { "koala": { "publicApiKey": "pk_b50d7184e0e39ddd5cdb43cf6abeadd9b97d" @@ -908,13 +1474,22 @@ { "title": "PRODUCT", "links": [ - { "label": "Secret Management", "url": "https://infisical.com/" }, - { "label": "Secret Scanning", "url": "https://infisical.com/radar" }, + { + "label": "Secret Management", + "url": "https://infisical.com/" + }, + { + "label": "Secret Scanning", + "url": "https://infisical.com/radar" + }, { "label": "Share Secrets", "url": "https://app.infisical.com/share-secret" }, - { "label": "Pricing", "url": "https://infisical.com/pricing" }, + { + "label": "Pricing", + "url": "https://infisical.com/pricing" + }, { "label": "Security", "url": "https://infisical.com/docs/internals/security" diff --git a/docs/sdks/languages/csharp.mdx b/docs/sdks/languages/csharp.mdx index e6cfd7f19..4cf75a5c6 100644 --- a/docs/sdks/languages/csharp.mdx +++ b/docs/sdks/languages/csharp.mdx @@ -9,6 +9,12 @@ If you're working with C#, the official [Infisical C# SDK](https://github.com/In - [Nuget Package](https://www.nuget.org/packages/Infisical.Sdk) - [Github Repository](https://github.com/Infisical/sdk/tree/main/languages/csharp) + + **Deprecation Notice** + + All versions prior to **2.3.9** should be considered deprecated and are no longer supported by Infisical. Please update to version **2.3.9** or newer. All changes are fully backwards compatible with older versions. + + ## Basic Usage ```cs @@ -42,7 +48,7 @@ namespace Example ProjectId = "PROJECT_ID", Environment = "dev", }; - var secret = infisical.GetSecret(getSecretOptions); + var secret = infisicalClient.GetSecret(getSecretOptions); Console.WriteLine($"The value of secret '{secret.SecretKey}', is: {secret.SecretValue}"); diff --git a/docs/sdks/languages/go.mdx b/docs/sdks/languages/go.mdx index 18fabf64e..b12b442a8 100644 --- a/docs/sdks/languages/go.mdx +++ b/docs/sdks/languages/go.mdx @@ -4,8 +4,6 @@ sidebarTitle: "Go" icon: "golang" --- - - If you're working with Go Lang, the official [Infisical Go SDK](https://github.com/infisical/go-sdk) package is the easiest way to fetch and work with secrets for your application. - [Package](https://pkg.go.dev/github.com/infisical/go-sdk) @@ -30,7 +28,7 @@ func main() { AutoTokenRefresh: true, // Wether or not to let the SDK handle the access token lifecycle. Defaults to true if not specified. }) - _, err = client.Auth().UniversalAuthLogin("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET") + _, err := client.Auth().UniversalAuthLogin("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET") if err != nil { fmt.Printf("Authentication failed: %v", err) @@ -57,7 +55,9 @@ func main() { This example demonstrates how to use the Infisical Go SDK in a simple Go application. The application retrieves a secret named `API_KEY` from the `dev` environment of the `YOUR_PROJECT_ID` project. - We do not recommend hardcoding your [Machine Identity Tokens](/platform/identities/overview). Setting it as an environment variable would be best. + We do not recommend hardcoding your [Machine Identity + Tokens](/platform/identities/overview). Setting it as an environment variable + would be best. # Installation @@ -95,6 +95,14 @@ client := infisical.NewInfisicalClient(context.Background(), infisical.Config{ Whether or not to suppress logs such as warnings from the token refreshing process. Defaults to false if not specified. + + + Defines how long certain responses should be cached in memory, in seconds. When set to a positive value, responses from specific methods (like secret fetching) will be cached for this duration. Set to 0 to disable caching. + + + + Allows you to pass custom headers to the HTTP requests made by the SDK. Expected format is a map of `Header1: Value1, Header2: Value 2`. + @@ -140,6 +148,7 @@ Call `.Auth().UniversalAuthLogin()` with empty arguments to use the following en - `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` - Your machine identity client secret. **Using the SDK directly** + ```go _, err := client.Auth().UniversalAuthLogin("CLIENT_ID", "CLIENT_SECRET") @@ -150,9 +159,12 @@ if err != nil { ``` #### GCP ID Token Auth + - Please note that this authentication method will only work if you're running your application on Google Cloud Platform. - Please [read more](/documentation/platform/identities/gcp-auth) about this authentication method. + Please note that this authentication method will only work if you're running + your application on Google Cloud Platform. Please [read + more](/documentation/platform/identities/gcp-auth) about this authentication + method. **Using environment variables** @@ -162,6 +174,7 @@ Call `.Auth().GcpIdTokenAuthLogin()` with empty arguments to use the following e - `INFISICAL_GCP_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID. **Using the SDK directly** + ```go _, err := client.Auth().GcpIdTokenAuthLogin("YOUR_MACHINE_IDENTITY_ID") @@ -181,6 +194,7 @@ Call `.Auth().GcpIamAuthLogin()` with empty arguments to use the following envir - `INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH` - The path to your GCP service account key file. **Using the SDK directly** + ```go _, err = client.Auth().GcpIamAuthLogin("MACHINE_IDENTITY_ID", "SERVICE_ACCOUNT_KEY_FILE_PATH") @@ -191,9 +205,12 @@ if err != nil { ``` #### AWS IAM Auth + - Please note that this authentication method will only work if you're running your application on AWS. - Please [read more](/documentation/platform/identities/aws-auth) about this authentication method. + Please note that this authentication method will only work if you're running + your application on AWS. Please [read + more](/documentation/platform/identities/aws-auth) about this authentication + method. **Using environment variables** @@ -203,6 +220,7 @@ Call `.Auth().AwsIamAuthLogin()` with empty arguments to use the following envir - `INFISICAL_AWS_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID. **Using the SDK directly** + ```go _, err = client.Auth().AwsIamAuthLogin("MACHINE_IDENTITY_ID") @@ -212,11 +230,13 @@ if err != nil { } ``` - #### Azure Auth + - Please note that this authentication method will only work if you're running your application on Azure. - Please [read more](/documentation/platform/identities/azure-auth) about this authentication method. + Please note that this authentication method will only work if you're running + your application on Azure. Please [read + more](/documentation/platform/identities/azure-auth) about this authentication + method. **Using environment variables** @@ -226,6 +246,7 @@ Call `.Auth().AzureAuthLogin()` with empty arguments to use the following enviro - `INFISICAL_AZURE_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID. **Using the SDK directly** + ```go _, err = client.Auth().AzureAuthLogin("MACHINE_IDENTITY_ID") @@ -236,9 +257,12 @@ if err != nil { ``` #### Kubernetes Auth + - Please note that this authentication method will only work if you're running your application on Kubernetes. - Please [read more](/documentation/platform/identities/kubernetes-auth) about this authentication method. + Please note that this authentication method will only work if you're running + your application on Kubernetes. Please [read + more](/documentation/platform/identities/kubernetes-auth) about this + authentication method. **Using environment variables** @@ -249,6 +273,7 @@ Call `.Auth().KubernetesAuthLogin()` with empty arguments to use the following e - `INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH_ENV_NAME` - The environment variable name that contains the path to the service account token. This is optional and will default to `/var/run/secrets/kubernetes.io/serviceaccount/token`. **Using the SDK directly** + ```go // Service account token path will default to /var/run/secrets/kubernetes.io/serviceaccount/token if empty value is passed _, err = client.Auth().KubernetesAuthLogin("MACHINE_IDENTITY_ID", "SERVICE_ACCOUNT_TOKEN_PATH") @@ -259,9 +284,10 @@ if err != nil { } ``` -## Working With Secrets +## Secrets ### List Secrets + `client.Secrets().List(options)` Retrieve all secrets within the Infisical project and environment that client is connected to. @@ -275,7 +301,7 @@ secrets, err := client.Secrets().List(infisical.ListSecretsOptions{ }) ``` -### Parameters +#### Parameters @@ -311,7 +337,9 @@ secrets, err := client.Secrets().List(infisical.ListSecretsOptions{ ### + ### Retrieve Secret + `client.Secrets().Retrieve(options)` Retrieve a secret from Infisical. By default `Secrets().Retrieve()` fetches and returns a shared secret. @@ -324,30 +352,37 @@ secret, err := client.Secrets().Retrieve(infisical.RetrieveSecretOptions{ }) ``` -### Parameters +#### Parameters - - - The key of the secret to retrieve. - - - The project ID where the secret lives in. - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The path from where secret should be fetched from. - - - The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". - - + + + The key of the secret to retrieve. + + + The project ID where the secret lives in. + + + The slug name (dev, prod, etc) of the environment from where secrets + should be fetched from. + + + The path from where secret should be fetched from. + + + The type of the secret. Valid options are "shared" or "personal". If not + specified, the default value is "shared". + + + The version of the secret to retrieve. + + ### + ### Create Secret + `client.Secrets().Create(options)` Create a new secret in Infisical. @@ -363,36 +398,38 @@ secret, err := client.Secrets().Create(infisical.CreateSecretOptions{ }) ``` - -### Parameters +#### Parameters - - - The key of the secret to create. - - - The value of the secret. - - - A comment for the secret. - - - The project ID where the secret lives in. - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The path from where secret should be created. - - - The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". - - + + + The key of the secret to create. + + + The value of the secret. + + + A comment for the secret. + + + The project ID where the secret lives in. + + + The slug name (dev, prod, etc) of the environment from where secrets + should be fetched from. + + + The path from where secret should be created. + + + The type of the secret. Valid options are "shared" or "personal". If not + specified, the default value is "shared". + + ### + ### Update Secret `client.Secrets().Update(options)` @@ -409,36 +446,45 @@ secret, err := client.Secrets().Update(infisical.UpdateSecretOptions{ }) ``` -### Parameters +#### Parameters - - - The key of the secret to update. - - - The new value of the secret. - - - Whether or not to skip multiline encoding for the new secret value. - - - The project ID where the secret lives in. - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The path from where secret should be updated. - - - The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". - - + + + The key of the secret to update. + + + The new value of the secret. + + + Whether or not to skip multiline encoding for the new secret value. + + + The project ID where the secret lives in. + + + The slug name (dev, prod, etc) of the environment from where secrets + should be fetched from. + + + The path from where secret should be updated. + + + The type of the secret. Valid options are "shared" or "personal". If not + specified, the default value is "shared". + + ### + ### Delete Secret + `client.Secrets().Delete(options)` Delete a secret in Infisical. @@ -451,33 +497,106 @@ secret, err := client.Secrets().Delete(infisical.DeleteSecretOptions{ }) ``` -### Parameters +#### Parameters - - - The key of the secret to update. - - - The project ID where the secret lives in. - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The path from where secret should be deleted. - - - The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". - - + + + The key of the secret to update. + + + The project ID where the secret lives in. + + + The slug name (dev, prod, etc) of the environment from where secrets + should be fetched from. + + + The path from where secret should be deleted. + + + The type of the secret. Valid options are "shared" or "personal". If not + specified, the default value is "shared". + + -## Working With folders +### Batch Create Secrets + +`client.Secrets().Batch().Create(options)` + +Create multiple secrets in Infisical. + +```go + createdSecrets, err := client.Secrets().Batch().Create(infisical.BatchCreateSecretsOptions{ + Environment: "", + SecretPath: "", + ProjectID: "", + Secrets: []infisical.BatchCreateSecret{ + { + SecretKey: "SECRET-1", + SecretValue: "test-value-1", + }, + { + SecretKey: "SECRET-2", + SecretValue: "test-value-2", + }, + }, + }) +``` + +#### Parameters + + + + The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. + + + The project ID where the secret lives in. + + + The path from where secret should be created. + + + + + The key of the secret to create. + + + The value of the secret. + + + The comment to add to the secret. + + + Whether or not to skip multiline encoding for the secret value. + + + The tag IDs to associate with the secret. + + + + + + The key of the metadata. + + + The value of the metadata. + + + + + + + + +## Folders ### + ### List Folders + `client.Folders().List(options)` Retrieve all within the Infisical project and environment that client is connected to. @@ -490,7 +609,7 @@ folders, err := client.Folders().List(infisical.ListFoldersOptions{ }) ``` -### Parameters +#### Parameters @@ -510,7 +629,9 @@ folders, err := client.Folders().List(infisical.ListFoldersOptions{ ### + ### Create Folder + `client.Folders().Create(options)` Create a new folder in Infisical. @@ -524,28 +645,30 @@ folder, err := client.Folders().Create(infisical.CreateFolderOptions{ }) ``` -### Parameters +#### Parameters - - - The ID of the project where the folder will be created. - - - The slug name (dev, prod, etc) of the environment where the folder will be created. - - - The path to create the folder in. The root path is `/`. - - - The name of the folder to create. - - + + + The ID of the project where the folder will be created. + + + The slug name (dev, prod, etc) of the environment where the folder will be + created. + + + The path to create the folder in. The root path is `/`. + + + The name of the folder to create. + + - ### + ### Update Folder + `client.Folders().Update(options)` Update an existing folder in Infisical. @@ -560,30 +683,33 @@ folder, err := client.Folders().Update(infisical.UpdateFolderOptions{ }) ``` -### Parameters +#### Parameters - - - The ID of the project where the folder will be updated. - - - The slug name (dev, prod, etc) of the environment from where the folder lives in. - - - The path from where the folder should be updated. - - - The ID of the folder to update. - - - The new name of the folder. - - + + + The ID of the project where the folder will be updated. + + + The slug name (dev, prod, etc) of the environment from where the folder + lives in. + + + The path from where the folder should be updated. + + + The ID of the folder to update. + + + The new name of the folder. + + ### + ### Delete Folder + `client.Folders().Delete(options)` Delete a folder in Infisical. @@ -599,7 +725,7 @@ deletedFolder, err := client.Folders().Delete(infisical.DeleteFolderOptions{ }) ``` -### Parameters +#### Parameters @@ -620,6 +746,355 @@ deletedFolder, err := client.Folders().Delete(infisical.DeleteFolderOptions{ The path from where the folder should be deleted. + +## KMS +### Create Key + +`client.Kms().Keys().Create(options)` + +Create a new key in Infisical. + +```go + newKey, err := client.Kms().Keys().Create(infisical.KmsCreateKeyOptions{ + KeyUsage: "|", + Description: "", + Name: "", + EncryptionAlgorithm: "|||", + ProjectId: "", + }) +``` + +#### Parameters + + + + + The usage of the key. Valid options are `sign-verify` or `encrypt-decrypt`. + The usage dictates what the key can be used for. + + + The description of the key. + + + The name of the key. + + + The encryption algorithm of the key. + + Valid options for Signing/Verifying keys are: + - `rsa-4096` + - `ecc-nist-p256` + + Valid options for Encryption/Decryption keys are: + - `aes-256-gcm` + - `aes-128-gcm` + + + The ID of the project where the key will be created. + + + + +#### Return (object) + + + + The ID of the key that was created. + + + The name of the key that was created. + + + The description of the key that was created. + + + Whether or not the key is disabled. + + + The ID of the organization that the key belongs to. + + + The ID of the project that the key belongs to. + + + The intended usage of the key that was created. + + + The encryption algorithm of the key that was created. + + + The version of the key that was created. + + + + +### Delete Key + +`client.Kms().Keys().Delete(options)` + +Delete a key in Infisical. + +```go +deletedKey, err = client.Kms().Keys().Delete(infisical.KmsDeleteKeyOptions{ + KeyId: "", + }) +``` + +#### Parameters + + + + + The ID of the key to delete. + + + + +#### Return (object) + + + + The ID of the key that was deleted + + + The name of the key that was deleted. + + + The description of the key that was deleted. + + + Whether or not the key is disabled. + + + The ID of the organization that the key belonged to. + + + The ID of the project that the key belonged to. + + + The intended usage of the key that was deleted. + + + The encryption algorithm of the key that was deleted. + + + The version of the key that was deleted. + + + + +### Signing Data + +`client.Kms().Signing().Sign(options)` +Sign data in Infisical. + +```go +res, err := client.Kms().Signing().SignData(infisical.KmsSignDataOptions{ + KeyId: "", + Data: "", // Must be a base64 encoded string. + SigningAlgorithm: "", // The signing algorithm that will be used to sign the data. +}) +``` + +#### Parameters + + + + + The ID of the key to sign the data with. + + + The data to sign. Must be a base64 encoded string. + + + Whether the data is already digested or not. + + + The signing algorithm to use. You must use a signing algorithm that matches the key usage. + + + If you are unsure about which signing algorithms are available for your key, you can use the `client.Kms().Signing().ListSigningAlgorithms()` method. It will return an array of signing algorithms that are available for your key. + + + Valid options for `RSA 4096` keys are: + - `RSASSA_PSS_SHA_512` + - `RSASSA_PSS_SHA_384` + - `RSASSA_PSS_SHA_256` + - `RSASSA_PKCS1_V1_5_SHA_512` + - `RSASSA_PKCS1_V1_5_SHA_384` + - `RSASSA_PKCS1_V1_5_SHA_256` + + Valid options for `ECC NIST P256` keys are: + - `ECDSA_SHA_512` + - `ECDSA_SHA_384` + - `ECDSA_SHA_256` + + + + +#### Return ([]byte) + + The signature of the data that was signed. + + +### Verifying Data + +`client.Kms().Signing().Verify(options)` +Verify data in Infisical. + +```go +res, err := client.Kms().Signing().Verify(infisical.KmsVerifyDataOptions{ + KeyId: "", + Data: "", // Must be a base64 encoded string. + SigningAlgorithm: "", // The signing algorithm that was used to sign the data. +}) +``` + +#### Parameters + + + + + The ID of the key to verify the data with. + + + The data to verify. Must be a base64 encoded string. + + + Whether the data is already digested or not. + + + The signing algorithm that was used to sign the data. + + + + +#### Return (object) + + + + Whether or not the data is valid. + + + The ID of the key that was used to verify the data. + + + The signing algorithm that was used to verify the data. + + + + +### List Signing Algorithms + +`client.Kms().Signing().ListSigningAlgorithms(options)` +List signing algorithms in Infisical. + +```go +res, err := client.Kms().Signing().ListSigningAlgorithms(infisical.KmsListSigningAlgorithmsOptions{ + KeyId: "", +}) +``` + +#### Parameters + + + + + The ID of the key to list signing algorithms for. + + + + +#### Return ([]string) + + The signing algorithms that are available for the key. + + +### Get Public Key + + This method is only available for keys with key usage `sign-verify`. If you attempt to use this method on a key that is intended for encryption/decryption, it will return an error. + + +`client.Kms().Signing().GetPublicKey(options)` +Get the public key in Infisical. + +```go +publicKey, err := client.Kms().Signing().GetPublicKey(infisical.KmsGetPublicKeyOptions{ + KeyId: "", +}) +``` + +#### Parameters + + + + + The ID of the key to get the public key for. + + + + +#### Return (string) + + The public key for the key. + + +### Encrypt Data + +`client.Kms().Encryption().Encrypt(options)` +Encrypt data with a key in Infisical KMS. + +```go +res, err := client.Kms().EncryptData(infisical.KmsEncryptDataOptions{ + KeyId: "", + Plaintext: "", +}) +``` + +#### Parameters + + + + + The ID of the key to encrypt the data with. + + + + +#### Return (string) + + The encrypted data. + + +### Decrypt Data + +`client.Kms().DecryptData(options)` +Decrypt data with a key in Infisical KMS. + +```go +res, err := client.Kms().DecryptData(infisical.KmsDecryptDataOptions{ + KeyId: "", + Ciphertext: "", +}) +``` + +#### Parameters + + + + + The ID of the key to decrypt the data with. + + + The encrypted data to decrypt. + + + + +#### Return (string) + + The decrypted data. + diff --git a/docs/sdks/languages/java.mdx b/docs/sdks/languages/java.mdx index 3a712322e..dc07b146d 100644 --- a/docs/sdks/languages/java.mdx +++ b/docs/sdks/languages/java.mdx @@ -1,9 +1,12 @@ --- title: "Infisical Java SDK" sidebarTitle: "Java" +url: "https://github.com/Infisical/java-sdk?tab=readme-ov-file#infisical-nodejs-sdk" icon: "java" --- +{ +/* If you're working with Java, the official [Infisical Java SDK](https://github.com/Infisical/sdk/tree/main/languages/java) package is the easiest way to fetch and work with secrets for your application. - [Maven Package](https://github.com/Infisical/sdk/packages/2019741) @@ -568,4 +571,5 @@ String decryptedString = client.decryptSymmetric(decryptOptions); #### Returns (string) -`Plaintext` (string): The decrypted plaintext. \ No newline at end of file +`Plaintext` (string): The decrypted plaintext. +*/} \ No newline at end of file diff --git a/docs/sdks/languages/ruby.mdx b/docs/sdks/languages/ruby.mdx index 617594957..b6fe0863a 100644 --- a/docs/sdks/languages/ruby.mdx +++ b/docs/sdks/languages/ruby.mdx @@ -6,11 +6,17 @@ icon: "diamond" -If you're working with Ruby , the official [Infisical Ruby SDK](https://github.com/infisical/sdk) package is the easiest way to fetch and work with secrets for your application. +If you're working with Ruby, the official [Infisical Ruby SDK](https://github.com/infisical/sdk) package is the easiest way to fetch and work with secrets for your application. - [Ruby Package](https://rubygems.org/gems/infisical-sdk) - [Github Repository](https://github.com/infisical/sdk) + + **Deprecation Notice** + + All versions prior to **2.3.9** should be considered deprecated and are no longer supported by Infisical. Please update to version **2.3.9** or newer. All changes are fully backwards compatible with older versions. + + ## Basic Usage ```ruby diff --git a/docs/sdks/overview.mdx b/docs/sdks/overview.mdx index 11d34bb38..4a047d4e2 100644 --- a/docs/sdks/overview.mdx +++ b/docs/sdks/overview.mdx @@ -16,7 +16,7 @@ From local development to production, Infisical SDKs provide the easiest way for Manage secrets for your Python application on demand - + Manage secrets for your Java application on demand @@ -34,12 +34,6 @@ From local development to production, Infisical SDKs provide the easiest way for ## FAQ - - The client SDK caches every secret and implements a 5-minute waiting period before re-requesting it. The waiting period can be controlled by - setting the `cacheTTL` parameter at the time of initializing the client. - - Note: The exact parameter name may differ depending on the language. - The SDK caches every secret and falls back to the cached value if a request fails. If no cached value ever-existed, the SDK falls back to whatever value is on the process environment. diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 3890547a1..8318869f2 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -30,13 +30,52 @@ Used to configure platform-specific security and operational settings - Telemetry helps us improve Infisical but if you want to dsiable it you may set this to `false`. + Telemetry helps us improve Infisical but if you want to disable it you may set + this to `false`. + + + + Determines whether App Connections and Dynamic Secrets are permitted to + connect with internal/private IP addresses. + + +## CORS + +Cross-Origin Resource Sharing (CORS) is a security feature that allows web applications running on one domain to access resources from another domain. +The following environment variables can be used to configure the Infisical Rest API to allow or restrict access to resources from different origins. + + + +Specify a list of origins that are allowed to access the Infisical API. + +An example value would be `CORS_ALLOWED_ORIGINS=["https://example.com"]`. + +Defaults to the same value as your `SITE_URL` environment variable. + + + + + Array of HTTP methods allowed for CORS requests. + +Defaults to reflecting the headers specified in the request's Access-Control-Request-Headers header. + ## Data Layer The platform utilizes Postgres to persist all of its data and Redis for caching and backgroud tasks +### PostgreSQL + + + Please note that the database user must have **CREATE** privileges along with ability to create and modify tables. This is needed for Infisical to run schema migrations. + + Postgres database connection string. @@ -47,10 +86,6 @@ The platform utilizes Postgres to persist all of its data and Redis for caching `echo "" | base64` - - Redis connection string. - - Postgres database read replica connection strings. It accepts a JSON string. ``` @@ -65,12 +100,19 @@ DB_READ_REPLICAS=[{"DB_CONNECTION_URI":""}] Use the command below to encode your certificate: `echo "" | base64` - If not provided it will use master SSL certificate. + If not provided it will use master SSL certificate. + -## Email service +### Redis + + + Redis connection string. + + +## Email Service Without email configuration, Infisical's core functions like sign-up/login and secret operations work, but this disables multi-factor authentication, email invites for projects, alerts for suspicious logins, and all other email-dependent features. @@ -350,8 +392,9 @@ Optional (for TLS/SSL): TLS: Available on the same ports (2525, 80, 25, 8025, or 587) SSL: Available on ports 465, 8465, and 443 + - + ## Authentication @@ -415,7 +458,56 @@ When set, all visits to the Infisical login page will automatically redirect use information. -## Native secret integrations +## App Connections + +You can configure third-party app connections for re-use across Infisical Projects. + + + + The AWS IAM User access key ID for assuming roles + + + + The AWS IAM User secret key for assuming roles + + + + + + + The ID of the GitHub App + + + + The slug of the GitHub App + + + + The client ID for the GitHub App + + + + The client secret for the GitHub App + + + + The private key for the GitHub App + + + + + + + The OAuth2 client ID for GitHub OAuth Connection + + + + The OAuth2 client secret for GitHub OAuth Connection + + + + +## Native Secret Integrations To help you sync secrets from Infisical to services such as Github and Gitlab, Infisical provides native integrations out of the box. @@ -489,7 +581,7 @@ To help you sync secrets from Infisical to services such as Github and Gitlab, I - + The AWS IAM User access key for assuming roles. @@ -518,3 +610,36 @@ To help you sync secrets from Infisical to services such as Github and Gitlab, I OAuth2 client secret for Gitlab integration + +## Observability + +You can configure Infisical to collect and expose telemetry data for analytics and monitoring. + + + Whether or not to collect and expose telemetry data. + + + + Supported types are `prometheus` and `otlp`. + +If export type is set to `prometheus`, metric data will be exposed in port 9464 in the `/metrics` path. + +If export type is set to `otlp`, you will have to configure a value for `OTEL_EXPORT_OTLP_ENDPOINT`. + + + + + Where telemetry data would be pushed to for collection. This is only + applicable when `OTEL_EXPORT_TYPE` is set to `otlp`. + + + + The username for authenticating with the telemetry collector. + + + The password for authenticating with the telemetry collector. + diff --git a/docs/self-hosting/configuration/requirements.mdx b/docs/self-hosting/configuration/requirements.mdx index e0e992b7f..7ed2691f2 100644 --- a/docs/self-hosting/configuration/requirements.mdx +++ b/docs/self-hosting/configuration/requirements.mdx @@ -22,33 +22,34 @@ The actual resource requirements will vary in direct proportion to the operation Infisical doesn’t require file storage as all persisted data is saved in the database. However, its logs and metrics are saved to disk for later viewing. As a result, we recommend provisioning 1-2 GB of storage. -### CPU +### CPU and Memory (Per Container/Instance) -CPU requirements vary heavily on the volume of secret operations (reads and writes) you anticipate. -Processing large volumes of secrets frequently and consistently will require higher CPU. +Infisical is stateless and scales horizontally by running across multiple containers/instances. Each instance typically does **not** need more than **2–4 CPU cores** and **4–8 GB** of memory. +If you need additional capacity, simply increase the **number** of containers/instances running in parallel. -Recommended minimum CPU hardware for different sizes of deployments: +| **Deployment Size** | **CPU (Cores, per container)** | **Memory (GB, per container)** | **Recommended Number of Containers** | +|---------------------|--------------------------------|--------------------------------|--------------------------------------| +| **Small** | 2 | 4 | 2+ | +| **Medium** | 2–4 | 4–8 | 5+ | +| **Large** | 2–4 | 4–8 | 10+ | -- **small:** 2-4 core is the **recommended** minimum -- **large:** 4-8 cores are suitable for larger deployments - -### Memory Allocation - -Memory needs depend on expected workload, including factors like user activity, automation level, and the frequency of secret operations. - -Recommended minimum memory hardware for different sizes of deployments: -- **small:** 4-8 GB is the **recommended** minimum -- **large:** 16-32 GB are suitable for larger deployments +> **Note:** +> - Adding more containers (horizontal scaling) is generally the best way to handle spikes in secret operations. +> - If you prefer, you can increase CPU/memory on a single container (vertical scaling), but horizontal scaling is more flexible and resilient. ## Database & caching layer ### Postgres PostgreSQL is the only database supported by Infisical. Infisical has been extensively tested with Postgres version 16. We recommend using versions 14 and up for optimal compatibility. +The compute required for Postgres is largely dependent on the number of secret operations (reads and writes) you expect. The more frequently you read and write secrets, the more compute you will need. +You'll notice that storage requirements are high and this is because audit logs are by default stored in the database. -Recommended resource allocation based on deployment size: -- **small:** 2 vCPU / 8 GB RAM / 20 GB Disk -- **large:** 4vCPU / 16 GB RAM / 100 GB Disk + +Recommended resource allocation based on deployment size. You may require more resources if you have a large number of secrets or high transaction volume: +- **small:** 2 vCPU / 8 GB RAM / 100 GB Disk +- **medium:** 4vCPU / 16 GB RAM / 200 GB Disk +- **large:** 8vCPU / 32 GB RAM / 500 GB Disk ### Redis diff --git a/docs/self-hosting/configuration/schema-migrations.mdx b/docs/self-hosting/configuration/schema-migrations.mdx deleted file mode 100644 index 5df52e713..000000000 --- a/docs/self-hosting/configuration/schema-migrations.mdx +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: "Schema migration" -description: "Learn how to run Postgres schema migrations." ---- - -Running schema migrations is a requirement before deploying Infisical. -Each time you decide to upgrade your version of Infisical, it's necessary to run schema migrations for that specific version. -The guide below outlines a step-by-step guide to help you manually run schema migrations for Infisical. - -### Prerequisites -- Docker installed on your machine -- An active PostgreSQL database -- Postgres database connection string - - - - First, ensure you have the correct version of the Infisical Docker image. You can pull it from Docker Hub using the following command: - ```bash - docker pull infisical/infisical: - ``` - Replace `` with the specific version number you intend to deploy. View available versions [here](https://hub.docker.com/r/infisical/infisical/tags) - - - - The Docker image requires a `DB_CONNECTION_URI` environment variable. This connection string should point to your PostgreSQL database. The format generally looks like this: `postgresql://username:password@host:port/database`. - - - - To run the schema migration for the version of Infisical you want to deploy, use the following Docker command: - - ```bash - docker run --env DB_CONNECTION_URI= infisical/infisical: npm run migration:latest - ``` - Replace `` with your actual PostgreSQL connection string, and `` with the desired version number. - - - - After running the migration, it's good practice to check if the migration was successful. You can do this by checking the logs or accessing your database to ensure the schema has been updated accordingly. - - - If you need to rollback a migration by one step, use the following command: - - ```bash - docker run --env DB_CONNECTION_URI= infisical/infisical: npm run migration:rollback - ``` - - - - It's important to run schema migrations for each version of the Infisical you deploy. For instance, if you're updating from `infisical/infisical:1` to `infisical/infisical:2`, ensure you run the schema migrations for `infisical/infisical:2` before deploying it. - - - - - In a production setting, we recommend a more structured approach to deploying migrations prior to upgrading Infisical. This can be accomplished via CI automation. - - -### Additional discussion -- Always back up your database before running migrations, especially in a production environment. -- Test the migration process in a staging environment before applying it to production. -- Keep track of the versions and their corresponding migrations to avoid any inconsistencies. diff --git a/docs/self-hosting/deployment-options/docker-swarm.mdx b/docs/self-hosting/deployment-options/docker-swarm.mdx index 5fce38b29..c7280d879 100644 --- a/docs/self-hosting/deployment-options/docker-swarm.mdx +++ b/docs/self-hosting/deployment-options/docker-swarm.mdx @@ -157,23 +157,6 @@ The [Docker stack file](https://github.com/Infisical/infisical/tree/main/docker- 3lznscvk7k5t infisical_spolo2 replicated 1/1 ghcr.io/zalando/spilo-16:3.2-p2 v04ml7rz2j5q infisical_spolo3 replicated 1/1 ghcr.io/zalando/spilo-16:3.2-p2 ``` - - - You'll notice that service `infisical_infisical` will not be in running state. - This is expected as the database does not yet have the desired schemas. - Once the database schema migrations have been successfully applied, this issue should be resolved. - - - - - Run the schema migration to initialize the database. Follow the [guide here](/self-hosting/configuration/schema-migrations) to learn how. - - To run the migrations, you'll need to connect to the Postgres instance deployed on your Docker swarm. The default Postgres user credentials are defined in the Docker swarm: username: `postgres`, password: `postgres` and database: `postgres`. - We recommend you change these credentials when deploying to production and creating a separate DB for Infisical. - - - After running the schema migrations, be sure to update the `.env` file to have the correct `DB_CONNECTION_URI`. - diff --git a/docs/self-hosting/deployment-options/kubernetes-helm.mdx b/docs/self-hosting/deployment-options/kubernetes-helm.mdx index ac8a098de..e0e5ba75d 100644 --- a/docs/self-hosting/deployment-options/kubernetes-helm.mdx +++ b/docs/self-hosting/deployment-options/kubernetes-helm.mdx @@ -78,18 +78,6 @@ description: "Learn how to use Helm chart to install Infisical on your Kubernete - - Infisical relies a relational database, which means that database schemas need to be migrated before the instance can become operational. - - To automate this process, the chart includes a option named `infisical.autoDatabaseSchemaMigration`. - When this option is enabled, a deployment/upgrade will only occur _after_ a successful schema migration. - - - If you are using in-cluster Postgres, you may notice the migration job failing initially. - This is expected as it is waiting for the database to be in ready state. - - - By default, this chart uses Nginx as its Ingress controller to direct traffic to Infisical services. diff --git a/docs/self-hosting/deployment-options/native/high-availability.mdx b/docs/self-hosting/deployment-options/native/high-availability.mdx deleted file mode 100644 index 931acb4df..000000000 --- a/docs/self-hosting/deployment-options/native/high-availability.mdx +++ /dev/null @@ -1,520 +0,0 @@ ---- -title: "Automatically deploy Infisical with High Availability" -sidebarTitle: "High Availability" ---- - - -# Self-Hosting Infisical with a native High Availability (HA) deployment - -This page describes the Infisical architecture designed to provide high availability (HA) and how to deploy Infisical with high availability. The high availability deployment is designed to ensure that Infisical services are always available and can handle service failures gracefully, without causing service disruptions. - - - This deployment option is currently only available for Debian-based nodes (e.g., Ubuntu, Debian). - We plan on adding support for other operating systems in the future. - - -## High availability architecture -| Service | Nodes | Configuration | GCP | AWS | -|----------------------------------|----------------|------------------------------|---------------|--------------| -| External load balancer$^1$ | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5n.xlarge | -| Internal load balancer$^2$ | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5n.xlarge | -| Etcd cluster$^3$ | 3 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5n.xlarge | -| PostgreSQL$^4$ | 3 | 2 vCPU, 7.5 GB memory | n1-standard-2 | m5.large | -| Sentinel$^4$ | 3 | 2 vCPU, 7.5 GB memory | n1-standard-2 | m5.large | -| Redis$^4$ | 3 | 2 vCPU, 7.5 GB memory | n1-standard-2 | m5.large | -| Infisical Core | 3 | 8 vCPU, 7.2 GB memory | n1-highcpu-8 | c5.2xlarge | - -**Footnotes:** -1. External load balancer: If you wish to have multiple instances of the internal load balancer, you will need to use an external load balancer to distribute incoming traffic across multiple internal load balancers. - Using multiple internal load balancers is recommended for high-traffic environments. In the following guide we will use a single internal load balancer, as external load balancing falls outside the scope of this guide. -2. Internal load balancer: The internal load balancer (a HAProxy instance) is used to distribute incoming traffic across multiple Infisical Core instances, Postgres nodes, and Redis nodes. The internal load balancer exposes a set of ports _(80 for Infiscial, 5000 for Read/Write postgres, 5001 for Read-only postgres, and 6379 for Redis)_. Where these ports route to is determained by the internal load balancer based on the availability and health of the service nodes. - The internal load balancer is only accessible from within the same network, and is not exposed to the public internet. -3. Etcd cluster: Etcd is a distributed key-value store used to store and distribute data between the PostgreSQL nodes. Etcd is dependent on high disk I/O performance, therefore it is highly recommended to use highly performant SSD disks for the Etcd nodes, with _at least_ 80GB of disk space. -4. The Redis and PostgreSQL nodes will automatically be configured for high availability and used in your Infisical Core instances. However, you can optionally choose to bring your own database (BYOD), and skip these nodes. See more on how to [provide your own databases](#provide-your-own-databases). - - - For all services that require multiple nodes, it is recommended to deploy them across multiple availability zones (AZs) to ensure high availability and fault tolerance. This will help prevent service disruptions in the event of an AZ failure. - - -![High availability stack](../../images/self-hosting/deployment-options/native/ha-stack.png) -The image above shows how a high availability deployment of Infisical is structured. In this example, an external load balancer is used to distribute incoming traffic across multiple internal load balancers. The internal load balancers. The external load balancer isn't required, and it will require additional configuration to set up. - -### Fault Tolerance -This setup provides N+1 redundancy, meaning it can tolerate the failure of any single node without service interruption. - -## Ansible -### What is Ansible -Ansible is an open-source automation tool that simplifies application deployment, configuration management, and task automation. -At Infisical, we use Ansible to automate the deployment of Infisical services. The Ansible roles are designed to make it easy to deploy Infisical services in a high availability environment. - -### Installing Ansible - - - ```bash - pipx install --include-deps ansible - ``` - - - ```bash - ansible --version - ``` - - - - -### Understanding Ansible Concepts - -* Inventory _(inventory.ini)_: A file that lists your target hosts. -* Playbook _(playbook.yml)_: YAML file containing a set of tasks to be executed on hosts. -* Roles: Reusable units of organization for playbooks. Roles are used to group tasks together in a structured and reusable manner. - - -### Basic Ansible Commands -Running a playbook with with an invetory file: -```bash - ansible-playbook -i inventory.ini playbook.yml -``` - -This is how you would run the playbook containing the roles for setting up Infisical in a high availability environment. - -### Installing the Infisical High Availability Deployment Ansible Role -The Infisical Ansible role is available on Ansible Galaxy. You can install the role by running the following command: -```bash - ansible-galaxy collection install infisical.infisical_core_ha_deployment -``` - - -## Set up components -1. External load balancer (optional, and not covered in this guide) -2. [Configure Etcd cluster](#configure-etcd-cluster) -3. [Configure PostgreSQL database](#configure-postgresql-database) -4. [Configure Redis/Sentinel](#configure-redis-and-sentinel) -5. [Configure Infisical Core](#configure-infisical-core) - - -The servers start on the same 52.1.0.0/24 private network range, and can connect to each other freely on these addresses. - -The following list includes descriptions of each server and its assigned IP: - -52.1.0.1: External Load Balancer -52.1.0.2: Internal Load Balancer -52.1.0.3: Etcd 1 -52.1.0.4: Etcd 2 -52.1.0.5: Etcd 3 -52.1.0.6: PostgreSQL 1 -52.1.0.7: PostgreSQL 2 -52.1.0.8: PostgreSQL 3 -52.1.0.9: Redis 1 -52.1.0.10: Redis 2 -52.1.0.11: Redis 3 -52.1.0.12: Sentinel 1 -52.1.0.13: Sentinel 2 -52.1.0.14: Sentinel 3 -52.1.0.15: Infisical Core 1 -52.1.0.16: Infisical Core 2 -52.1.0.17: Infisical Core 3 - - - -### Configure Etcd cluster - -Configuring the ETCD cluster is the first step in setting up a high availability deployment of Infisical. -The ETCD cluster is used to store and distribute data between the PostgreSQL nodes. The ETCD cluster is a distributed key-value store that is highly available and fault-tolerant. - -```yaml example.playbook.yml - - hosts: all - gather_facts: true - - - name: Set up etcd cluster - hosts: etcd - become: true - collections: - - infisical.infisical_core_ha_deployment - roles: - - role: etcd -``` - -```ini example.inventory.ini - [etcd] - etcd1 ansible_host=52.1.0.3 - etcd2 ansible_host=52.1.0.4 - etcd3 ansible_host=52.1.0.5 - - [etcd:vars] - ansible_user=ubuntu - ansible_ssh_private_key_file=./ssh-key.pem - ansible_ssh_common_args='-o StrictHostKeyChecking=no' -``` - -### Configure PostgreSQL database - -The Postgres role takes a set of parameters that are used to configure your PostgreSQL database. - -Make sure to set the following variables in your playbook.yml file: -- `postgres_super_user_password`: The password for the 'postgres' database user. -- `postgres_db_name`: The name of the database that will be created on the leader node and replicated to the secondary nodes. -- `postgres_user`: The name of the user that will be created on the leader node and replicated to the secondary nodes. -- `postgres_user_password`: The password for the user that will be created on the leader node and replicated to the secondary nodes. -- `etcd_hosts`: The list of etcd hosts that the PostgreSQL nodes will use to communicate with etcd. By default you want to keep this value set to `"{{ groups['etcd'] }}"` - -```yaml example.playbook.yml - - hosts: all - gather_facts: true - - - name: Set up PostgreSQL with Patroni - hosts: postgres - become: true - collections: - - infisical.infisical_core_ha_deployment - roles: - - role: postgres - vars: - postgres_super_user_password: "your-super-user-password" - postgres_user: infisical-user - postgres_user_password: "your-password" - postgres_db_name: infisical-db - - etcd_hosts: "{{ groups['etcd'] }}" -``` - -```ini example.inventory.ini - [postgres] - postgres1 ansible_host=52.1.0.6 - postgres2 ansible_host=52.1.0.7 - postgres3 ansible_host=52.1.0.8 -``` - -### Configure Redis and Sentinel - -The Redis role takes a single variable as input, which is the redis password. -The Sentinel and Redis hosts will run the same role, therefore we are running the task for both the sentinel and redis hosts, `hosts: redis:sentinel`. - -- `redis_password`: The password that will be set for the Redis instance. - -```yaml example.playbook.yml - - hosts: all - gather_facts: true - - - name: Setup Redis and Sentinel - hosts: redis:sentinel - become: true - collections: - - infisical.infisical_core_ha_deployment - roles: - - role: redis - vars: - redis_password: "REDIS_PASSWORD" -``` - -```ini example.inventory.ini - [redis] - redis1 ansible_host=52.1.0.9 - redis2 ansible_host=52.1.0.10 - redis3 ansible_host=52.1.0.11 - - [sentinel] - sentinel1 ansible_host=52.1.0.12 - sentinel2 ansible_host=52.1.0.13 - sentinel3 ansible_host=52.1.0.14 -``` - -### Configure Internal Load Balancer - -The internal load balancer used is HAProxy. HAProxy will expose a set of ports as listed below. Each port will route to a different service based on the availability and health of the service nodes. - -- Port 80: Infisical Core -- Port 5000: Read/Write PostgreSQL -- Port 5001: Read-only PostgreSQL -- Port 6379: Redis -- Port 7000: HAProxy monitoring -These ports will need to be exposed on your network to become accessible from the outside world. - -The HAProxy configuration file is generated by the Infisical Core role, and is located at `/etc/haproxy/haproxy.cfg` on your internal load balancer node. - -The HAProxy setup comes with a monitoring panel. You have to set the username/password combination for the monitoring panel by setting the `stats_user` and `stats_password` variables in the HAProxy role. - - -Once the HAProxy role has fully executed, you can monitor your HA setup by navigating to `http://52.1.0.2:7000/haproxy?stats` in your browser. - -```ini example.inventory.ini -[haproxy] -internal_lb ansible_host=52.1.0.2 -``` - -```yaml example.playbook.yml -- name: Set up HAProxy - hosts: haproxy - become: true - collections: - - infisical.infisical_core_ha_deployment - roles: - - role: haproxy - vars: - stats_user: "stats-username" - stats_password: "stats-password!" - - postgres_servers: "{{ groups['postgres'] }}" - infisical_servers: "{{ groups['infisical'] }}" - redis_servers: "{{ groups['redis'] }}" -``` - - - -### Configure Infisical Core - -The Infisical Core role will set up your actual Infisical instances. - -The `env_vars` variable is used to set the environment variables that Infisical will use. The minimum required environment variables are `ENCRYPTION_KEY` and `AUTH_SECRET`. You can find a list of all available environment variables [here](/docs/self-hosting/configuration/envars#general-platform). -The `DB_CONNECTION_URI` and `REDIS_URL` variables will automatically be set if you're running the full playbook. However, you can choose to set them yourself, and skip the Postgres, etcd, redis/sentinel roles entirely. - - - If you later need to add new environment varibles to your Infisical deployments, it's important you add the variables to **all** your Infisical nodes.
- You can find the environment file for Infisical at `/etc/infisical/environment`.
- After editing the environment file, you need to reload the Infisical service by doing `systemctl restart infisical`. -
- -```yaml example.playbook.yml - - hosts: all - gather_facts: true - - - name: Setup Infisical - hosts: infisical - become: true - collections: - - infisical.infisical_core_ha_deployment - roles: - - role: infisical - env_vars: - ENCRYPTION_KEY: "YOUR_ENCRYPTION_KEY" # openssl rand -hex 16 - AUTH_SECRET: "YOUR_AUTH_SECRET" # openssl rand -base64 32 -``` - -```ini example.inventory.ini - [infisical] - infisical1 ansible_host=52.1.0.15 - infisical2 ansible_host=52.1.0.16 - infisical3 ansible_host=52.1.0.17 -``` - -## Provide your own databases -Bringing your own database is an option using the Infisical Core deployment role. -By bringing your own database, you're able to skip the Etcd, Postgres, and Redis/Sentinel roles entirely. - -To bring your own database, you need to set the `DB_CONNECTION_URI` and `REDIS_URL` environment variables in the Infisical Core role. - -```yaml example.playbook.yml - - hosts: all - gather_facts: true - - - name: Setup Infisical - hosts: infisical - become: true - collections: - - infisical.infisical_core_ha_deployment - roles: - - role: infisical - env_vars: - ENCRYPTION_KEY: "YOUR_ENCRYPTION_KEY" # openssl rand -hex 16 - AUTH_SECRET: "YOUR_AUTH_SECRET" # openssl rand -base64 32 - DB_CONNECTION_URI: "postgres://user:password@localhost:5432/infisical" - REDIS_URL: "redis://localhost:6379" -``` - -```ini example.inventory.ini - [infisical] - infisical1 ansible_host=52.1.0.15 - infisical2 ansible_host=52.1.0.16 - infisical3 ansible_host=52.1.0.17 -``` - -## Full deployment example -To make it easier to get started, we've provided a full deployment example that you can use to deploy Infisical in a high availability environment. - -- This deployment does not use an external load balancer. -- You **must** change the environment variables defined in the `playbook.yml` example. -- You have update the IP addresses in the `inventory.ini` file to match your own network configuration. -- You need to set the SSH key and ssh user in the `inventory.ini` file. - - - - Install Ansible using the pipx Python package manager. - ```bash - pipx install --include-deps ansible - ``` - - - - Install the Infisical deployment role from Ansible Galaxy. - ```bash - ansible-galaxy collection install infisical.infisical_core_ha_deployment - ``` - - - - Create an `inventory.ini` file, and define your hosts and their IP addresses. You can use the example below as a template, and update the IP addresses to match your own network configuration. - Make sure to set the SSH key and ssh user in the `inventory.ini` file. Please see the example below. - - ```ini example.inventory.ini - [etcd] - etcd1 ansible_host=52.1.0.3 - etcd2 ansible_host=52.1.0.4 - etcd3 ansible_host=52.1.0.5 - - [postgres] - postgres1 ansible_host=52.1.0.6 - postgres2 ansible_host=52.1.0.7 - postgres3 ansible_host=52.1.0.8 - - [infisical] - infisical1 ansible_host=52.1.0.15 - infisical2 ansible_host=52.1.0.16 - infisical3 ansible_host=52.1.0.17 - - [redis] - redis1 ansible_host=52.1.0.9 - redis2 ansible_host=52.1.0.10 - redis3 ansible_host=52.1.0.11 - - [sentinel] - sentinel1 ansible_host=52.1.0.12 - sentinel2 ansible_host=52.1.0.13 - sentinel3 ansible_host=52.1.0.14 - - [haproxy] - internal_lb ansible_host=52.1.0.2 - - ; This can be defined individually for each host, or globally for all hosts. - ; In this case the credentials are the same for all hosts, so we define them globally as seen below ([all:vars]). - [all:vars] - ansible_user=ubuntu - ansible_ssh_private_key_file=./your-ssh-key.pem - ansible_ssh_common_args='-o StrictHostKeyChecking=no' - ``` - - - The Ansible playbook is where you define which roles/tasks to execute on which hosts. - - ```yaml example.playbook.yml - --- - # Important, we must gather facts from all hosts prior to running the roles to ensure we have all the information we need. - - hosts: all - gather_facts: true - - - name: Set up etcd cluster - hosts: etcd - become: true - collections: - - infisical.infisical_core_ha_deployment - roles: - - role: etcd - - - name: Set up PostgreSQL with Patroni - hosts: postgres - become: true - collections: - - infisical.infisical_core_ha_deployment - roles: - - role: postgres - vars: - postgres_super_user_password: "" # Password for the 'postgres' database user - - # A database with these credentials will be created on the leader node, and replicated to the secondary nodes. - postgres_db_name: - postgres_user: - postgres_user_password: - - etcd_hosts: "{{ groups['etcd'] }}" - - - name: Setup Redis and Sentinel - hosts: redis:sentinel - become: true - collections: - - infisical.infisical_core_ha_deployment - roles: - - role: redis - vars: - redis_password: "" - - - name: Set up HAProxy - hosts: haproxy - become: true - collections: - - infisical.infisical_core_ha_deployment - roles: - - role: haproxy - vars: - stats_user: "" - stats_password: "" - - postgres_servers: "{{ groups['postgres'] }}" - infisical_servers: "{{ groups['infisical'] }}" - redis_servers: "{{ groups['redis'] }}" - - name: Setup Infisical - hosts: infisical - become: true - collections: - - infisical.infisical_core_ha_deployment - roles: - - role: infisical - env_vars: - ENCRYPTION_KEY: "YOUR_ENCRYPTION_KEY" # openssl rand -hex 16 - AUTH_SECRET: "YOUR_AUTH_SECRET" # openssl rand -base64 32 - ``` - - - After creating the `playbook.yml` and `inventory.ini` files, you can run the playbook using the following command - ```bash - ansible-playbook -i inventory.ini playbook.yml - ``` - - This step may take upwards of 10 minutes to complete, depending on the number of nodes and the network speed. - Once the playbook has completed, you should have a fully deployed high availability Infisical environment. - - To access Infisical, you can try navigating to `http://52.1.0.2`, in order to view your newly deployed Infisical instance. - - - - -## Post-deployment steps -After deploying Infisical in a high availability environment, you should perform the following post-deployment steps: -- Check your deployment to ensure that all services are running as expected. You can use the HAProxy monitoring panel to check the status of your services (http://52.1.0.2:7000/haproxy?stats) -- Attempt to access the Infisical Core instances to ensure that they are accessible from the internal load balancer. (http://52.1.0.2) - -A HAProxy stats page indicating success will look like the image below -![HAProxy stats page](../../images/self-hosting/deployment-options/native/haproxy-stats.png) - - -## Security Considerations -### Network Security -Secure the network that your instances run on. While this falls outside the scope of Infisical deployment, it's crucial for overall security. -AWS-specific recommendations: - -Use Virtual Private Cloud (VPC) to isolate your infrastructure. -Configure security groups to restrict inbound and outbound traffic. -Use Network Access Control Lists (NACLs) for additional network-level security. - - - Please take note that the Infisical team cannot provide infrastructure support for **free self-hosted** deployments.
If you need help with infrastructure, we recommend upgrading to a [paid plan](https://infisical.com/pricing) which includes infrastructure support. - - You can also join our community [Slack](https://infisical.com/slack) for help and support from the community. -
- - -### Troubleshooting - - If you encounter this issue, please update your ansible config (`ansible.cfg`) file with the following configuration: - ```ini - [defaults] - allow_world_readable_tmpfiles = true - ``` - - You can read more about the solution [here](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/sh_shell.html#parameter-world_readable_temp) - - - - This issue can be caused by a number of reasons, mostly realted to the network configuration. Here are a few things you can check: - 1. Ensure that the firewall is not blocking the connection. You can check this by running `ufw status`. Ensure that port 80 is open. - 2. If you're using a cloud provider like AWS or GCP, ensure that the security group allows traffic on port 80. - 3. Ensure that the HAProxy service is running. You can check this by running `systemctl status haproxy`. - 4. Ensure that the Infisical service is running. You can check this by running `systemctl status infisical`. - \ No newline at end of file diff --git a/docs/self-hosting/deployment-options/standalone-infisical.mdx b/docs/self-hosting/deployment-options/standalone-infisical.mdx index 6e41e4b5f..b7f76ce98 100644 --- a/docs/self-hosting/deployment-options/standalone-infisical.mdx +++ b/docs/self-hosting/deployment-options/standalone-infisical.mdx @@ -22,11 +22,6 @@ The following guide provides a detailed step-by-step walkthrough on how you can Remember to replace `` with the docker image tag of your choice.
- - Before you can start the instance of Infisical, you need to run the database schema migrations. - Follow the step by [step guide here](/self-hosting/configuration/schema-migrations) on running schema migrations for Infisical. - - For a minimal installation of Infisical, you must configure `ENCRYPTION_KEY`, `AUTH_SECRET`, `DB_CONNECTION_URI`, `SITE_URL`, and `REDIS_URL`. [View all available configurations](/self-hosting/configuration/envars). diff --git a/docs/self-hosting/guides/automated-bootstrapping.mdx b/docs/self-hosting/guides/automated-bootstrapping.mdx new file mode 100644 index 000000000..ebc9c3c80 --- /dev/null +++ b/docs/self-hosting/guides/automated-bootstrapping.mdx @@ -0,0 +1,150 @@ +--- +title: "Programmatic Provisioning" +description: "Learn how to provision and configure Infisical instances programmatically without UI interaction" +--- + +Infisical's Automated Bootstrapping feature enables you to provision and configure an Infisical instance without using the UI, allowing for complete automation through static configuration files, API calls, or CLI commands. This is especially valuable for enterprise environments where automated deployment and infrastructure-as-code practices are essential. + +## Overview + +The Automated Bootstrapping workflow automates the following processes: +- Creating an admin user account +- Initializing an organization for the entire instance +- Establishing an **instance admin machine identity** with full administrative permissions +- Returning the machine identity credentials for further automation + +## Key Concepts + +- **Instance Initialization**: Infisical requires [configuration variables](/self-hosting/configuration/envars) to be set during launch, after which the bootstrap process can be triggered. +- **Instance Admin Machine Identity**: The bootstrapping process creates a machine identity with instance-level admin privileges, which can be used to programmatically manage all aspects of the Infisical instance. + ![Instance Admin Identity](/images/self-hosting/guides/automated-bootstrapping/identity-instance-admin.png) +- **Token Auth**: The instance admin machine identity uses [Token Auth](/documentation/platform/identities/token-auth), providing a JWT token that can be used directly to make authenticated requests to the Infisical API. + +## Prerequisites + +- An Infisical instance launched with all required configuration variables +- Access to the Infisical CLI or the ability to make API calls to the instance +- Network connectivity to the Infisical instance + +## Bootstrap Methods + +You can bootstrap an Infisical instance using either the API or the CLI. + + + + Make a POST request to the bootstrap endpoint: + + ``` + POST: http://your-infisical-instance.com/api/v1/admin/bootstrap + { + "email": "admin@example.com", + "password": "your-secure-password", + "organization": "your-org-name" + } + ``` + + Example using curl: + + ```bash + curl -X POST \ + -H "Content-Type: application/json" \ + -d '{"email":"admin@example.com","password":"your-secure-password","organization":"your-org-name"}' \ + http://your-infisical-instance.com/api/v1/admin/bootstrap + ``` + + + Use the [Infisical CLI](/cli/commands/bootstrap) to bootstrap the instance and extract the token for immediate use in automation: + + ```bash + infisical bootstrap --domain="http://localhost:8080" --email="admin@example.com" --password="your-secure-password" --organization="your-org-name" | jq ".identity.credentials.token" + ``` + + This example command pipes the output through `jq` to extract only the machine identity token, making it easy to capture and use directly in automation scripts or export as an environment variable for tools like Terraform. + + + +## API Response Structure + +The bootstrap process returns a JSON response with details about the created user, organization, and machine identity: + +```json +{ + "identity": { + "credentials": { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eUlkIjoiZGIyMjQ3OTItZWQxOC00Mjc3LTlkYWUtNTdlNzUyMzE1ODU0IiwiaWRlbnRpdHlBY2Nlc3NUb2tlbklkIjoiZmVkZmZmMGEtYmU3Yy00NjViLWEwZWEtZjM5OTNjMTg4OGRlIiwiYXV0aFRva2VuVHlwZSI6ImlkZW50aXR5QWNjZXNzVG9rZW4iLCJpYXQiOjE3NDIzMjI0ODl9.mqcZZqIFqER1e9ubrQXp8FbzGYi8nqqZwfMvz09g-8Y" + }, + "id": "db224792-ed18-4277-9dae-57e752315854", + "name": "Instance Admin Identity" + }, + "message": "Successfully bootstrapped instance", + "organization": { + "id": "b56bece0-42f5-4262-b25e-be7bf5f84957", + "name": "dog", + "slug": "dog-v-e5l" + }, + "user": { + "email": "admin@example.com", + "firstName": "Admin", + "id": "a418f355-c8da-453c-bbc8-6c07208eeb3c", + "lastName": "User", + "superAdmin": true, + "username": "admin@example.com" + } +} +``` + +## Using the Instance Admin Machine Identity Token + +The bootstrap process automatically creates a machine identity with Token Auth configured. The returned token has instance-level admin privileges (the highest level of access) and should be treated with the same security considerations as a root credential. + +The token enables full programmatic control of your Infisical instance and can be used in the following ways: + +### 1. Infrastructure Automation + +Store the token securely for use with infrastructure automation tools. Due to the sensitive nature of this token, ensure it's protected using appropriate secret management practices: + +#### Kubernetes Secret (with appropriate RBAC restrictions) + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: infisical-admin-credentials +type: Opaque +data: + token: +``` + +#### Environment Variable for Terraform + +```bash +export INFISICAL_TOKEN=your-access-token +terraform apply +``` + +### 2. Programmatic Resource Management + +Use the token to authenticate API calls for creating and managing Infisical resources. The token works exactly like any other Token Auth access token in the Infisical API: + +```bash +curl -X POST \ + -H "Authorization: Bearer ${INFISICAL_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{ + "projectName": "New Project", + "projectDescription": "A project created via API", + "slug": "new-project-slug", + "template": "default", + "type": "SECRET_MANAGER" + }' \ + https://your-infisical-instance.com/api/v2/projects +``` + +## Important Notes + +- **Security Warning**: The instance admin machine identity has the highest level of privileges in your Infisical deployment. The token should be treated with the utmost security and handled like a root credential. Unauthorized access to this token could compromise your entire Infisical instance. +- Security controls prevent privilege escalation: instance admin identities cannot be managed by non-instance admin users and identities +- The instance admin permission of the generated identity can be revoked later in the server admin panel if needed +- The generated admin user account can still be used for UI access if needed, or can be removed if you prefer to manage everything through the machine identity +- This process is designed to work with future Crossplane providers and the existing Terraform provider for full infrastructure-as-code capabilities +- All necessary configuration variables should be set during the initial launch of the Infisical instance diff --git a/docs/self-hosting/guides/upgrading-infisical.mdx b/docs/self-hosting/guides/upgrading-infisical.mdx new file mode 100644 index 000000000..60c6edbff --- /dev/null +++ b/docs/self-hosting/guides/upgrading-infisical.mdx @@ -0,0 +1,57 @@ +--- + +title: "Upgrade Infisical Instance" +description: "How to upgrade Infisical self-hosted instance" + +--- + +Keeping your Infisical instance up to date is key to making sure you receive the latest performance improvements, security patches, and feature updates. +We release updates approximately once a week, which may include new features, bug fixes, performance enhancements, and critical security patches. + +Since secrets management is a critical component of your infrastructure, we aim to avoid disruptive changes that will impact fetching secrets in downstream clients. +If a release requires specific attention, a note will be attached to the corresponding [release](https://github.com/Infisical/infisical/releases) version. + +During an upgrade, two key components are updated: + +- **Infisical Application:** The core application code is updated. +- **PostgreSQL Database Schema:** Schema migrations run automatically to ensure your database remains in sync with the updated application. + +> **Before You Upgrade:** +> **Always back up your database.** While our automated migration system is robust, having a backup ensures you can recover quickly in the event of an issue. + +## Automated Schema Migrations + +In previous versions (prior to `v0.111.0-postgres`), schema migrations had to be executed manually before starting the application. +Now, migrations run automatically during boot-up. This improvement streamlines the upgrade process, reduces manual steps, and minimizes the risk of inconsistencies between your database schema and application code. + +### Benefits of Automated Migrations + +- **Seamless Integration:** + Migrations are now part of the boot-up process, removing the need for manual intervention. + +- **Synchronous Upgrades:** + In multi-instance deployments, one instance acquires a lock and performs the migration while the others wait. This ensures that if a migration fails, the rollout is halted to prevent inconsistencies. + +- **Reduced Room for Error:** + Automatic migrations help ensure that your database schema always remains in sync with your application code. + +## Upgrade Steps + +1. **Back Up Your Data:** + - Ensure you have a complete backup of your Postgres database. + - Verify that your backup is current and accessible. + +2. **Select the Upgrade Version:** + - Visit the [Infisical releases page](https://github.com/Infisical/infisical/releases) for a list of available versions. + - Look for releases with the prefix `infisical/` as there are other releases that are not related to the Infisical instance. + +3. **Start the Upgrade Process:** + - Launch the new version of Infisical. During startup, the application will automatically compare the current database schema with the updated schema in the code. + - If any differences are detected, Infisical will apply the necessary migrations automatically. + +4. **Multi-Instance Coordination:** + - In environments with multiple instances, one instance will acquire a lock and perform the migration while the other instances wait. + - Once the migration is complete, all instances will operate with the updated schema. + +5. **Verify the Upgrade:** + - Review the logs for any migration errors or warnings. diff --git a/docs/self-hosting/reference-architectures/aws-ecs.mdx b/docs/self-hosting/reference-architectures/aws-ecs.mdx index a4ce4a2b6..12deaa3cb 100644 --- a/docs/self-hosting/reference-architectures/aws-ecs.mdx +++ b/docs/self-hosting/reference-architectures/aws-ecs.mdx @@ -1,5 +1,5 @@ --- -title: "AWS ECS" +title: "AWS ECS (HA)" description: "Reference architecture for self-hosting Infisical on AWS ECS" --- @@ -48,9 +48,3 @@ This ensures that if there is a failure in one availability zone, the working re Yes, Infisical can function in an air-gapped environment. To do so, update your ECS task to use the publicly available AWS Elastic Container Registry (ECR) image instead of the default Docker Hub image. Additionally, it's necessary to configure VPC endpoints, which allows your system to access AWS ECR via a private network route instead of the internet, ensuring all connectivity remains within the secure, private network. - - Since the Amazon RDS instance is housed within a private network to enhance security, it is not directly accessible from the internet. This means that in order to run the required [Postgres schema migrations](/self-hosting/configuration/schema-migrations), you need to connect to this instance of RDS. There are many approaches you can take: - - To automate schema migrations, you may setup CI/CD pipeline with access to the same RDS network to run the schema migrations before making deployment to ECS. This ensures that if migrations fail, your Infisical instances continues to run. - - If you would like to run the migrations manually, consider using AWS Systems Manager Session Manager to access the RDS within the VPC on your local machine. - - If your organization already has mechanisms in place for secure access to the VPC, such as VPNs or Direct Connect, these can also be utilized for performing database migrations manually. - diff --git a/docs/self-hosting/reference-architectures/google-cloud-run.mdx b/docs/self-hosting/reference-architectures/google-cloud-run.mdx new file mode 100644 index 000000000..36fa1de24 --- /dev/null +++ b/docs/self-hosting/reference-architectures/google-cloud-run.mdx @@ -0,0 +1,114 @@ +--- +title: "Google Cloud Run" +description: "Reference architecture for self-hosting Infisical on Google Cloud Run." +--- + +## Overview +This guide outlines a reference architecture for deploying Infisical in a self-hosted configuration using Google Cloud Run. +It is intended to provide a scalable, secure, and production-ready baseline for organizations choosing Google Cloud Platform (GCP) as their infrastructure provider. + +## Core Components + +- **Cloud Run:** Infisical service is containerized and deployed as fully managed Cloud Run services. + +- **Cloud SQL:** Infisical uses Postgres as its persistence layer. As such, Cloud SQL for PostgreSQL is used. + +- **MemoryStore for Redis:** To schedule jobs, process audit logs and cache performance, Infisical requires Redis. + +## Securing Infisical's root credential + +- **Secrets Manager:** To secure Infisical’s root credentials (database connection string, encryption key, etc.), +we highly recommend that you use Google Secrets Manager and only allow the tasks running Infisical to access them. + +## High Availability and Scalability + +This architecture leverages Google Cloud's managed services to achieve high availability and scalability out of the box: + +**Cloud Run:** + +- Automatically scales the number of container instances up or down based on incoming request volume. +- Supports rapid scaling during traffic spikes, ensuring low latency. +- Configurable minimum and maximum instances to handle baseline and peak loads. + +**Cloud SQL:** + +- Provides high availability configurations (regional instances with automatic failover) to ensure database uptime. +- Automated backups, point-in-time recovery, and maintenance. + +**MemoryStore:** + +- Offers highly available Redis configurations with replication. +- Fully managed with automatic scaling and patching. + +**Cloud Load Balancer:** + +- Distributes user traffic across available Cloud Run instances. +- Provides SSL termination, global load balancing, and health checks. + + + **Note:** To further improve performance and availability, consider enabling multi-region deployment strategies, + regional VPC Connectors, and database replicas for read-heavy workloads. + + +## Configuration + + + + **Cloud SQL (PostgreSQL):** + - Create a Cloud SQL instance. + - Under `Zonal availability`, select the `Multiple zones` option to ensure High Availability. + - Configure private IP access. + + **MemoryStore (Redis):** + - Deploy a Redis instance. + - Configure VPC access. + + + + Visit [Docker Hub](https://hub.docker.com/r/infisical/infisical/tags) and select a version of Infisical image you would like to deploy. + Then, within Cloud Run, paste the URL of the specific Infisical Docker image you would like to use within the `Container image URL` field. + + ![Cloud Run container image settings UI](/images/self-hosting/reference-architectures/google-cloud-run/cloud-run-container-image.png) + + Remember to replace `` with the docker image tag of your choice. + + + For a minimal installation of Infisical, you must configure the following environment variables: + + ```bash + ENCRYPTION_KEY= + AUTH_SECRET= + DB_CONNECTION_URI="" + SITE_URL="" + REDIS_URL="" + ``` + [View all available configurations](/self-hosting/configuration/envars). + + You will want to setup Postgres and Redis within Google Cloud Platform to connect to Infisical. + + Once you have added the required environment variables to the `Environment Variables` section within Cloud Run, + create the container to get Infisical up and running. + + ![Cloud Run container environment variables settings UI](/images/self-hosting/reference-architectures/google-cloud-run/container-env-vars.png) + + + The above environment variable values are only to be used as an example and should not be used in production + + + + + + Enable `Connect to a VPC for outbound traffic`: This enables the service to talk to private resources (e.g., a Cloud SQL database, Redis instance on a private IP) inside your Google Cloud VPC network. + + Select `Send traffic directly to a VPC`: It gives lower latency and better performance, but uses more IPs from the subnet. + + + Your Cloud Run revision must be in the same VPC network + + + ![Cloud Run container network settings UI](/images/self-hosting/reference-architectures/google-cloud-run/container-network-configuration.png) + + Once the container is running, verify the installation by opening your web browser and navigating to the Site URL. + + + \ No newline at end of file diff --git a/docs/self-hosting/reference-architectures/linux-deployment-ha.mdx b/docs/self-hosting/reference-architectures/linux-deployment-ha.mdx new file mode 100644 index 000000000..7e4240016 --- /dev/null +++ b/docs/self-hosting/reference-architectures/linux-deployment-ha.mdx @@ -0,0 +1,383 @@ +--- +title: "Linux (HA)" +description: "Infisical High Availability Deployment architecture for Linux" +--- + +This guide describes how to achieve a highly available deployment of Infisical on Linux machines without containerization. The architecture provided serves as a foundation for minimum high availability, which you can scale based on your specific requirements. + +## Architecture Overview + +![High availability stack](/images/self-hosting/deployment-options/native/ha-stack.png) + +The deployment consists of the following key components: + +| Service | Nodes | Recommended Specs | GCP Instance | AWS Instance | +|---------------------------|-------|---------------------------|-----------------|--------------| +| External Load Balancer | 1 | 4 vCPU, 4 GB memory | n1-highcpu-4 | c5n.xlarge | +| Internal Load Balancer | 1 | 4 vCPU, 4 GB memory | n1-highcpu-4 | c5n.xlarge | +| Etcd Cluster | 3 | 4 vCPU, 4 GB memory | n1-highcpu-4 | c5n.xlarge | +| PostgreSQL Cluster | 3 | 2 vCPU, 8 GB memory | n1-standard-2 | m5.large | +| Redis + Sentinel | 3+3 | 2 vCPU, 8 GB memory | n1-standard-2 | m5.large | +| Infisical Core | 3 | 2 vCPU, 4 GB memory | n1-highcpu-2 | c5.large | + +### Network Architecture + +All servers operate within the 52.1.0.0/24 private network range with the following IP assignments: + +| Service | IP Address | +|----------------------|------------| +| External Load Balancer| 52.1.0.1 | +| Internal Load Balancer| 52.1.0.2 | +| Etcd Node 1 | 52.1.0.3 | +| Etcd Node 2 | 52.1.0.4 | +| Etcd Node 3 | 52.1.0.5 | +| PostgreSQL Node 1 | 52.1.0.6 | +| PostgreSQL Node 2 | 52.1.0.7 | +| PostgreSQL Node 3 | 52.1.0.8 | +| Redis Node 1 | 52.1.0.9 | +| Redis Node 2 | 52.1.0.10 | +| Redis Node 3 | 52.1.0.11 | +| Sentinel Node 1 | 52.1.0.12 | +| Sentinel Node 2 | 52.1.0.13 | +| Sentinel Node 3 | 52.1.0.14 | +| Infisical Core 1 | 52.1.0.15 | +| Infisical Core 2 | 52.1.0.16 | +| Infisical Core 3 | 52.1.0.17 | + +## Component Setup Guide + +### 1. Configure Etcd Cluster + +The Etcd cluster is needed for leader election in the PostgreSQL HA setup. Skip this step if using managed PostgreSQL. + +1. Install Etcd on each node: +```bash +sudo apt update +sudo apt install etcd +``` + +2. Configure each node with unique identifiers and cluster membership. Example configuration for Node 1 (`/etc/etcd/etcd.conf`): +```yaml +name: etcd1 +data-dir: /var/lib/etcd +initial-cluster-state: new +initial-cluster-token: etcd-cluster-1 +initial-cluster: etcd1=http://52.1.0.3:2380,etcd2=http://52.1.0.4:2380,etcd3=http://52.1.0.5:2380 +initial-advertise-peer-urls: http://52.1.0.3:2380 +listen-peer-urls: http://52.1.0.3:2380 +listen-client-urls: http://52.1.0.3:2379,http://127.0.0.1:2379 +advertise-client-urls: http://52.1.0.3:2379 +``` + +### 2. Configure PostgreSQL + +For production deployments, you have two options for highly available PostgreSQL: + +#### Option A: Managed PostgreSQL Service (Recommended for Most Users) + +Use cloud provider managed services: +- AWS: Amazon RDS for PostgreSQL with Multi-AZ +- GCP: Cloud SQL for PostgreSQL with HA configuration +- Azure: Azure Database for PostgreSQL with zone redundant HA + +These services handle replication, failover, and maintenance automatically. + +#### Option B: Self-Managed PostgreSQL Cluster + +Full HA installation guide of PostgreSQL is beyond the scope of this document. However, we have provided an overview of resources and code snippets below to guide your deployment. + +1. Required Components: + - PostgreSQL 14+ on each node + - Patroni for cluster management + - Etcd for distributed consensus + +2. Documentation we recommend you read: + - [Complete Patroni Setup Guide](https://patroni.readthedocs.io/en/latest/README.html) + - [PostgreSQL Replication Documentation](https://www.postgresql.org/docs/current/high-availability.html) + +3. Key Steps Overview: +```bash +# 1. Install requirements on each PostgreSQL node +sudo apt update +sudo apt install -y postgresql-14 postgresql-contrib-14 python3-pip +pip3 install patroni[etcd] psycopg2-binary + +# 2. Create Patroni config directory +sudo mkdir /etc/patroni +sudo chown postgres:postgres /etc/patroni + +# 3. Create Patroni configuration (example for first node) +# /etc/patroni/config.yml - REQUIRES CAREFUL CUSTOMIZATION +``` + +```yaml +scope: infisical-cluster +namespace: /db/ +name: postgresql1 + +restapi: + listen: 52.1.0.6:8008 + connect_address: 52.1.0.6:8008 + +etcd: + hosts: 52.1.0.3:2379,52.1.0.4:2379,52.1.0.5:2379 + +bootstrap: + dcs: + ttl: 30 + loop_wait: 10 + retry_timeout: 10 + maximum_lag_on_failover: 1048576 + postgresql: + use_pg_rewind: true + parameters: + max_connections: 1000 + shared_buffers: 2GB + work_mem: 8MB + max_worker_processes: 8 + max_parallel_workers_per_gather: 4 + max_parallel_workers: 8 + wal_level: replica + hot_standby: "on" + max_wal_senders: 10 + max_replication_slots: 10 + hot_standby_feedback: "on" +``` + +4. Important considerations: + - Proper disk configuration for WAL and data directories + - Network latency between nodes + - Backup strategy and point-in-time recovery + - Monitoring and alerting setup + - Connection pooling configuration + - Security and network access controls + +5. Recommended readings: + - [PostgreSQL Backup and Recovery](https://www.postgresql.org/docs/current/backup.html) + - [PostgreSQL Monitoring](https://www.postgresql.org/docs/current/monitoring.html) + +### 3. Configure Redis and Sentinel + +Similar to PostgreSQL, a full HA Redis setup guide is beyond the scope of this document. Below are the key resources and considerations for your deployment. + +#### Option A: Managed Redis Service (Recommended for Most Users) + +Use cloud provider managed Redis services: +- AWS: ElastiCache for Redis with Multi-AZ +- GCP: Memorystore for Redis with HA +- Azure: Azure Cache for Redis with zone redundancy + +Follow your cloud provider's documentation: +- [AWS ElastiCache Documentation](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/WhatIs.html) +- [GCP Memorystore Documentation](https://cloud.google.com/memorystore/docs/redis) +- [Azure Redis Cache Documentation](https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/) + +#### Option B: Self-Managed Redis Cluster + +Setting up a production Redis HA cluster requires understanding several components. Refer to these linked resources: + +1. Required Reading: + - [Redis Sentinel Documentation](https://redis.io/docs/management/sentinel/) + - [Redis Replication Guide](https://redis.io/topics/replication) + - [Redis Security Guide](https://redis.io/topics/security) + +2. Key Steps Overview: +```bash +# 1. Install Redis on all nodes +sudo apt update +sudo apt install redis-server + +# 2. Configure master node (52.1.0.9) +# /etc/redis/redis.conf +``` + +```conf +bind 52.1.0.9 +port 6379 +dir /var/lib/redis +maxmemory 3gb +maxmemory-policy noeviction +requirepass "your_redis_password" +masterauth "your_redis_password" +``` + +3. Configure replica nodes (`52.1.0.10`, `52.1.0.11`): +```conf +bind 52.1.0.10 # Change for each replica +port 6379 +dir /var/lib/redis +replicaof 52.1.0.9 6379 +masterauth "your_redis_password" +requirepass "your_redis_password" +``` + +4. Configure Sentinel nodes (`52.1.0.12`, `52.1.0.13`, `52.1.0.14`): +```conf +port 26379 +sentinel monitor mymaster 52.1.0.9 6379 2 +sentinel auth-pass mymaster "your_redis_password" +sentinel down-after-milliseconds mymaster 5000 +sentinel failover-timeout mymaster 60000 +sentinel parallel-syncs mymaster 1 +``` + +5. Recommended Additional Reading: + - [Redis High Availability Tools](https://redis.io/topics/high-availability) + - [Redis Sentinel Client Implementation](https://redis.io/topics/sentinel-clients) + +### 4. Configure HAProxy Load Balancer + +Install and configure HAProxy for internal load balancing: + +```conf ha-proxy-config +global + maxconn 10000 + log stdout format raw local0 + +defaults + log global + mode tcp + retries 3 + timeout client 30m + timeout connect 10s + timeout server 30m + timeout check 5s + +listen stats + mode http + bind *:7000 + stats enable + stats uri / + +resolvers hostdns + nameserver dns 127.0.0.11:53 + resolve_retries 3 + timeout resolve 1s + timeout retry 1s + hold valid 5s + +frontend postgres_master + bind *:5000 + default_backend postgres_master_backend + +frontend postgres_replicas + bind *:5001 + default_backend postgres_replica_backend + +backend postgres_master_backend + option httpchk GET /master + http-check expect status 200 + default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions + server postgres-1 52.1.0.6:5432 check port 8008 + server postgres-2 52.1.0.7:5432 check port 8008 + server postgres-3 52.1.0.8:5432 check port 8008 + +backend postgres_replica_backend + option httpchk GET /replica + http-check expect status 200 + default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions + server postgres-1 52.1.0.6:5432 check port 8008 + server postgres-2 52.1.0.7:5432 check port 8008 + server postgres-3 52.1.0.8:5432 check port 8008 + +frontend redis_master_frontend + bind *:6379 + default_backend redis_master_backend + +backend redis_master_backend + option tcp-check + tcp-check send AUTH\ 123456\r\n + tcp-check expect string +OK + tcp-check send PING\r\n + tcp-check expect string +PONG + tcp-check send info\ replication\r\n + tcp-check expect string role:master + tcp-check send QUIT\r\n + tcp-check expect string +OK + server redis-1 52.1.0.9:6379 check inter 1s + server redis-2 52.1.0.10:6379 check inter 1s + server redis-3 52.1.0.11:6379 check inter 1s + +frontend infisical_frontend + bind *:80 + default_backend infisical_backend + +backend infisical_backend + option httpchk GET /api/status + http-check expect status 200 + server infisical-1 52.1.0.15:8080 check inter 1s + server infisical-2 52.1.0.16:8080 check inter 1s + server infisical-3 52.1.0.17:8080 check inter 1s +``` + +### 5. Deploy Infisical Core + + + First, add the Infisical repository: + ```bash + curl -1sLf \ + 'https://dl.cloudsmith.io/public/infisical/infisical-core/setup.deb.sh' \ + | sudo -E bash + ``` + + Then install Infisical: + ```bash + sudo apt-get update && sudo apt-get install -y infisical-core + ``` + + + For production environments, we strongly recommend installing a specific version of the package to maintain consistency across reinstalls. View available versions at [Infisical Package Versions](https://cloudsmith.io/~infisical/repos/infisical-core/packages/). + + + + + First, add the Infisical repository: + ```bash + curl -1sLf \ + 'https://dl.cloudsmith.io/public/infisical/infisical-core/setup.rpm.sh' \ + | sudo -E bash + ``` + + Then install Infisical: + ```bash + sudo yum install infisical-core + ``` + + + For production environments, we strongly recommend installing a specific version of the package to maintain consistency across reinstalls. View available versions at [Infisical Package Versions](https://cloudsmith.io/~infisical/repos/infisical-core/packages/). + + + + + +Next, create configuration file `/etc/infisical/infisical.rb` with the following: + +```ruby +infisical_core['ENCRYPTION_KEY'] = 'your-secure-encryption-key' +infisical_core['AUTH_SECRET'] = 'your-secure-auth-secret' + +infisical_core['DB_CONNECTION_URI'] = 'postgres://user:pass@52.1.0.2:5000/infisical' +infisical_core['REDIS_URL'] = 'redis://52.1.0.2:6379' + +infisical_core['PORT'] = 8080 +``` + +To generate `ENCRYPTION_KEY` and `AUTH_SECRET` view the [following configurations documentation here](/self-hosting/configuration/envars). + +If you are using managed services for either Postgres or Redis, please replace the values of the secrets accordingly. + + +Lastly, start and verify each node running infisical-core: +```bash +sudo infisical-ctl reconfigure +sudo infisical-ctl status +``` + +## Monitoring and Maintenance + +1. Monitor HAProxy stats: `http://52.1.0.2:7000/haproxy?stats` +2. Monitor Infisical logs: `sudo infisical-ctl tail` +3. Check cluster health: + - Etcd: `etcdctl cluster-health` + - PostgreSQL: `patronictl list` + - Redis: `redis-cli info replication` diff --git a/docs/self-hosting/reference-architectures/on-prem-k8s-ha.mdx b/docs/self-hosting/reference-architectures/on-prem-k8s-ha.mdx new file mode 100644 index 000000000..6dc8ce574 --- /dev/null +++ b/docs/self-hosting/reference-architectures/on-prem-k8s-ha.mdx @@ -0,0 +1,231 @@ +--- +title: "Kubernetes (HA)" +description: "Reference architecture for self-hosting Infisical on Kubernetes (HA)" +--- +Deploying Infisical on-premise with high availability requires expertise in networking, container orchestration, and database management. +This guide serves as a reference architecture and a starting point. Actual deployments may vary depending on your organization's existing infrastructure and capabilities. + + +## Architecture Overview +{/* ![On premise architecture](/images/self-hosting/reference-architectures/on-premise-architecture.png) */} +```mermaid +flowchart TB + subgraph GLB["Global LB (HAProxy/NGINX)"] + end + + subgraph OS["Object Storage"] + direction LR + store["S3/MinIO/Enterprise Storage"] + subgraph store_contents["Storage Contents"] + wal["PostgreSQL WAL"] + pgbackup["PostgreSQL Backups"] + redisbackup["Redis Backups"] + end + end + + subgraph DC1["Active Data Center"] + direction TB + subgraph k8s1["Kubernetes Cluster"] + ing1["Ingress Controller"] + app1["Infisical Deployment"] + + subgraph db1["CloudNativePG"] + pg1p["PostgreSQL Primary"] + pg1r["PostgreSQL Replicas"] + end + + subgraph red1["Redis (Bitnami)"] + rp1["Redis Primary"] + end + end + end + + subgraph DC2["Passive Data Center"] + direction TB + subgraph k8s2["Kubernetes Cluster"] + ing2["Ingress Controller"] + app2["Infisical Deployment"] + + subgraph db2["CloudNativePG"] + pg2["PostgreSQL Replicas"] + end + + subgraph red2["Redis (Bitnami)"] + r2["Redis Standby"] + end + end + end + + %% Connections + GLB --> ing1 + GLB -.-> ing2 + + %% Database connections + pg1p --> store + store --> pg2 + + %% Redis backup flow + rp1 --> store + store -.-> r2 + + %% Intra-DC connections + ing1 --> app1 + app1 --> db1 + app1 --> red1 + + ing2 --> app2 + app2 --> db2 + app2 --> red2 + + classDef primary fill:#f96,stroke:#333 + classDef replica fill:#69f,stroke:#333 + classDef storage fill:#9c6,stroke:#333 + classDef lb fill:#c9f,stroke:#333 + + class pg1p,rp1 primary + class pg1r,pg2,r2 replica + class store,wal,pgbackup,redisbackup storage + class GLB,ing1,ing2 lb +``` +The architecture above makes use of Kubernetes for orchestrating both stateless and stateful components. +The architecture spans multiple data centers for increased redundancy, availability and disaster recovery capabilities using an active-passive configuration. + +### Stateful vs stateless workloads +While managing databases within Kubernetes has typically been complex, modern operators like [CloudNativePG](https://cloudnative-pg.io/) simplify this process by handling storage provisioning, persistent volume management, and backup/recovery processes. +However, if you lack deep expertise in Kubernetes operators or database management, we recommend a hybrid approach where the database is on a managed service for production deployments. + + + Managing stateful components like databases can be challenging without deep expertise or a dedicated in-house database management team. + To simplify operations and reduce complexity, we recommend offloading databases to managed services from AWS/GCP. + These managed services automatically handle provisioning, scaling, failover, backups and rollbacks. + + +## Core Components +### Kubernetes Cluster +Infisical is deployed on a Kubernetes cluster, which allows for container management, auto-scaling, and self-healing capabilities. +A load balancer sits in front of the Kubernetes cluster, directing traffic and making sure there is an even load distribution across the application nodes. +This is the entry point where all other services will interact with Infisical. + +### Object Storage +The architecture requires S3-compatible object storage for database backups and cross-datacenter replication. This can be provided by: +- Existing enterprise object storage solution +- Dedicated MinIO deployment +- In-cluster MinIO deployment if neither option above is available + +The object storage must be accessible from all Kubernetes clusters and provides: +- Storage for PostgreSQL WAL archiving and backups +- Storage for Redis backups + +### CloudNativePG for High Availability PostgreSQL +The database layer is powered by PostgreSQL, managed by CloudNativePG operator for high availability: +- **Redundancy:** CloudNativePG manages a primary-replica setup where the primary handles write operations and replicas handle read operations +- **Failover:** The operator automatically handles failover within a cluster by promoting a replica to primary when needed +- **Backup and Recovery:** Built-in support for backup to S3-compatible storage with point-in-time recovery capabilities + +### Redis High Availability +Redis is deployed using the [Bitnami Helm chart](https://github.com/bitnami/charts/tree/main/bitnami/redis) in a simple primary configuration: +- Single Redis instance per cluster without streaming replication +- Regular backups to object storage +- Restore from backup during failover + + +Infisical does not support Redis cluster mode, and since this is an active-passive setup, we use a simple Redis deployment with backup/restore for failover. + + +#### PostgreSQL Backup and Restore +PostgreSQL is the single source of truth for nearly all application data on Infisical. + +CloudNativePG provides well defined backup and restore capabilities: +- **Continuous Backup:** The operator continuously archives WAL files to object storage +- **Point-in-Time Recovery:** Supports restoring to any point in time using WAL archiving +- **Regular Testing:** Periodically test backup restoration to exercise the full lifecycle of this process + +#### Redis Backup and Restore +Each Redis instance is backed up through a Kubernetes CronJob that: +1. Executes the Redis `SAVE` command +2. Copies the resulting `dump.rdb` to object storage +3. Manages backup retention + + + ```yaml + apiVersion: batch/v1 + kind: CronJob + metadata: + name: redis-backup + spec: + schedule: "0 * * * *" # Every hour + jobTemplate: + spec: + template: + spec: + containers: + - name: redis-backup + image: bitnami/redis + command: + - /bin/sh + - -c + - | + redis-cli -a $REDIS_PASSWORD save + mc cp /data/dump.rdb object-store/redis-backups/ + volumes: + - name: redis-data + persistentVolumeClaim: + claimName: redis-data + ``` + + +During failover, the latest Redis backup is restored from object storage to the passive data center. This process is manual and requires operator intervention. + +## Multi Data Center Deployment +Infisical can be deployed across multiple data centers in an active-passive configuration for disaster recovery. In this setup, one data center serves as the active site while others remain as passive standbys. + +### Active Data Center +The active data center contains: +- The primary PostgreSQL cluster managed by CloudNativePG handling all write operations +- The active Redis instance handling all traffic +- The active Infisical deployment serving all user traffic + +### Passive Data Centers +Passive data centers act as disaster recovery sites. Each contains: +- A replica PostgreSQL cluster that replicates from the active site's primary cluster +- A standby Redis instance (not receiving traffic) +- A standby Infisical deployment (not receiving traffic) + +### Traffic Management and Failover +Traffic routing between data centers requires: +1. A global load balancer for traffic management. For on-premises deployments, this can be implemented using: + - HAProxy or NGINX configured as a global load balancer + - Any enterprise network routing solutions you may already have in place +2. Each data center should have its own ingress or load balancer + +The global load balancer should be deployed in a highly available configuration across multiple locations to avoid it becoming a single point of failure. + +During normal operation: +- The global load balancer routes all traffic to the active data center +- Replica PostgreSQL clusters continuously replicate from the primary cluster +- Redis backups are regularly created and stored in object storage + +During failover: +- A human operator must initiate the failover process +- The operator promotes a replica PostgreSQL cluster in the target passive data center to become primary using CloudNativePG's promotion process +- The latest Redis backup is restored from object storage to the passive data center's Redis instance +- Once database failover is complete, the global load balancer is updated to direct traffic to the new active data center + + +This is an active-passive setup where failover must be initiated manually by an operator. Automatic failover between data centers is not recommended as it can lead to split-brain scenarios. The operator should verify the state of both data centers before initiating failover. + + +## Data Replication Across Data Centers + +### PostgreSQL Replication +CloudNativePG manages replication across data centers: +- **Replica Clusters:** Each data center runs a replica cluster that replicates from the primary cluster +- **WAL Shipping:** Changes are replicated via WAL shipping to object storage +- **Failover:** The operator can promote a replica cluster to primary during planned switchovers or failures + +### Object Storage Configuration +If using MinIO for object storage, ensure: +- High availability deployment if running dedicated MinIO cluster +- Proper access controls and encryption for data at rest +- Regular monitoring of storage capacity and performance +- Backup of object storage data itself if running your own MinIO deployment \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..3104f340d --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1741445498, + "narHash": "sha256-F5Em0iv/CxkN5mZ9hRn3vPknpoWdcdCyR0e4WklHwiE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "52e3095f6d812b91b22fb7ad0bfc1ab416453634", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..094cfedc6 --- /dev/null +++ b/flake.nix @@ -0,0 +1,24 @@ +{ + description = "Flake for github:Infisical/infisical repository."; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; + }; + + outputs = { self, nixpkgs }: { + devShells.aarch64-darwin.default = let + pkgs = nixpkgs.legacyPackages.aarch64-darwin; + in + pkgs.mkShell { + packages = with pkgs; [ + git + lazygit + + python312Full + nodejs_20 + nodePackages.prettier + infisical + ]; + }; + }; +} diff --git a/frontend/.dockerignore b/frontend/.dockerignore index a3fe26e2f..f06235c46 100644 --- a/frontend/.dockerignore +++ b/frontend/.dockerignore @@ -1,3 +1,2 @@ node_modules -**/.next -.next \ No newline at end of file +dist diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js deleted file mode 100644 index 13e8e5ab7..000000000 --- a/frontend/.eslintrc.js +++ /dev/null @@ -1,108 +0,0 @@ -module.exports = { - overrides: [ - { - files: ["next.config.js"] - } - ], - root: true, - env: { - browser: true, - es2021: true, - es6: true - }, - extends: [ - "airbnb", - "airbnb-typescript", - "airbnb/hooks", - "plugin:react/recommended", - "prettier", - "plugin:storybook/recommended" - ], - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - project: "./tsconfig.json", - ecmaFeatures: { - jsx: true - }, - tsconfigRootDir: __dirname - }, - plugins: ["react", "prettier", "simple-import-sort", "import"], - rules: { - "@typescript-eslint/no-empty-function": "off", - quotes: ["error", "double", { avoidEscape: true }], - "comma-dangle": ["error", "only-multiline"], - "react/react-in-jsx-scope": "off", - "import/prefer-default-export": "off", - "react-hooks/exhaustive-deps": "off", - "@typescript-eslint/ban-ts-comment": "warn", - "react/jsx-props-no-spreading": "off", // switched off for component building - // TODO: This rule will be switched ON after complete revamp of frontend - "@typescript-eslint/no-explicit-any": "off", - "jsx-a11y/control-has-associated-label": "off", - "no-console": "off", - "arrow-body-style": "off", - "no-underscore-dangle": [ - "error", - { - allow: ["_id"] - } - ], - "jsx-a11y/anchor-is-valid": "off", - // all those tags must be converted to label or a p component - // - "react/require-default-props": "off", - "react/jsx-filename-extension": [ - 1, - { - extensions: [".tsx", ".ts"] - } - ], - // TODO: turn this rule ON after migration. everything should use arrow functions - "react/function-component-definition": [ - 0, - { - namedComponents: "arrow-function" - } - ], - "react/no-unknown-property": [ - "error", - { - ignore: ["jsx"] - } - ], - "@typescript-eslint/no-non-null-assertion": "off", - "simple-import-sort/exports": "warn", - "simple-import-sort/imports": [ - "warn", - { - groups: [ - // Node.js builtins. You could also generate this regex if you use a `.js` config. - // For example: `^(${require("module").builtinModules.join("|")})(/|$)` - // Note that if you use the `node:` prefix for Node.js builtins, - // you can avoid this complexity: You can simply use "^node:". - [ - "^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)" - ], - // Packages `react` related packages - ["^react", "^next", "^@?\\w"], - ["^@app"], - // Internal packages. - ["^~(/.*|$)"], - // Relative imports - ["^\\.\\.(?!/?$)", "^\\.\\./?$", "^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"], - // Style imports. - ["^.+\\.?(css|scss)$"] - ] - } - ] - }, - ignorePatterns: ["next.config.js", "cypress/**/*.js", "cypress.config.js"], - settings: { - "import/resolver": { - typescript: { - project: ["./tsconfig.json"] - } - } - } -}; diff --git a/frontend/.gitignore b/frontend/.gitignore index 5edd5a7fa..a547bf36d 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1,36 +1,24 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -.env - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store - -# debug +# Logs +logs +*.log npm-debug.log* yarn-debug.log* yarn-error.log* +pnpm-debug.log* +lerna-debug.log* -# local env files -.env.local -.env.development.local -.env.test.local -.env.production.local -.vercel -.env.infisical +node_modules +dist +dist-ssr +*.local -.vscode \ No newline at end of file +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/.prettierrc b/frontend/.prettierrc index 0b8ef54d2..6a877df47 100644 --- a/frontend/.prettierrc +++ b/frontend/.prettierrc @@ -3,5 +3,8 @@ "printWidth": 100, "trailingComma": "none", "tabWidth": 2, - "semi": true + "semi": true, + "plugins": ["prettier-plugin-tailwindcss"], + "tailwindStylesheet": "./src/index.css", + "tailwindFunctions": ["clsx", "twMerge"] } diff --git a/frontend/.storybook/main.js b/frontend/.storybook/main.js deleted file mode 100644 index 1a68699d2..000000000 --- a/frontend/.storybook/main.js +++ /dev/null @@ -1,28 +0,0 @@ -const path = require("path"); -module.exports = { - stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], - addons: [ - "@storybook/addon-links", - "@storybook/addon-essentials", - "@storybook/addon-interactions", - "storybook-dark-mode", - { - name: "@storybook/addon-styling", - options: { - postCss: { - implementation: require("postcss") - } - } - } - ], - framework: { - name: "@storybook/nextjs", - options: {} - }, - core: { - disableTelemetry: true - }, - docs: { - autodocs: "tag" - } -}; diff --git a/frontend/.storybook/preview.js b/frontend/.storybook/preview.js deleted file mode 100644 index 2a3a61d5a..000000000 --- a/frontend/.storybook/preview.js +++ /dev/null @@ -1,29 +0,0 @@ -import { themes } from "@storybook/theming"; -import "react-day-picker/dist/style.css"; -import "../src/styles/globals.css"; - -export const parameters = { - actions: { argTypesRegex: "^on[A-Z].*" }, - backgrounds: { - default: "dark", - values: [ - { - name: "dark", - value: "rgb(14, 16, 20)" - }, - { - name: "paper", - value: "rgb(30, 31, 34)" - } - ] - }, - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/ - } - }, - darkMode: { - dark: { ...themes.dark, appContentBg: "rgb(14,16,20)", appBg: "rgb(14,16,20)" } - } -}; diff --git a/frontend/Dockerfile b/frontend/Dockerfile deleted file mode 100644 index 5251dc406..000000000 --- a/frontend/Dockerfile +++ /dev/null @@ -1,84 +0,0 @@ -ARG POSTHOG_HOST=https://app.posthog.com -ARG POSTHOG_API_KEY=posthog-api-key -ARG INTERCOM_ID=intercom-id -ARG NEXT_INFISICAL_PLATFORM_VERSION=next-infisical-platform-version -ARG CAPTCHA_SITE_KEY=captcha-site-key - -FROM node:16-alpine AS deps -# Install dependencies only when needed. Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. -# RUN apk add --no-cache libc6-compat -WORKDIR /app - -# Copy over dependency files -COPY package.json package-lock.json next.config.js ./ - -# Install dependencies -RUN npm ci --only-production --ignore-scripts - -# Rebuild the source code only when needed -FROM node:16-alpine AS builder -WORKDIR /app - -# Copy dependencies -COPY --from=deps /app/node_modules ./node_modules -# Copy all files -COPY . . - -ENV NODE_ENV production -ENV NEXT_PUBLIC_ENV production -ARG POSTHOG_HOST -ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST -ARG POSTHOG_API_KEY -ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY -ARG INTERCOM_ID -ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID -ARG CAPTCHA_SITE_KEY -ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY - -# Build -RUN npm run build - - -# Production image -FROM node:16-alpine AS runner -WORKDIR /app - -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs - -RUN mkdir -p /app/.next/cache/images && chown nextjs:nodejs /app/.next/cache/images -VOLUME /app/.next/cache/images - -ARG POSTHOG_API_KEY -ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \ - BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY -ARG INTERCOM_ID -ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \ - BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID -ARG SAML_ORG_SLUG -ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG \ - BAKED_NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG -ARG NEXT_INFISICAL_PLATFORM_VERSION -ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION=$NEXT_INFISICAL_PLATFORM_VERSION -ARG CAPTCHA_SITE_KEY -ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \ - BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY -COPY --chown=nextjs:nodejs --chmod=555 scripts ./scripts -COPY --from=builder /app/public ./public -RUN chown nextjs:nodejs ./public/data -COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs --chmod=777 /app/.next/static ./.next/static -RUN chmod -R 777 /app/.next/server - -USER nextjs - -EXPOSE 3000 - -ENV PORT 3000 -ENV NEXT_TELEMETRY_DISABLED 1 - -HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \ - CMD node scripts/healthcheck.js - - -CMD ["/app/scripts/start.sh"] diff --git a/frontend/Dockerfile.dev b/frontend/Dockerfile.dev index cb462bbc4..555fb52de 100644 --- a/frontend/Dockerfile.dev +++ b/frontend/Dockerfile.dev @@ -1,5 +1,5 @@ # Base layer -FROM node:16-alpine +FROM node:20-alpine # Set the working directory WORKDIR /app @@ -11,9 +11,6 @@ COPY package-lock.json ./ # Install RUN npm install --ignore-scripts -# Copy over next.js config -COPY next.config.js ./next.config.js - # Copy all files COPY . . diff --git a/frontend/README.md b/frontend/README.md index a0bcd1ef0..74872fd4a 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,22 +1,50 @@ -This is the client repository for Infisical. +# React + TypeScript + Vite -## Before you get started with development locally +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. -Please ensure you have Docker and Docker Compose installed for your OS. +Currently, two official plugins are available: -### Steps to start server +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh -- `CD` into the repo -- run command `docker-compose -f docker-compose.dev.yml up --build --force-recreate` -- Visit localhost:8080 and the website should be live +## Expanding the ESLint configuration -### Steps to shutdown this Docker compose +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: -- `CD` into this repo -- run command `docker-compose -f docker-compose.dev.yml down` +- Configure the top-level `parserOptions` property like this: -### Notes +```js +export default tseslint.config({ + languageOptions: { + // other options... + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + }, +}) +``` -Any changes made to local files in the `/components`, `/pages`, `/styles` will be hot reloaded. If would like like to watch for other files or folders live, please add them to the docker volume. +- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` +- Optionally add `...tseslint.configs.stylisticTypeChecked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: -You will also need to ensure that a .env.local file exists with all required environment variables +```js +// eslint.config.js +import react from 'eslint-plugin-react' + +export default tseslint.config({ + // Set the react version + settings: { react: { version: '18.3' } }, + plugins: { + // Add the react plugin + react, + }, + rules: { + // other rules... + // Enable its recommended rules + ...react.configs.recommended.rules, + ...react.configs['jsx-runtime'].rules, + }, +}) +``` diff --git a/frontend/cypress.config.js b/frontend/cypress.config.js deleted file mode 100644 index f50a1c607..000000000 --- a/frontend/cypress.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - e2e: { - baseUrl: 'http://localhost:8080', - viewportWidth: 1480, - viewportHeight: 920, - }, -}; diff --git a/frontend/cypress/e2e/org-overview.cy.js b/frontend/cypress/e2e/org-overview.cy.js deleted file mode 100644 index a18fe208e..000000000 --- a/frontend/cypress/e2e/org-overview.cy.js +++ /dev/null @@ -1,47 +0,0 @@ -/// - -describe('organization Overview', () => { - beforeEach(() => { - cy.login(`test@localhost.local`, `testInfisical1`) - }) - - const projectName = "projectY" - - it('can`t create projects with empty names', () => { - cy.get('.button').click() - cy.get('input[placeholder="Type your project name"]').type('abc').clear() - cy.intercept('*').as('anyRequest'); - cy.get('@anyRequest').should('not.exist'); - }) - - it('can delete a newly-created project', () => { - // Create a project - cy.get('.button').click() - cy.get('input[placeholder="Type your project name"]').type(`${projectName}`) - cy.contains('button', 'Create Project').click() - cy.url().should('include', '/project') - - // Delete a project - cy.get(`[href^="/project/"][href$="/settings"] > a > .group`).click() - cy.contains('button', `Delete ${projectName}`).click() - cy.contains('button', 'Delete Project').should('have.attr', 'disabled') - cy.get('input[placeholder="Type to delete..."]').type('confirm') - cy.contains('button', 'Delete Project').should('not.have.attr', 'disabled') - cy.url().then((currentUrl) => { - let projectId = currentUrl.split("/")[4] - cy.intercept('DELETE', `/api/v1/workspace/${projectId}`).as('deleteProject'); - cy.contains('button', 'Delete Project').click(); - cy.get('@deleteProject').should('have.property', 'response').and('have.property', 'statusCode', 200); - }) - }) - - it('can display no projects', () => { - cy.intercept('/api/v1/workspace', { - body: { - "workspaces": [] - }, - }) - cy.get('.border-mineshaft-700 > :nth-child(2)').should('have.text', 'You are not part of any projects in this organization yet. When you are, they will appear here.') - }) - -}) diff --git a/frontend/cypress/e2e/org-settings.cy.js b/frontend/cypress/e2e/org-settings.cy.js deleted file mode 100644 index b9aff2516..000000000 --- a/frontend/cypress/e2e/org-settings.cy.js +++ /dev/null @@ -1,24 +0,0 @@ -/// - -describe('Organization Settings', () => { - let orgId; - - beforeEach(() => { - cy.login(`test@localhost.local`, `testInfisical1`) - cy.url().then((currentUrl) => { - orgId = currentUrl.split("/")[4] - cy.visit(`org/${orgId}/settings`) - }) - }) - - it('can rename org', () => { - cy.get('input[placeholder="Acme Corp"]').clear().type('ABC') - - cy.intercept('PATCH', `/api/v1/organization/${orgId}/name`).as('renameOrg'); - cy.get('form.p-4 > .button').click() - cy.get('@renameOrg').should('have.property', 'response').and('have.property', 'statusCode', 200); - - cy.get('.pl-3').should("have.text", "ABC ") - }) - -}) diff --git a/frontend/cypress/e2e/project-secret-operations.cy.js b/frontend/cypress/e2e/project-secret-operations.cy.js deleted file mode 100644 index b4c3a7787..000000000 --- a/frontend/cypress/e2e/project-secret-operations.cy.js +++ /dev/null @@ -1,84 +0,0 @@ -/// - -describe('Project Overview', () => { - const projectName = "projectY" - let projectId; - let isFirstTest = true; - - before(() => { - cy.login(`test@localhost.local`, `testInfisical1`) - - // Create a project - cy.get('.button').click() - cy.get('input[placeholder="Type your project name"]').type(`${projectName}`) - cy.contains('button', 'Create Project').click() - cy.url().should('include', '/project').then((currentUrl) => { - projectId = currentUrl.split("/")[4] - }) - }) - - beforeEach(() => { - if (isFirstTest) { - isFirstTest = false; - return; // Skip the rest of the beforeEach for the first test - } - cy.login(`test@localhost.local`, `testInfisical1`) - cy.visit(`/project/${projectId}/secrets/overview`) - }) - - it('can create secrets', () => { - cy.contains('button', 'Go to Development').click() - cy.contains('button', 'Add a new secret').click() - cy.get('input[placeholder="Type your secret name"]').type('SECRET_A') - cy.contains('button', 'Create Secret').click() - cy.get('.w-80 > .inline-flex > .input').should('have.value', 'SECRET_A') - cy.get(':nth-child(6) > .button > .w-min').should('have.text', '1 Commit') - }) - - it('can update secrets', () => { - cy.get(':nth-child(2) > .flex > .button').click() - cy.get('.overflow-auto > .relative > .absolute').type('VALUE_A') - cy.get('.button.text-primary > .svg-inline--fa').click() - cy.get(':nth-child(6) > .button > .w-min').should('have.text', '2 Commits') - }) - - it('can`t create duplicate-name secrets', () => { - cy.get(':nth-child(2) > .flex > .button').click() - cy.contains('button', 'Add Secret').click() - cy.get('input[placeholder="Type your secret name"]').type('SECRET_A') - cy.intercept('POST', `/api/v3/secrets/SECRET_A`).as('createSecret'); - cy.contains('button', 'Create Secret').click() - cy.get('@createSecret').should('have.property', 'response').and('have.property', 'statusCode', 400); - }) - - it('can add another secret', () => { - cy.get(':nth-child(2) > .flex > .button').click() - cy.contains('button', 'Add Secret').click() - cy.get('input[placeholder="Type your secret name"]').type('SECRET_B') - cy.contains('button', 'Create Secret').click() - cy.get(':nth-child(6) > .button > .w-min').should('have.text', '3 Commits') - }) - - it('can delete a secret', () => { - cy.get(':nth-child(2) > .flex > .button').click() - // cy.get(':nth-child(3) > .shadow-none').trigger('mouseover') - cy.get(':nth-child(3) > .shadow-none > .group > .h-10 > .border-red').click() - cy.contains('button', 'Delete Secret').should('have.attr', 'disabled') - cy.get('input[placeholder="Type to delete..."]').type('SECRET_B') - cy.intercept('DELETE', `/api/v3/secrets/SECRET_B`).as('deleteSecret'); - cy.contains('button', 'Delete Secret').should('not.have.attr', 'disabled') - cy.contains('button', 'Delete Secret').click(); - cy.get('@deleteSecret').should('have.property', 'response').and('have.property', 'statusCode', 200); - }) - - it('can add a comment', () => { - return; - cy.get(':nth-child(2) > .flex > .button').click() - // for some reason this hover does not want to work - cy.get('.overflow-auto').trigger('mouseover').then(() => { - cy.get('.shadow-none > .group > .pl-4 > .h-8 > button[aria-label="add-comment"]').should('be.visible').click() - }); - - }) - -}) diff --git a/frontend/cypress/fixtures/example.json b/frontend/cypress/fixtures/example.json deleted file mode 100644 index 02e425437..000000000 --- a/frontend/cypress/fixtures/example.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Using fixtures to represent data", - "email": "hello@cypress.io", - "body": "Fixtures are a great way to mock data for responses to routes" -} diff --git a/frontend/cypress/support/commands.js b/frontend/cypress/support/commands.js deleted file mode 100644 index 6778e394c..000000000 --- a/frontend/cypress/support/commands.js +++ /dev/null @@ -1,19 +0,0 @@ - -Cypress.Commands.add('login', (username, password) => { - cy.visit('/login') - cy.get('input[placeholder="Enter your email..."]').type(username) - cy.get('input[placeholder="Enter your password..."]').type(password) - cy.contains('Continue with Email').click() - cy.url().should('include', '/overview') -}) - -// Cypress.Commands.add('login', (username, password) => { -// cy.session([username, password], () => { -// cy.visit('/login') -// cy.get('input[placeholder="Enter your email..."]').type(username) -// cy.get('input[placeholder="Enter your password..."]').type(password) -// cy.contains('Continue with Email').click() -// cy.url().should('include', '/overview') -// cy.wait(2000); -// }) -// }) diff --git a/frontend/cypress/support/e2e.js b/frontend/cypress/support/e2e.js deleted file mode 100644 index 0e7290a13..000000000 --- a/frontend/cypress/support/e2e.js +++ /dev/null @@ -1,20 +0,0 @@ -// *********************************************************** -// This example support/e2e.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// Import commands.js using ES2015 syntax: -import './commands' - -// Alternatively you can use CommonJS syntax: -// require('./commands') \ No newline at end of file diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 000000000..00c024056 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,137 @@ +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import eslintPluginPrettier from "eslint-plugin-prettier/recommended"; +import simpleImportSort from "eslint-plugin-simple-import-sort"; +import tseslint from "typescript-eslint"; +import { FlatCompat } from "@eslint/eslintrc"; +import stylisticPlugin from "@stylistic/eslint-plugin"; +import importPlugin from "eslint-plugin-import"; +import pluginRouter from "@tanstack/eslint-plugin-router"; + +const compat = new FlatCompat({ + baseDirectory: import.meta.dirname +}); + +export default tseslint.config( + { ignores: ["dist"] }, + { + extends: [ + ...pluginRouter.configs["flat/recommended"], + js.configs.recommended, + tseslint.configs.recommended, + ...compat.extends("airbnb"), + ...compat.extends("@kesills/airbnb-typescript"), + eslintPluginPrettier + ], + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname + } + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + "simple-import-sort": simpleImportSort, + import: importPlugin + }, + settings: { + "import/resolver": { + typescript: { + project: ["./tsconfig.json"] + } + } + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": "off", + "@typescript-eslint/only-throw-error": "off", + "@typescript-eslint/no-empty-function": "off", + quotes: ["error", "double", { avoidEscape: true }], + "comma-dangle": ["error", "only-multiline"], + "react/react-in-jsx-scope": "off", + "import/prefer-default-export": "off", + "react-hooks/exhaustive-deps": "off", + "@typescript-eslint/ban-ts-comment": "warn", + "react/jsx-props-no-spreading": "off", // switched off for component building + // TODO: This rule will be switched ON after complete revamp of frontend + "@typescript-eslint/no-explicit-any": "off", + "jsx-a11y/control-has-associated-label": "off", + "import/no-extraneous-dependencies": [ + "error", + { + devDependencies: true + } + ], + "no-console": "off", + "arrow-body-style": "off", + "no-underscore-dangle": [ + "error", + { + allow: ["_id"] + } + ], + "jsx-a11y/anchor-is-valid": "off", + // all those tags must be converted to label or a p component + // + "react/require-default-props": "off", + "react/jsx-filename-extension": [ + 1, + { + extensions: [".tsx", ".ts"] + } + ], + // TODO: turn this rule ON after migration. everything should use arrow functions + "react/function-component-definition": [ + 0, + { + namedComponents: "arrow-function" + } + ], + "react/no-unknown-property": [ + "error", + { + ignore: ["jsx"] + } + ], + "@typescript-eslint/no-non-null-assertion": "off", + "simple-import-sort/exports": "warn", + "simple-import-sort/imports": [ + "warn", + { + groups: [ + // Node.js builtins. You could also generate this regex if you use a `.js` config. + // For example: `^(${require("module").builtinModules.join("|")})(/|$)` + // Note that if you use the `node:` prefix for Node.js builtins, + // you can avoid this complexity: You can simply use "^node:". + [ + "^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)" + ], + // Packages `react` related packages + ["^react", "^next", "^@?\\w"], + ["^@app"], + // Internal packages. + ["^~(/.*|$)"], + // Relative imports + ["^\\.\\.(?!/?$)", "^\\.\\./?$", "^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"], + // Style imports. + ["^.+\\.?(css|scss)$"] + ] + } + ], + "import/first": "error", + "import/newline-after-import": "error", + "import/no-duplicates": "error" + } + }, + { + rules: Object.fromEntries( + Object.keys(stylisticPlugin.configs["all-flat"].rules ?? {}).map((key) => [key, "off"]) + ) + } +); diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 000000000..e3a051915 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,29 @@ + + + + + + + + Infisical + + + +

+ + + diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts deleted file mode 100644 index 4f11a03dc..000000000 --- a/frontend/next-env.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -/// -/// - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/frontend/next.config.js b/frontend/next.config.js deleted file mode 100644 index e07695ed6..000000000 --- a/frontend/next.config.js +++ /dev/null @@ -1,98 +0,0 @@ -const path = require("path"); - -const ContentSecurityPolicy = ` - default-src 'self'; - connect-src 'self' https://*.posthog.com; - script-src 'self' https://*.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com https://hcaptcha.com https://*.hcaptcha.com 'unsafe-inline' 'unsafe-eval'; - style-src 'self' https://rsms.me 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com; - child-src https://api.stripe.com; - frame-src https://js.stripe.com/ https://api.stripe.com https://www.youtube.com/ https://hcaptcha.com https://*.hcaptcha.com; - connect-src 'self' wss://nexus-websocket-a.intercom.io https://api-iam.intercom.io https://api.heroku.com/ https://id.heroku.com/oauth/authorize https://id.heroku.com/oauth/token https://checkout.stripe.com https://app.posthog.com https://api.stripe.com https://api.pwnedpasswords.com http://127.0.0.1:* https://hcaptcha.com https://*.hcaptcha.com; - img-src 'self' https://static.intercomassets.com https://js.intercomcdn.com https://downloads.intercomcdn.com https://*.stripe.com https://i.ytimg.com/ data:; - media-src https://js.intercomcdn.com; - font-src 'self' https://fonts.intercomcdn.com/ https://maxcdn.bootstrapcdn.com https://rsms.me https://fonts.gstatic.com; -`; - -// You can choose which headers to add to the list -// after learning more below. -const securityHeaders = [ - { - key: "X-DNS-Prefetch-Control", - value: "on" - }, - { - key: "Strict-Transport-Security", - value: "max-age=63072000; includeSubDomains; preload" - }, - { - key: "X-XSS-Protection", - value: "1; mode=block" - }, - { - key: "X-Frame-Options", - value: "SAMEORIGIN" - }, - { - key: "Permissions-Policy", - value: "camera=(), microphone=()" - }, - { - key: "X-Content-Type-Options", - value: "nosniff" - }, - { - key: "Referrer-Policy", - value: "strict-origin-when-cross-origin" - }, - { - key: "Content-Security-Policy", - value: ContentSecurityPolicy.replace(/\s{2,}/g, " ").trim() - } -]; -/** - * @type {import('next').NextConfig} - **/ -module.exports = { - output: "standalone", - i18n: { - locales: ["en", "ko", "fr", "pt-BR", "pt-PT", "es"], - defaultLocale: "en" - }, - async headers() { - return [ - { - // Apply these headers to all routes in your application. - source: "/:path*", - headers: securityHeaders - } - ]; - }, - webpack: (config, { isServer, webpack }) => { - // config - config.module.rules.push({ - test: /\.wasm$/, - loader: "base64-loader", - type: "javascript/auto" - }); - - config.module.noParse = /\.wasm$/; - - config.module.rules.forEach((rule) => { - (rule.oneOf || []).forEach((oneOf) => { - if (oneOf.loader && oneOf.loader.indexOf("file-loader") >= 0) { - oneOf.exclude.push(/\.wasm$/); - } - }); - }); - - if (!isServer) { - config.resolve.fallback.fs = false; - } - - // Perform customizations to webpack config - config.plugins.push(new webpack.IgnorePlugin({ resourceRegExp: /\/__tests__\// })); - - // Important: return the modified config - return config; - } -}; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6e5ad62e5..e7f57e85c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,285 +1,209 @@ { - "name": "frontend", + "name": "frontend-v2", + "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { + "name": "frontend-v2", + "version": "0.0.0", "dependencies": { - "@casl/ability": "^6.5.0", - "@casl/react": "^3.1.0", - "@dnd-kit/core": "^6.0.8", - "@dnd-kit/modifiers": "^6.0.1", - "@dnd-kit/sortable": "^7.0.2", - "@emotion/css": "^11.10.0", - "@emotion/server": "^11.10.0", - "@fontsource/inter": "^4.5.15", - "@fortawesome/fontawesome-svg-core": "^6.1.2", - "@fortawesome/free-brands-svg-icons": "^6.1.2", - "@fortawesome/free-regular-svg-icons": "^6.1.1", - "@fortawesome/free-solid-svg-icons": "^6.1.2", - "@fortawesome/react-fontawesome": "^0.2.0", - "@hcaptcha/react-hcaptcha": "^1.10.1", - "@headlessui/react": "^1.7.7", - "@hookform/resolvers": "^2.9.10", - "@octokit/rest": "^19.0.7", - "@peculiar/x509": "^1.11.0", - "@radix-ui/react-accordion": "^1.1.2", - "@radix-ui/react-alert-dialog": "^1.0.5", - "@radix-ui/react-checkbox": "^1.0.4", - "@radix-ui/react-collapsible": "^1.0.3", - "@radix-ui/react-dialog": "^1.0.5", - "@radix-ui/react-dropdown-menu": "^2.0.6", - "@radix-ui/react-hover-card": "^1.0.7", - "@radix-ui/react-label": "^2.0.2", - "@radix-ui/react-popover": "^1.0.7", - "@radix-ui/react-popper": "^1.1.3", - "@radix-ui/react-progress": "^1.0.3", - "@radix-ui/react-radio-group": "^1.1.3", - "@radix-ui/react-select": "^2.0.0", - "@radix-ui/react-switch": "^1.0.3", - "@radix-ui/react-tabs": "^1.0.4", - "@radix-ui/react-toast": "^1.1.5", - "@radix-ui/react-tooltip": "^1.0.7", - "@reduxjs/toolkit": "^1.8.3", - "@sindresorhus/slugify": "1.1.0", - "@stripe/react-stripe-js": "^1.16.3", - "@stripe/stripe-js": "^1.46.0", - "@tanstack/react-query": "^4.23.0", - "@types/argon2-browser": "^1.18.1", + "@casl/ability": "^6.7.2", + "@casl/react": "^4.0.0", + "@dagrejs/dagre": "^1.1.4", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", + "@fontsource/inter": "^5.1.0", + "@fortawesome/fontawesome-svg-core": "^6.7.1", + "@fortawesome/free-brands-svg-icons": "^6.7.1", + "@fortawesome/free-regular-svg-icons": "^6.7.1", + "@fortawesome/free-solid-svg-icons": "^6.7.1", + "@fortawesome/react-fontawesome": "^0.2.2", + "@hcaptcha/react-hcaptcha": "^1.11.0", + "@headlessui/react": "^1.7.19", + "@hookform/resolvers": "^3.9.1", + "@lexical/react": "^0.29.0", + "@lottiefiles/dotlottie-react": "^0.12.0", + "@octokit/rest": "^21.0.2", + "@peculiar/x509": "^1.12.3", + "@radix-ui/react-accordion": "^1.2.2", + "@radix-ui/react-alert-dialog": "^1.1.3", + "@radix-ui/react-checkbox": "^1.1.3", + "@radix-ui/react-collapsible": "^1.1.2", + "@radix-ui/react-dialog": "^1.1.3", + "@radix-ui/react-dropdown-menu": "^2.1.3", + "@radix-ui/react-hover-card": "^1.1.3", + "@radix-ui/react-label": "^2.1.1", + "@radix-ui/react-popover": "^1.1.3", + "@radix-ui/react-popper": "^1.2.1", + "@radix-ui/react-progress": "^1.1.1", + "@radix-ui/react-radio-group": "^1.2.2", + "@radix-ui/react-select": "^2.1.3", + "@radix-ui/react-switch": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.2", + "@radix-ui/react-toast": "^1.2.3", + "@radix-ui/react-tooltip": "^1.1.5", + "@sindresorhus/slugify": "^2.2.1", + "@tanstack/react-query": "^5.62.7", + "@tanstack/react-router": "^1.95.1", + "@tanstack/virtual-file-routes": "^1.87.6", + "@tanstack/zod-adapter": "^1.91.0", + "@types/dagre": "^0.7.52", + "@types/nprogress": "^0.2.3", "@ucast/mongo2js": "^1.3.4", - "add": "^2.0.6", + "@xyflow/react": "^12.4.4", "argon2-browser": "^1.18.0", - "axios": "^0.28.0", - "axios-auth-refresh": "^3.3.6", - "base64-loader": "^1.0.0", - "classnames": "^2.3.1", - "cookies": "^0.9.1", - "cva": "npm:class-variance-authority@^0.4.0", - "date-fns": "^2.30.0", + "axios": "^1.7.9", + "classnames": "^2.5.1", + "cva": "npm:class-variance-authority@^0.7.1", + "date-fns": "^4.1.0", + "dompurify": "^3.2.4", "file-saver": "^2.0.5", - "framer-motion": "^6.2.3", - "fs": "^0.0.2", - "gray-matter": "^4.0.3", - "http-proxy": "^1.18.1", - "i18next": "^22.4.15", - "i18next-browser-languagedetector": "^7.0.1", - "i18next-http-backend": "^2.2.0", - "infisical-node": "^1.0.37", + "framer-motion": "^11.14.1", + "i18next": "^24.1.0", + "i18next-browser-languagedetector": "^8.0.2", + "i18next-http-backend": "^3.0.1", "jspdf": "^2.5.2", "jsrp": "^0.2.4", - "jwt-decode": "^3.1.2", - "lottie-react": "^2.4.0", - "markdown-it": "^13.0.1", + "jwt-decode": "^4.0.0", + "lexical": "^0.29.0", "ms": "^2.1.3", - "next": "^12.3.4", "nprogress": "^0.2.0", - "picomatch": "^2.3.1", - "posthog-js": "^1.105.6", - "query-string": "^7.1.3", - "react": "^17.0.2", - "react-beautiful-dnd": "^13.1.1", + "picomatch": "^4.0.2", + "posthog-js": "^1.198.0", + "qrcode": "^1.5.4", + "react": "^18.3.1", "react-code-input": "^3.10.1", - "react-day-picker": "^8.8.0", - "react-dom": "^17.0.2", - "react-grid-layout": "^1.3.4", - "react-hook-form": "^7.43.0", - "react-i18next": "^12.2.2", - "react-icons": "^5.3.0", - "react-mailchimp-subscribe": "^2.1.3", - "react-markdown": "^8.0.3", - "react-redux": "^8.0.2", - "react-select": "^5.8.1", - "react-table": "^7.8.0", - "react-toastify": "^9.1.3", - "sanitize-html": "^2.12.1", - "set-cookie-parser": "^2.5.1", - "sharp": "^0.33.2", - "styled-components": "^5.3.7", - "tailwind-merge": "^1.8.1", + "react-day-picker": "^9.4.3", + "react-dom": "^18.3.1", + "react-helmet": "^6.1.0", + "react-hook-form": "^7.54.0", + "react-i18next": "^15.2.0", + "react-icons": "^5.4.0", + "react-markdown": "^10.0.1", + "react-select": "^5.9.0", + "react-toastify": "^10.0.6", + "redaxios": "^0.5.1", + "rehype-raw": "^7.0.0", + "tailwind-merge": "^2.5.5", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", - "uuid": "^8.3.2", - "uuidv4": "^6.2.13", - "yaml": "^2.2.2", - "yup": "^0.32.11", - "zod": "^3.22.3", - "zustand": "^4.5.0" + "yaml": "^2.6.1", + "zod": "^3.24.1", + "zustand": "^5.0.2" }, "devDependencies": { - "@storybook/addon-essentials": "^7.5.2", - "@storybook/addon-interactions": "^7.0.23", - "@storybook/addon-links": "^7.0.23", - "@storybook/addon-styling": "^1.3.0", - "@storybook/blocks": "^7.0.23", - "@storybook/client-api": "^7.2.1", - "@storybook/nextjs": "^7.0.23", - "@storybook/react": "^7.0.23", - "@storybook/testing-library": "^0.2.0", - "@tailwindcss/typography": "^0.5.4", - "@types/file-saver": "^2.0.5", - "@types/jsrp": "^0.2.4", - "@types/node": "^18.11.9", - "@types/picomatch": "^2.3.0", - "@types/react": "^18.0.26", - "@types/sanitize-html": "^2.9.0", - "@typescript-eslint/eslint-plugin": "^5.48.1", - "@typescript-eslint/parser": "^5.45.0", - "autoprefixer": "^10.4.7", - "cypress": "^13.3.2", - "eslint": "^8.32.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.15.0", + "@kesills/eslint-config-airbnb-typescript": "^20.0.0", + "@stylistic/eslint-plugin": "^2.12.1", + "@tailwindcss/typography": "^0.5.15", + "@tanstack/eslint-plugin-router": "^1.87.6", + "@tanstack/router-devtools": "^1.87.9", + "@tanstack/router-plugin": "^1.95.1", + "@types/argon2-browser": "^1.18.4", + "@types/file-saver": "^2.0.7", + "@types/jsrp": "^0.2.6", + "@types/ms": "^0.7.34", + "@types/picomatch": "^3.0.1", + "@types/qrcode": "^1.5.5", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@types/react-helmet": "^6.1.11", + "@vitejs/plugin-react-swc": "^3.5.0", + "autoprefixer": "^10.4.20", + "eslint": "^8.57.1", "eslint-config-airbnb": "^19.0.4", - "eslint-config-airbnb-typescript": "^17.0.0", - "eslint-config-next": "^13.0.5", - "eslint-config-prettier": "^8.6.0", - "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.27.4", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-react": "^7.32.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-simple-import-sort": "^8.0.0", - "eslint-plugin-storybook": "^0.6.12", - "postcss": "^8.4.39", - "prettier": "^2.8.3", - "prettier-plugin-tailwindcss": "^0.2.2", - "storybook": "^7.6.20", - "storybook-dark-mode": "^3.0.0", - "tailwindcss": "3.2", - "typescript": "^4.9.3" + "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.7.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.14", + "eslint-plugin-simple-import-sort": "^12.1.1", + "globals": "^15.12.0", + "postcss": "^8.4.49", + "prettier": "3.4.2", + "prettier-plugin-tailwindcss": "^0.6.9", + "tailwindcss": "^3.4.16", + "typescript": "~5.6.2", + "typescript-eslint": "^8.15.0", + "vite": "^5.4.18", + "vite-plugin-node-polyfills": "^0.22.0", + "vite-plugin-top-level-await": "^1.4.4", + "vite-plugin-wasm": "^3.3.0", + "vite-tsconfig-paths": "^5.1.4" } }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "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": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, - "node_modules/@aw-web-design/x-default-browser": { - "version": "1.4.126", - "resolved": "https://registry.npmjs.org/@aw-web-design/x-default-browser/-/x-default-browser-1.4.126.tgz", - "integrity": "sha512-Xk1sIhyNC/esHGGVjL/niHLowM0csl/kFO5uawBy4IrWwy0o1G8LGt3jP6nmWGz+USxeeqbihAmp/oVZju6wug==", - "dev": true, - "dependencies": { - "default-browser-id": "3.0.0" - }, - "bin": { - "x-default-browser": "bin/x-default-browser.js" - } - }, "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/compat-data": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", - "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz", + "integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==", + "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.7.tgz", - "integrity": "sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", + "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", + "dev": true, + "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.7", - "@babel/parser": "^7.23.6", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.7", - "@babel/types": "^7.23.6", + "@babel/code-frame": "^7.26.0", + "@babel/generator": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.0", + "@babel/parser": "^7.26.0", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -294,56 +218,55 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } }, "node_modules/@babel/generator": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", + "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", - "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.15" + "@babel/parser": "^7.26.3", + "@babel/types": "^7.26.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", + "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "dev": true, + "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", - "browserslist": "^4.22.2", + "@babel/compat-data": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -351,126 +274,49 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.7.tgz", - "integrity": "sha512-xCoqR/8+BoNnXOY7RVSgv6X+o7pmT5q1d+gGcRlXYkI+9B31glE4jeejhKVpA04O1AtzOt7OSQ6VYKP5FcRl9g==", + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, + "license": "ISC", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-member-expression-to-functions": "^7.23.0", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "yallist": "^3.0.2" } }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", - "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "regexpu-core": "^5.3.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz", - "integrity": "sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA==", - "dev": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", - "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.22.15" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -479,226 +325,66 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", - "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", + "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-wrap-function": "^7.22.20" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", - "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dependencies": { - "@babel/types": "^7.22.5" - }, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", - "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", "dev": true, - "dependencies": { - "@babel/helper-function-name": "^7.22.5", - "@babel/template": "^7.22.15", - "@babel/types": "^7.22.19" - }, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.23.8", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.8.tgz", - "integrity": "sha512-KDqYz4PiOWvDFrdHLPhKtCThtIcKVy6avWD2oG4GEvyQ+XDZwHD4YQd+H2vNMnq2rkdxsDkU82T+Vk8U/WXHRQ==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", + "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", + "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.7", - "@babel/types": "^7.23.6" + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/parser": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", - "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.10" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -706,318 +392,14 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz", - "integrity": "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz", - "integrity": "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.23.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.7.tgz", - "integrity": "sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-flow": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.23.3.tgz", - "integrity": "sha512-YZiAIpkJAwQXBJLIQbRFayR5c+gJ35Vcz3bg954k7cd73zqjvhacJuL9RbrzPz8qPmZdgqP6EUKwy0PCNhaaPA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz", - "integrity": "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz", - "integrity": "sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", - "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1027,12 +409,13 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz", - "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1041,1224 +424,11 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz", - "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.7.tgz", - "integrity": "sha512-PdxEpL71bJp1byMG0va5gwQcXHxuEYC/BgI/e88mGTtohbZN28O5Yit0Plkkm/dBzCF/BxmbNcses1RH1T+urA==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20", - "@babel/plugin-syntax-async-generators": "^7.8.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", - "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz", - "integrity": "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz", - "integrity": "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz", - "integrity": "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz", - "integrity": "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.23.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.8.tgz", - "integrity": "sha512-yAYslGsY1bX6Knmg46RjiCiNSwJKv2IUC8qOdYKqMMr0491SXFhcHqOdRDeCRohOOIzwN/90C6mQ9qAKgrP7dg==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20", - "@babel/helper-split-export-declaration": "^7.22.6", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz", - "integrity": "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz", - "integrity": "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz", - "integrity": "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz", - "integrity": "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz", - "integrity": "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz", - "integrity": "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==", - "dev": true, - "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz", - "integrity": "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.23.3.tgz", - "integrity": "sha512-26/pQTf9nQSNVJCrLB1IkHUKyPxR+lMrH2QDPG89+Znu9rAMbtrybdbWeE9bb7gzjmE5iXHEY+e0HUwM6Co93Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-flow": "^7.23.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz", - "integrity": "sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz", - "integrity": "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==", - "dev": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz", - "integrity": "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-json-strings": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz", - "integrity": "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz", - "integrity": "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz", - "integrity": "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz", - "integrity": "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz", - "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.3.tgz", - "integrity": "sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==", - "dev": true, - "dependencies": { - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz", - "integrity": "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", - "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz", - "integrity": "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz", - "integrity": "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz", - "integrity": "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz", - "integrity": "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.23.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz", - "integrity": "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz", - "integrity": "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz", - "integrity": "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz", - "integrity": "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz", - "integrity": "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz", - "integrity": "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz", - "integrity": "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.23.3.tgz", - "integrity": "sha512-GnvhtVfA2OAtzdX58FJxU19rhoGeQzyVndw3GgtdECQvQFXPEZIOVULHVZGAYmOgmqjXpVpfocAbSjh99V/Fqw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz", - "integrity": "sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-jsx": "^7.23.3", - "@babel/types": "^7.23.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz", - "integrity": "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==", - "dev": true, - "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.23.3.tgz", - "integrity": "sha512-qMFdSS+TUhB7Q/3HVPnEdYJDQIk57jkntAwSuz9xfSE4n+3I+vHYCli3HoHawN1Z3RfCz/y1zXA/JXjG6cVImQ==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz", - "integrity": "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "regenerator-transform": "^0.15.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz", - "integrity": "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.7.tgz", - "integrity": "sha512-fa0hnfmiXc9fq/weK34MUV0drz2pOL/vfKWvN7Qw127hiUPabFCUMgAbYWcchRzMJit4o5ARsK/s+5h0249pLw==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "babel-plugin-polyfill-corejs2": "^0.4.7", - "babel-plugin-polyfill-corejs3": "^0.8.7", - "babel-plugin-polyfill-regenerator": "^0.5.4", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz", - "integrity": "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz", - "integrity": "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz", - "integrity": "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz", - "integrity": "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz", - "integrity": "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.23.6.tgz", - "integrity": "sha512-6cBG5mBvUu4VUD04OHKnYzbuHNP8huDsD3EDqqpIpsswTDoqHCjLoHb6+QgsV1WsT2nipRqCPgxD3LXnEO7XfA==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.23.6", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-typescript": "^7.23.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz", - "integrity": "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz", - "integrity": "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz", - "integrity": "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz", - "integrity": "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.23.8", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.8.tgz", - "integrity": "sha512-lFlpmkApLkEP6woIKprO6DO60RImpatTQKtz4sUcDjVcK8M8mQ4sZsuxaTMNOZf0sqAq/ReYW1ZBHnOQwKpLWA==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.23.5", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.23.3", - "@babel/plugin-syntax-import-attributes": "^7.23.3", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.23.3", - "@babel/plugin-transform-async-generator-functions": "^7.23.7", - "@babel/plugin-transform-async-to-generator": "^7.23.3", - "@babel/plugin-transform-block-scoped-functions": "^7.23.3", - "@babel/plugin-transform-block-scoping": "^7.23.4", - "@babel/plugin-transform-class-properties": "^7.23.3", - "@babel/plugin-transform-class-static-block": "^7.23.4", - "@babel/plugin-transform-classes": "^7.23.8", - "@babel/plugin-transform-computed-properties": "^7.23.3", - "@babel/plugin-transform-destructuring": "^7.23.3", - "@babel/plugin-transform-dotall-regex": "^7.23.3", - "@babel/plugin-transform-duplicate-keys": "^7.23.3", - "@babel/plugin-transform-dynamic-import": "^7.23.4", - "@babel/plugin-transform-exponentiation-operator": "^7.23.3", - "@babel/plugin-transform-export-namespace-from": "^7.23.4", - "@babel/plugin-transform-for-of": "^7.23.6", - "@babel/plugin-transform-function-name": "^7.23.3", - "@babel/plugin-transform-json-strings": "^7.23.4", - "@babel/plugin-transform-literals": "^7.23.3", - "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", - "@babel/plugin-transform-member-expression-literals": "^7.23.3", - "@babel/plugin-transform-modules-amd": "^7.23.3", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-modules-systemjs": "^7.23.3", - "@babel/plugin-transform-modules-umd": "^7.23.3", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.23.3", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", - "@babel/plugin-transform-numeric-separator": "^7.23.4", - "@babel/plugin-transform-object-rest-spread": "^7.23.4", - "@babel/plugin-transform-object-super": "^7.23.3", - "@babel/plugin-transform-optional-catch-binding": "^7.23.4", - "@babel/plugin-transform-optional-chaining": "^7.23.4", - "@babel/plugin-transform-parameters": "^7.23.3", - "@babel/plugin-transform-private-methods": "^7.23.3", - "@babel/plugin-transform-private-property-in-object": "^7.23.4", - "@babel/plugin-transform-property-literals": "^7.23.3", - "@babel/plugin-transform-regenerator": "^7.23.3", - "@babel/plugin-transform-reserved-words": "^7.23.3", - "@babel/plugin-transform-shorthand-properties": "^7.23.3", - "@babel/plugin-transform-spread": "^7.23.3", - "@babel/plugin-transform-sticky-regex": "^7.23.3", - "@babel/plugin-transform-template-literals": "^7.23.3", - "@babel/plugin-transform-typeof-symbol": "^7.23.3", - "@babel/plugin-transform-unicode-escapes": "^7.23.3", - "@babel/plugin-transform-unicode-property-regex": "^7.23.3", - "@babel/plugin-transform-unicode-regex": "^7.23.3", - "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.7", - "babel-plugin-polyfill-corejs3": "^0.8.7", - "babel-plugin-polyfill-regenerator": "^0.5.4", - "core-js-compat": "^3.31.0", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-flow": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.23.3.tgz", - "integrity": "sha512-7yn6hl8RIv+KNk6iIrGZ+D06VhVY35wLVf23Cz/mMu1zOr7u4MMP4j0nZ9tLf8+4ZFpnib8cFYgB/oYg9hfswA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-transform-flow-strip-types": "^7.23.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/preset-react": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.23.3.tgz", - "integrity": "sha512-tbkHOS9axH6Ysf2OUEqoSZ6T3Fa2SrNH6WTWSPBboxKzdxNc9qOICeLXkNG0ZEwbQ1HY8liwOce4aN/Ceyuq6w==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-transform-react-display-name": "^7.23.3", - "@babel/plugin-transform-react-jsx": "^7.22.15", - "@babel/plugin-transform-react-jsx-development": "^7.22.5", - "@babel/plugin-transform-react-pure-annotations": "^7.23.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-typescript": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.23.3.tgz", - "integrity": "sha512-17oIGVlqz6CchO9RFYn5U6ZpWRZIngayYCtrPRSgANSwC2V1Jb+iP74nVxzzXJte8b8BYxrL1yY96xfhTBrNNQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-syntax-jsx": "^7.23.3", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-typescript": "^7.23.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/register": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.24.6.tgz", - "integrity": "sha512-WSuFCc2wCqMeXkz/i3yfAAsxwWflEgbVkZzivgAmXl/MxrXeoYFZOOPllbC8R8WTF7u61wSRQtDVZ1879cdu6w==", - "dev": true, - "dependencies": { - "clone-deep": "^4.0.1", - "find-cache-dir": "^2.0.0", - "make-dir": "^2.1.0", - "pirates": "^4.0.6", - "source-map-support": "^0.5.16" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/register/node_modules/find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "dev": true, - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/register/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/register/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/register/node_modules/make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "dependencies": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/register/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@babel/register/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/register/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/register/node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/register/node_modules/pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/register/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", - "dev": true - }, "node_modules/@babel/runtime": { - "version": "7.23.8", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz", - "integrity": "sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", + "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -2267,31 +437,30 @@ } }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz", - "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==", + "version": "7.26.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", + "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.6", - "@babel/types": "^7.23.6", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.3", + "@babel/parser": "^7.26.3", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.3", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -2299,29 +468,33 @@ "node": ">=6.9.0" } }, + "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==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@base2/pretty-print-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz", - "integrity": "sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==", - "dev": true - }, "node_modules/@casl/ability": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.5.0.tgz", - "integrity": "sha512-3guc94ugr5ylZQIpJTLz0CDfwNi0mxKVECj1vJUPAvs+Lwunh/dcuUjwzc4MHM9D8JOYX0XUZMEPedpB3vIbOw==", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.7.2.tgz", + "integrity": "sha512-KjKXlcjKbUz8dKw7PY56F7qlfOFgxTU6tnlJ8YrbDyWkJMIlHa6VRWzCD8RU20zbJUC1hExhOFggZjm6tf1mUw==", + "license": "MIT", "dependencies": { "@ucast/mongo2js": "^1.3.0" }, @@ -2330,114 +503,44 @@ } }, "node_modules/@casl/react": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@casl/react/-/react-3.1.0.tgz", - "integrity": "sha512-p4Xmex1Slxz/G0cBtZik+xyOkeOynBUe0UrMFTai6aYkYOb4NyUy3w+9rtnedjcuKijiow2HKJQjnSurLxdc/g==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@casl/react/-/react-4.0.0.tgz", + "integrity": "sha512-ovmI4JfNw7TfVVV+XhAJ//gXgMEkkPJU6YBWFVFZGa8Oikdh8Qxr/sdXcqj71QWEHAGN7aSKMtBE0MZylPUVsg==", + "license": "MIT", "peerDependencies": { "@casl/ability": "^3.0.0 || ^4.0.0 || ^5.1.0 || ^6.0.0", "react": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@cypress/request": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", - "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", - "dev": true, + "node_modules/@dagrejs/dagre": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-1.1.4.tgz", + "integrity": "sha512-QUTc54Cg/wvmlEUxB+uvoPVKFazM1H18kVHBQNmK2NbrDR5ihOCR6CXLnDSZzMcSQKJtabPUWridBOlJM3WkDg==", + "license": "MIT", "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "http-signature": "~1.3.6", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "performance-now": "^2.1.0", - "qs": "6.10.4", - "safe-buffer": "^5.1.2", - "tough-cookie": "^4.1.3", - "tunnel-agent": "^0.6.0", - "uuid": "^8.3.2" - }, + "@dagrejs/graphlib": "2.2.4" + } + }, + "node_modules/@dagrejs/graphlib": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-2.2.4.tgz", + "integrity": "sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==", + "license": "MIT", "engines": { - "node": ">= 6" + "node": ">17.0.0" } }, - "node_modules/@cypress/request/node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/@cypress/request/node_modules/qs": { - "version": "6.10.4", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", - "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", - "dev": true, - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@cypress/xvfb": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", - "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", - "dev": true, - "dependencies": { - "debug": "^3.1.0", - "lodash.once": "^4.1.1" - } - }, - "node_modules/@cypress/xvfb/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } + "node_modules/@date-fns/tz": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz", + "integrity": "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==", + "license": "MIT" }, "node_modules/@dnd-kit/accessibility": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz", - "integrity": "sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, @@ -2446,11 +549,12 @@ } }, "node_modules/@dnd-kit/core": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.1.0.tgz", - "integrity": "sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", "dependencies": { - "@dnd-kit/accessibility": "^3.1.0", + "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, @@ -2460,28 +564,30 @@ } }, "node_modules/@dnd-kit/modifiers": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz", - "integrity": "sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz", + "integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==", + "license": "MIT", "dependencies": { - "@dnd-kit/utilities": "^3.2.1", + "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { - "@dnd-kit/core": "^6.0.6", + "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "node_modules/@dnd-kit/sortable": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz", - "integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", "dependencies": { - "@dnd-kit/utilities": "^3.2.0", + "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { - "@dnd-kit/core": "^6.0.7", + "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, @@ -2489,6 +595,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, @@ -2496,26 +603,17 @@ "react": ">=16.8.0" } }, - "node_modules/@emnapi/runtime": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz", - "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emotion/babel-plugin": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz", - "integrity": "sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw==", + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", - "@emotion/serialize": "^1.2.0", + "@emotion/serialize": "^1.3.3", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", @@ -2524,81 +622,49 @@ "stylis": "4.2.0" } }, - "node_modules/@emotion/babel-plugin/node_modules/@emotion/memoize": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "license": "MIT" }, "node_modules/@emotion/cache": { - "version": "11.13.1", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz", - "integrity": "sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==", + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", "license": "MIT", "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", - "@emotion/utils": "^1.4.0", + "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" } }, - "node_modules/@emotion/cache/node_modules/@emotion/memoize": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", - "license": "MIT" - }, - "node_modules/@emotion/css": { - "version": "11.11.2", - "resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.11.2.tgz", - "integrity": "sha512-VJxe1ucoMYMS7DkiMdC2T7PWNbrEI0a39YRiyDvK2qq4lXwjRbVP/z4lpG+odCsRzadlR+1ywwrTzhdm5HNdew==", - "dependencies": { - "@emotion/babel-plugin": "^11.11.0", - "@emotion/cache": "^11.11.0", - "@emotion/serialize": "^1.1.2", - "@emotion/sheet": "^1.2.2", - "@emotion/utils": "^1.2.1" - } - }, "node_modules/@emotion/hash": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", "license": "MIT" }, - "node_modules/@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", - "optional": true, - "dependencies": { - "@emotion/memoize": "0.7.4" - } - }, - "node_modules/@emotion/is-prop-valid/node_modules/@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", - "optional": true - }, "node_modules/@emotion/memoize": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" }, "node_modules/@emotion/react": { - "version": "11.13.3", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.3.tgz", - "integrity": "sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==", + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.12.0", - "@emotion/cache": "^11.13.0", - "@emotion/serialize": "^1.3.1", - "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", - "@emotion/utils": "^1.4.0", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "hoist-non-react-statics": "^3.3.1" }, @@ -2612,54 +678,24 @@ } }, "node_modules/@emotion/serialize": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.2.tgz", - "integrity": "sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", "license": "MIT", "dependencies": { "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/unitless": "^0.10.0", - "@emotion/utils": "^1.4.1", + "@emotion/utils": "^1.4.2", "csstype": "^3.0.2" } }, - "node_modules/@emotion/serialize/node_modules/@emotion/memoize": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", - "license": "MIT" - }, - "node_modules/@emotion/server": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/@emotion/server/-/server-11.11.0.tgz", - "integrity": "sha512-6q89fj2z8VBTx9w93kJ5n51hsmtYuFPtZgnc1L8VzRx9ti4EU6EyvF6Nn1H1x3vcCQCF7u2dB2lY4AYJwUW4PA==", - "dependencies": { - "@emotion/utils": "^1.2.1", - "html-tokenize": "^2.0.0", - "multipipe": "^1.0.2", - "through": "^2.3.8" - }, - "peerDependencies": { - "@emotion/css": "^11.0.0-rc.0" - }, - "peerDependenciesMeta": { - "@emotion/css": { - "optional": true - } - } - }, "node_modules/@emotion/sheet": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", "license": "MIT" }, - "node_modules/@emotion/stylis": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", - "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" - }, "node_modules/@emotion/unitless": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", @@ -2667,18 +703,18 @@ "license": "MIT" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz", - "integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", "license": "MIT", "peerDependencies": { "react": ">=16.8.0" } }, "node_modules/@emotion/utils": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.1.tgz", - "integrity": "sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", "license": "MIT" }, "node_modules/@emotion/weak-memoize": { @@ -2687,14 +723,32 @@ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", "license": "MIT" }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -2704,13 +758,14 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -2720,13 +775,14 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -2736,13 +792,14 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -2752,13 +809,14 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -2768,13 +826,14 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -2784,13 +843,14 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -2800,13 +860,14 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2816,13 +877,14 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2832,13 +894,14 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2848,13 +911,14 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2864,13 +928,14 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2880,13 +945,14 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2896,13 +962,14 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2912,13 +979,14 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2928,13 +996,14 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2943,14 +1012,32 @@ "node": ">=12" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz", + "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -2959,14 +1046,32 @@ "node": ">=12" } }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz", + "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -2976,13 +1081,14 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" @@ -2992,13 +1098,14 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -3008,13 +1115,14 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -3024,13 +1132,14 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -3040,45 +1149,30 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", "dev": true, + "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -3086,89 +1180,107 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "dev": true, + "license": "MIT", "dependencies": { - "type-fest": "^0.20.2" + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": ">=8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "node_modules/@eslint/eslintrc/node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "argparse": "^2.0.1" + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz", + "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==", "dev": true, + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@fal-works/esbuild-plugin-global-externals": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@fal-works/esbuild-plugin-global-externals/-/esbuild-plugin-global-externals-2.1.2.tgz", - "integrity": "sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==", - "dev": true - }, "node_modules/@floating-ui/core": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.3.tgz", - "integrity": "sha512-O0WKDOo0yhJuugCx6trZQj5jVJ9yR0ystG2JaNAemYUWce+pmM6WUEFIibnWyEJKdrDxhm75NoSRME35FNaM/Q==", + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", + "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.0" + "@floating-ui/utils": "^0.2.8" } }, "node_modules/@floating-ui/dom": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.4.tgz", - "integrity": "sha512-jByEsHIY+eEdCjnTVu+E3ephzTOzkQ8hgUfGwos+bg7NlH33Zc5uO+QHz1mrQUOgIKKDD1RtS201P9NvAfq3XQ==", + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz", + "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==", + "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.5.3", - "@floating-ui/utils": "^0.2.0" + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.8" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.5.tgz", - "integrity": "sha512-UsBK30Bg+s6+nsgblXtZmwHhgS2vmbuQK22qgt2pTQM6M3X6H1+cQcLXqgRY3ihVLcZJE6IvqDQozhsnIVqK/Q==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.5.4" + "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", @@ -3176,76 +1288,79 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", - "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==", + "license": "MIT" }, "node_modules/@fontsource/inter": { - "version": "4.5.15", - "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-4.5.15.tgz", - "integrity": "sha512-FzleM9AxZQK2nqsTDtBiY0PMEVWvnKnuu2i09+p6DHvrHsuucoV2j0tmw+kAT3L4hvsLdAIDv6MdGehsPIdT+Q==" + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.1.0.tgz", + "integrity": "sha512-zKZR3kf1G0noIes1frLfOHP5EXVVm0M7sV/l9f/AaYf+M/DId35FO4LkigWjqWYjTJZGgplhdv4cB+ssvCqr5A==", + "license": "OFL-1.1" }, "node_modules/@fortawesome/fontawesome-common-types": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz", - "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==", - "hasInstallScript": true, + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.1.tgz", + "integrity": "sha512-gbDz3TwRrIPT3i0cDfujhshnXO9z03IT1UKRIVi/VEjpNHtSBIP2o5XSm+e816FzzCFEzAxPw09Z13n20PaQJQ==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/@fortawesome/fontawesome-svg-core": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz", - "integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==", - "hasInstallScript": true, + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.1.tgz", + "integrity": "sha512-8dBIHbfsKlCk2jHQ9PoRBg2Z+4TwyE3vZICSnoDlnsHA6SiMlTwfmW6yX0lHsRmWJugkeb92sA0hZdkXJhuz+g==", + "license": "MIT", "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.1" + "@fortawesome/fontawesome-common-types": "6.7.1" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-brands-svg-icons": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.5.1.tgz", - "integrity": "sha512-093l7DAkx0aEtBq66Sf19MgoZewv1zeY9/4C7vSKPO4qMwEsW/2VYTUTpBtLwfb9T2R73tXaRDPmE4UqLCYHfg==", - "hasInstallScript": true, + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.7.1.tgz", + "integrity": "sha512-nJR76eqPzCnMyhbiGf6X0aclDirZriTPRcFm1YFvuupyJOGwlNF022w3YBqu+yrHRhnKRpzFX+8wJKqiIjWZkA==", + "license": "(CC-BY-4.0 AND MIT)", "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.1" + "@fortawesome/fontawesome-common-types": "6.7.1" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-regular-svg-icons": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.1.tgz", - "integrity": "sha512-m6ShXn+wvqEU69wSP84coxLbNl7sGVZb+Ca+XZq6k30SzuP3X4TfPqtycgUh9ASwlNh5OfQCd8pDIWxl+O+LlQ==", - "hasInstallScript": true, + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.1.tgz", + "integrity": "sha512-e13cp+bAx716RZOTQ59DhqikAgETA9u1qTBHO3e3jMQQ+4H/N1NC1ZVeFYt1V0m+Th68BrEL1/X6XplISutbXg==", + "license": "(CC-BY-4.0 AND MIT)", "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.1" + "@fortawesome/fontawesome-common-types": "6.7.1" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-solid-svg-icons": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz", - "integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==", - "hasInstallScript": true, + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.1.tgz", + "integrity": "sha512-BTKc0b0mgjWZ2UDKVgmwaE0qt0cZs6ITcDgjrti5f/ki7aF5zs+N91V6hitGo3TItCFtnKg6cUVGdTmBFICFRg==", + "license": "(CC-BY-4.0 AND MIT)", "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.1" + "@fortawesome/fontawesome-common-types": "6.7.1" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/react-fontawesome": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz", - "integrity": "sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz", + "integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==", + "license": "MIT", "dependencies": { "prop-types": "^15.8.1" }, @@ -3257,12 +1372,14 @@ "node_modules/@hcaptcha/loader": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@hcaptcha/loader/-/loader-1.2.4.tgz", - "integrity": "sha512-3MNrIy/nWBfyVVvMPBKdKrX7BeadgiimW0AL/a/8TohNtJqxoySKgTJEXOQvYwlHemQpUzFrIsK74ody7JiMYw==" + "integrity": "sha512-3MNrIy/nWBfyVVvMPBKdKrX7BeadgiimW0AL/a/8TohNtJqxoySKgTJEXOQvYwlHemQpUzFrIsK74ody7JiMYw==", + "license": "MIT" }, "node_modules/@hcaptcha/react-hcaptcha": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@hcaptcha/react-hcaptcha/-/react-hcaptcha-1.10.1.tgz", - "integrity": "sha512-P0en4gEZAecah7Pt3WIaJO2gFlaLZKkI0+Tfdg8fNqsDxqT9VytZWSkH4WAkiPRULK1QcGgUZK+J56MXYmPifw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@hcaptcha/react-hcaptcha/-/react-hcaptcha-1.11.0.tgz", + "integrity": "sha512-UKHtzzVMHLTGwab5pgV96UbcXdyh5Qyq8E0G5DTyXq8txMvuDx7rSyC+BneOjWVW0a7O9VuZmkg/EznVLRE45g==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.17.9", "@hcaptcha/loader": "^1.2.1" @@ -3273,9 +1390,10 @@ } }, "node_modules/@headlessui/react": { - "version": "1.7.18", - "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.18.tgz", - "integrity": "sha512-4i5DOrzwN4qSgNsL4Si61VMkUcWbcSKueUV7sFhpHzQcSShdlHENE5+QBntMSRvHt8NyoFO2AGG8si9lq+w4zQ==", + "version": "1.7.19", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.19.tgz", + "integrity": "sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==", + "license": "MIT", "dependencies": { "@tanstack/react-virtual": "^3.0.0-beta.60", "client-only": "^0.0.1" @@ -3289,20 +1407,23 @@ } }, "node_modules/@hookform/resolvers": { - "version": "2.9.11", - "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-2.9.11.tgz", - "integrity": "sha512-bA3aZ79UgcHj7tFV7RlgThzwSSHZgvfbt2wprldRkYBcMopdMvHyO17Wwp/twcJasNFischFfS7oz8Katz8DdQ==", + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.1.tgz", + "integrity": "sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==", + "license": "MIT", "peerDependencies": { "react-hook-form": "^7.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" }, @@ -3315,6 +1436,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=12.22" }, @@ -3324,447 +1446,19 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", - "dev": true - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.2.tgz", - "integrity": "sha512-itHBs1rPmsmGF9p4qRe++CzCgd+kFYktnsoR1sbIAfsRMrJZau0Tt1AH9KVnufc2/tU02Gf6Ibujx+15qRE03w==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.1" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.2.tgz", - "integrity": "sha512-/rK/69Rrp9x5kaWBjVN07KixZanRr+W1OiyKdXcbjQD6KbW+obaTeBBtLUAtbBsnlTTmWthw99xqoOS7SsySDg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.1" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.1.tgz", - "integrity": "sha512-kQyrSNd6lmBV7O0BUiyu/OEw9yeNGFbQhbxswS1i6rMDwBBSX+e+rPzu3S+MwAiGU3HdLze3PanQ4Xkfemgzcw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "macos": ">=11", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.1.tgz", - "integrity": "sha512-eVU/JYLPVjhhrd8Tk6gosl5pVlvsqiFlt50wotCvdkFGf+mDNBJxMh+bvav+Wt3EBnNZWq8Sp2I7XfSjm8siog==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "macos": ">=10.13", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.1.tgz", - "integrity": "sha512-FtdMvR4R99FTsD53IA3LxYGghQ82t3yt0ZQ93WMZ2xV3dqrb0E8zq4VHaTOuLEAuA83oDawHV3fd+BsAPadHIQ==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "glibc": ">=2.28", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.1.tgz", - "integrity": "sha512-bnGG+MJjdX70mAQcSLxgeJco11G+MxTz+ebxlz8Y3dxyeb3Nkl7LgLI0mXupoO+u1wRNx/iRj5yHtzA4sde1yA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "glibc": ">=2.26", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.1.tgz", - "integrity": "sha512-3+rzfAR1YpMOeA2zZNp+aYEzGNWK4zF3+sdMxuCS3ey9HhDbJ66w6hDSHDMoap32DueFwhhs3vwooAB2MaK4XQ==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "glibc": ">=2.28", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.1.tgz", - "integrity": "sha512-3NR1mxFsaSgMMzz1bAnnKbSAI+lHXVTqAHgc1bgzjHuXjo4hlscpUxc0vFSAPKI3yuzdzcZOkq7nDPrP2F8Jgw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "glibc": ">=2.26", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.1.tgz", - "integrity": "sha512-5aBRcjHDG/T6jwC3Edl3lP8nl9U2Yo8+oTl5drd1dh9Z1EBfzUKAJFUDTDisDjUwc7N4AjnPGfCA3jl3hY8uDg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "musl": ">=1.2.2", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.1.tgz", - "integrity": "sha512-dcT7inI9DBFK6ovfeWRe3hG30h51cBAP5JXlZfx6pzc/Mnf9HFCQDLtYf4MCBjxaaTfjCCjkBxcy3XzOAo5txw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "musl": ">=1.2.2", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.2.tgz", - "integrity": "sha512-Fndk/4Zq3vAc4G/qyfXASbS3HBZbKrlnKZLEJzPLrXoJuipFNNwTes71+Ki1hwYW5lch26niRYoZFAtZVf3EGA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "glibc": ">=2.28", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.1" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.2.tgz", - "integrity": "sha512-pz0NNo882vVfqJ0yNInuG9YH71smP4gRSdeL09ukC2YLE6ZyZePAlWKEHgAzJGTiOh8Qkaov6mMIMlEhmLdKew==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.1" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.2.tgz", - "integrity": "sha512-MBoInDXDppMfhSzbMmOQtGfloVAflS2rP1qPcUIiITMi36Mm5YR7r0ASND99razjQUpHTzjrU1flO76hKvP5RA==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "glibc": ">=2.28", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.1" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.2.tgz", - "integrity": "sha512-xUT82H5IbXewKkeF5aiooajoO1tQV4PnKfS/OZtb5DDdxS/FCI/uXTVZ35GQ97RZXsycojz/AJ0asoz6p2/H/A==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.1" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.2.tgz", - "integrity": "sha512-F+0z8JCu/UnMzg8IYW1TMeiViIWBVg7IWP6nE0p5S5EPQxlLd76c8jYemG21X99UzFwgkRo5yz2DS+zbrnxZeA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "musl": ">=1.2.2", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.1" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.2.tgz", - "integrity": "sha512-+ZLE3SQmSL+Fn1gmSaM8uFusW5Y3J9VOf+wMGNnTtJUMUxFhv+P4UPaYEYT8tqnyYVaOVGgMN/zsOxn9pSsO2A==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "musl": ">=1.2.2", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.1" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.2.tgz", - "integrity": "sha512-fLbTaESVKuQcpm8ffgBD7jLb/CQLcATju/jxtTXR1XCLwbOQt+OL5zPHSDMmp2JZIeq82e18yE0Vv7zh6+6BfQ==", - "cpu": [ - "wasm32" - ], - "optional": true, - "dependencies": { - "@emnapi/runtime": "^0.45.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.2.tgz", - "integrity": "sha512-okBpql96hIGuZ4lN3+nsAjGeggxKm7hIRu9zyec0lnfB8E7Z6p95BuRZzDDXZOl2e8UmR4RhYt631i7mfmKU8g==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.2.tgz", - "integrity": "sha512-E4magOks77DK47FwHUIGH0RYWSgRBfGdK56kIHSVeB9uIS4pPFr4N2kIVsXdQQo4LzOsENKV5KAhRlRL7eMAdg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -3778,10 +1472,11 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -3789,28 +1484,12 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -3821,571 +1500,356 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/transform/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", "engines": { "node": ">=6.0.0" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", - "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, "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==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.21", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.21.tgz", - "integrity": "sha512-SRfKmRe1KvYnxjEMtxEr+J4HIeMX5YBg/qhRHpxEIGjhX1rshcHlnFUE9K0GazhVKWM7B+nARSkV8LuvJdJ5/g==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@juggle/resize-observer": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", - "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==", - "dev": true - }, - "node_modules/@mdx-js/react": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-2.3.0.tgz", - "integrity": "sha512-zQH//gdOmuu7nt2oJR29vFhDv88oGPmVw6BggmrHeMI+xgEkp1B2dX9/bMBSYtK0dyLX/aOmesKS09g222K1/g==", + "node_modules/@kesills/eslint-config-airbnb-typescript": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@kesills/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-20.0.0.tgz", + "integrity": "sha512-P3DBcIs5eQsEMz4mgwG9ZsOqwE42EGFJ+fI49IFp8ai8D2dHhByr7oUoTW6aDzGe4tVjFOfCjEjcAA1b8sVWTg==", "dev": true, - "dependencies": { - "@types/mdx": "^2.0.0", - "@types/react": ">=16" + "license": "MIT", + "peerDependencies": { + "@stylistic/eslint-plugin": "^2.6.1", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "eslint": "^8.57.0", + "eslint-config-airbnb": "^19.0.0", + "eslint-config-airbnb-base": "^15.0.0", + "typescript": "*" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "peerDependenciesMeta": { + "eslint-config-airbnb": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/clipboard": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.29.0.tgz", + "integrity": "sha512-llxZosYCwH13p2GfPfhAinukdvAZYxWuwf5md107X80hsE8TQJj25unjqTwRKQ+w/wD+hpmBMziU8+K/WTitWQ==", + "license": "MIT", + "dependencies": { + "@lexical/html": "0.29.0", + "@lexical/list": "0.29.0", + "@lexical/selection": "0.29.0", + "@lexical/utils": "0.29.0", + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/code": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.29.0.tgz", + "integrity": "sha512-yKGzoKpyIO39Xf7OKLPpoCE5V8mTDCM3l3CDHZR3X1gM/VZQzf4jAiO3b06y9YkQ2fM8kqwchYu87wGvs8/iIQ==", + "license": "MIT", + "dependencies": { + "@lexical/utils": "0.29.0", + "lexical": "0.29.0", + "prismjs": "^1.30.0" + } + }, + "node_modules/@lexical/devtools-core": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.29.0.tgz", + "integrity": "sha512-uUq0m9ql/7mthp7Ho1vnG7Id6imQ5kD5mxUhX2lmgHretS+yAHGsGsGiPIVHdPWeVmUb2n4IVDJ+cJbUsUjQJw==", + "license": "MIT", + "dependencies": { + "@lexical/html": "0.29.0", + "@lexical/link": "0.29.0", + "@lexical/mark": "0.29.0", + "@lexical/table": "0.29.0", + "@lexical/utils": "0.29.0", + "lexical": "0.29.0" }, "peerDependencies": { - "react": ">=16" + "react": ">=17.x", + "react-dom": ">=17.x" } }, - "node_modules/@motionone/animation": { - "version": "10.17.0", - "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.17.0.tgz", - "integrity": "sha512-ANfIN9+iq1kGgsZxs+Nz96uiNcPLGTXwfNo2Xz/fcJXniPYpaz/Uyrfa+7I5BPLxCP82sh7quVDudf1GABqHbg==", + "node_modules/@lexical/dragon": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.29.0.tgz", + "integrity": "sha512-Zaky2jd/Pp1blAZqPeGNdyhxnVL4lwVjbWPxhfS1gbW4Q5CBQ3aD3B0T4ljiKfmRNJm004LJ9q7KjhlRbREvZA==", + "license": "MIT", "dependencies": { - "@motionone/easing": "^10.17.0", - "@motionone/types": "^10.17.0", - "@motionone/utils": "^10.17.0", - "tslib": "^2.3.1" + "lexical": "0.29.0" } }, - "node_modules/@motionone/dom": { - "version": "10.12.0", - "resolved": "https://registry.npmjs.org/@motionone/dom/-/dom-10.12.0.tgz", - "integrity": "sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw==", + "node_modules/@lexical/hashtag": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.29.0.tgz", + "integrity": "sha512-fa7s0Yi2RKz/GvgT5XU9fborx6VPU3VtvvEPaIXgyd6zXZRiOhD9rGypwB3oj4fMK1ndx2dX0m7SwhMJo48D8w==", + "license": "MIT", "dependencies": { - "@motionone/animation": "^10.12.0", - "@motionone/generators": "^10.12.0", - "@motionone/types": "^10.12.0", - "@motionone/utils": "^10.12.0", - "hey-listen": "^1.0.8", - "tslib": "^2.3.1" + "@lexical/utils": "0.29.0", + "lexical": "0.29.0" } }, - "node_modules/@motionone/easing": { - "version": "10.17.0", - "resolved": "https://registry.npmjs.org/@motionone/easing/-/easing-10.17.0.tgz", - "integrity": "sha512-Bxe2wSuLu/qxqW4rBFS5m9tMLOw+QBh8v5A7Z5k4Ul4sTj5jAOfZG5R0bn5ywmk+Fs92Ij1feZ5pmC4TeXA8Tg==", + "node_modules/@lexical/history": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.29.0.tgz", + "integrity": "sha512-OrCwZycp/yaq63mw511NutkwAB+W6WSchG1xTxlLh6nbc8jnbvKhCf4CGbnrvlhD7hTuzxJ8FI9/2M/2zv/mNQ==", + "license": "MIT", "dependencies": { - "@motionone/utils": "^10.17.0", - "tslib": "^2.3.1" + "@lexical/utils": "0.29.0", + "lexical": "0.29.0" } }, - "node_modules/@motionone/generators": { - "version": "10.17.0", - "resolved": "https://registry.npmjs.org/@motionone/generators/-/generators-10.17.0.tgz", - "integrity": "sha512-T6Uo5bDHrZWhIfxG/2Aut7qyWQyJIWehk6OB4qNvr/jwA/SRmixwbd7SOrxZi1z5rH3LIeFFBKK1xHnSbGPZSQ==", + "node_modules/@lexical/html": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.29.0.tgz", + "integrity": "sha512-+jV6ijppOpxpUGeXkGssXJbsAmFALfeLrgbM0xuZbxZ7RgYZ+5Atn00WjSno7+JV5EOuRkYmCNtS1tiHtXMY1g==", + "license": "MIT", "dependencies": { - "@motionone/types": "^10.17.0", - "@motionone/utils": "^10.17.0", - "tslib": "^2.3.1" + "@lexical/selection": "0.29.0", + "@lexical/utils": "0.29.0", + "lexical": "0.29.0" } }, - "node_modules/@motionone/types": { - "version": "10.17.0", - "resolved": "https://registry.npmjs.org/@motionone/types/-/types-10.17.0.tgz", - "integrity": "sha512-EgeeqOZVdRUTEHq95Z3t8Rsirc7chN5xFAPMYFobx8TPubkEfRSm5xihmMUkbaR2ErKJTUw3347QDPTHIW12IA==" - }, - "node_modules/@motionone/utils": { - "version": "10.17.0", - "resolved": "https://registry.npmjs.org/@motionone/utils/-/utils-10.17.0.tgz", - "integrity": "sha512-bGwrki4896apMWIj9yp5rAS2m0xyhxblg6gTB/leWDPt+pb410W8lYWsxyurX+DH+gO1zsQsfx2su/c1/LtTpg==", + "node_modules/@lexical/link": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.29.0.tgz", + "integrity": "sha512-wGbKRF0x/6ZQHuCfr8m8qD1J0R1kFmWINBG2A1hUXPDf7UY5qm/nS2oKNDGpjiDMGwkVZ7n7WfzeBGO+KRe/Lg==", + "license": "MIT", "dependencies": { - "@motionone/types": "^10.17.0", - "hey-listen": "^1.0.8", - "tslib": "^2.3.1" + "@lexical/utils": "0.29.0", + "lexical": "0.29.0" } }, - "node_modules/@ndelangen/get-tarball": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@ndelangen/get-tarball/-/get-tarball-3.0.9.tgz", - "integrity": "sha512-9JKTEik4vq+yGosHYhZ1tiH/3WpUS0Nh0kej4Agndhox8pAdWhEx5knFVRcb/ya9knCRCs1rPxNrSXTDdfVqpA==", - "dev": true, + "node_modules/@lexical/list": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.29.0.tgz", + "integrity": "sha512-sWiof+i2ff8rL7KxJ3dxHLwyJfX423e1EVLmAdQEOPhyZJiNbeLTSNhNGsZ8FjFoBwvTTEDwuQZm3iT3hliKOg==", + "license": "MIT", "dependencies": { - "gunzip-maybe": "^1.4.2", - "pump": "^3.0.0", - "tar-fs": "^2.1.1" + "@lexical/selection": "0.29.0", + "@lexical/utils": "0.29.0", + "lexical": "0.29.0" } }, - "node_modules/@ndelangen/get-tarball/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true - }, - "node_modules/@ndelangen/get-tarball/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, + "node_modules/@lexical/mark": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.29.0.tgz", + "integrity": "sha512-UB3x6pyUdpZHRqF4tiajLnC1+Umvt7x8Rkkdi29aNNvzIWniVwGkBOlmvFus7x+4dOV1D1fydwiP4m38nGgLDw==", + "license": "MIT", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "@lexical/utils": "0.29.0", + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/markdown": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.29.0.tgz", + "integrity": "sha512-4Od8WoDoviv9DxJZVgrIORTIAzyoGOpztbGbIBXguGmwvy7NnHQDh9fZYIYRrdI1Awp1VVGdJ3ku/7KTgSOoRw==", + "license": "MIT", + "dependencies": { + "@lexical/code": "0.29.0", + "@lexical/link": "0.29.0", + "@lexical/list": "0.29.0", + "@lexical/rich-text": "0.29.0", + "@lexical/text": "0.29.0", + "@lexical/utils": "0.29.0", + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/offset": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.29.0.tgz", + "integrity": "sha512-VyD2Ff3rBJpo++Fxvi3MNYmDELa+9nA0EgXqGRNb3MvRehRjHbaDbymtLMMHIwvbkF5lnra+ubStcTRQmoQxXw==", + "license": "MIT", + "dependencies": { + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/overflow": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.29.0.tgz", + "integrity": "sha512-IzH3M652Ej2gB2sK65N3yTgyiQAa3I3tqKbSnBRiXu/+isxHoCy/qRr9/kL63uy7zhGvgV+EYsoffQCawIFt8Q==", + "license": "MIT", + "dependencies": { + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/plain-text": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.29.0.tgz", + "integrity": "sha512-F5C3meDb2HmO0NmKJBVRkjmX9PNln6O1jXU/APJuSFBdvfcIWSY58ncHR4zy2M5LF1Q5PQMWyIay9p+SqOtY5A==", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.29.0", + "@lexical/selection": "0.29.0", + "@lexical/utils": "0.29.0", + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/react": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.29.0.tgz", + "integrity": "sha512-YMlnljW/jxmwSzsRv5UPatfOoMZXqxFmRIEltTUIQfrOFdqn+ssUtCpjE6xRD1oxD6KpSIekakzLs+y/8+7CuQ==", + "license": "MIT", + "dependencies": { + "@lexical/devtools-core": "0.29.0", + "@lexical/dragon": "0.29.0", + "@lexical/hashtag": "0.29.0", + "@lexical/history": "0.29.0", + "@lexical/link": "0.29.0", + "@lexical/list": "0.29.0", + "@lexical/mark": "0.29.0", + "@lexical/markdown": "0.29.0", + "@lexical/overflow": "0.29.0", + "@lexical/plain-text": "0.29.0", + "@lexical/rich-text": "0.29.0", + "@lexical/table": "0.29.0", + "@lexical/text": "0.29.0", + "@lexical/utils": "0.29.0", + "@lexical/yjs": "0.29.0", + "lexical": "0.29.0", + "react-error-boundary": "^3.1.4" }, - "engines": { - "node": ">= 6" + "peerDependencies": { + "react": ">=17.x", + "react-dom": ">=17.x" } }, - "node_modules/@ndelangen/get-tarball/node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "dev": true, + "node_modules/@lexical/rich-text": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.29.0.tgz", + "integrity": "sha512-fSKgXGxJUOWo7dwSTUYFVBNNk4pPN8norsZfdmKM1kGDS1/GKuVzlzHLKZ7rQb8RLD5a43p4ifEL+28P+q0Qqg==", + "license": "MIT", "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" + "@lexical/clipboard": "0.29.0", + "@lexical/selection": "0.29.0", + "@lexical/utils": "0.29.0", + "lexical": "0.29.0" } }, - "node_modules/@ndelangen/get-tarball/node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, + "node_modules/@lexical/selection": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.29.0.tgz", + "integrity": "sha512-lX9CRrXgKte65cozTHFXwUJ2fvZD92OEtos+YU+U40GJjf3NdheGeKDxDfOpF4AXrYRSszY7E0CzmIvuEs0p4A==", + "license": "MIT", "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/table": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.29.0.tgz", + "integrity": "sha512-Jdj32kBDeJh/0dGaZB14JggnEIS956/cN7grnLr7cmhhVzDicvLMBENSXQVEJAQVcSIU4G9EvxC7GJZ9VgqDnA==", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.29.0", + "@lexical/utils": "0.29.0", + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/text": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.29.0.tgz", + "integrity": "sha512-QnNGr6ickTLk76o3PdxJjPwt//dpuh8idVfR73WdCIoAwkhiEPUxxTZERoMsudXj6O/lJ+/HhI61wVjLckYr3A==", + "license": "MIT", + "dependencies": { + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/utils": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.29.0.tgz", + "integrity": "sha512-y2hhWQDjcXdplsAaQMuZx6ht9u1I4BV5NynA+WKoQ3h8vKxzeDnpCxVOK/zxU1R5dhM/nilnFu7uhvrSeEn+TQ==", + "license": "MIT", + "dependencies": { + "@lexical/list": "0.29.0", + "@lexical/selection": "0.29.0", + "@lexical/table": "0.29.0", + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/yjs": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.29.0.tgz", + "integrity": "sha512-6IXWWlGkVJEzWP/+LcuKYJ9jmcFp8k7TT/jmz4V5gBD9Ut3swOGsIA/sQCtB9y7jad10csaDVmFdFzGNWKVH9A==", + "license": "MIT", + "dependencies": { + "@lexical/offset": "0.29.0", + "@lexical/selection": "0.29.0", + "lexical": "0.29.0" }, - "engines": { - "node": ">=6" + "peerDependencies": { + "yjs": ">=13.5.22" } }, - "node_modules/@next/env": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-12.3.4.tgz", - "integrity": "sha512-H/69Lc5Q02dq3o+dxxy5O/oNxFsZpdL6WREtOOtOM1B/weonIwDXkekr1KV5DPVPr12IHFPrMrcJQ6bgPMfn7A==" - }, - "node_modules/@next/eslint-plugin-next": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.5.6.tgz", - "integrity": "sha512-ng7pU/DDsxPgT6ZPvuprxrkeew3XaRf4LAT4FabaEO/hAbvVx4P7wqnqdbTdDn1kgTvsI4tpIgT4Awn/m0bGbg==", - "dev": true, + "node_modules/@lottiefiles/dotlottie-react": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.12.0.tgz", + "integrity": "sha512-33Tsd67vlotrm43R8oko30Krwyuqb0YdOLra7L5m2mVfrTvDPEDBt8jfjgiveoLJ0jG/FlTLiCPXi/vCaBSXrg==", + "license": "MIT", "dependencies": { - "glob": "7.1.7" - } - }, - "node_modules/@next/eslint-plugin-next/node_modules/glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "@lottiefiles/dotlottie-web": "0.38.2" }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "peerDependencies": { + "react": "^17 || ^18 || ^19" } }, - "node_modules/@next/swc-android-arm-eabi": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.4.tgz", - "integrity": "sha512-cM42Cw6V4Bz/2+j/xIzO8nK/Q3Ly+VSlZJTa1vHzsocJRYz8KT6MrreXaci2++SIZCF1rVRCDgAg5PpqRibdIA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-android-arm64": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.3.4.tgz", - "integrity": "sha512-5jf0dTBjL+rabWjGj3eghpLUxCukRhBcEJgwLedewEA/LJk2HyqCvGIwj5rH+iwmq1llCWbOky2dO3pVljrapg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.4.tgz", - "integrity": "sha512-DqsSTd3FRjQUR6ao0E1e2OlOcrF5br+uegcEGPVonKYJpcr0MJrtYmPxd4v5T6UCJZ+XzydF7eQo5wdGvSZAyA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.4.tgz", - "integrity": "sha512-PPF7tbWD4k0dJ2EcUSnOsaOJ5rhT3rlEt/3LhZUGiYNL8KvoqczFrETlUx0cUYaXe11dRA3F80Hpt727QIwByQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-freebsd-x64": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.4.tgz", - "integrity": "sha512-KM9JXRXi/U2PUM928z7l4tnfQ9u8bTco/jb939pdFUHqc28V43Ohd31MmZD1QzEK4aFlMRaIBQOWQZh4D/E5lQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm-gnueabihf": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.4.tgz", - "integrity": "sha512-3zqD3pO+z5CZyxtKDTnOJ2XgFFRUBciOox6EWkoZvJfc9zcidNAQxuwonUeNts6Xbm8Wtm5YGIRC0x+12YH7kw==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.4.tgz", - "integrity": "sha512-kiX0vgJGMZVv+oo1QuObaYulXNvdH/IINmvdZnVzMO/jic/B8EEIGlZ8Bgvw8LCjH3zNVPO3mGrdMvnEEPEhKA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.4.tgz", - "integrity": "sha512-EETZPa1juczrKLWk5okoW2hv7D7WvonU+Cf2CgsSoxgsYbUCZ1voOpL4JZTOb6IbKMDo6ja+SbY0vzXZBUMvkQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.4.tgz", - "integrity": "sha512-4csPbRbfZbuWOk3ATyWcvVFdD9/Rsdq5YHKvRuEni68OCLkfy4f+4I9OBpyK1SKJ00Cih16NJbHE+k+ljPPpag==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.4.tgz", - "integrity": "sha512-YeBmI+63Ro75SUiL/QXEVXQ19T++58aI/IINOyhpsRL1LKdyfK/35iilraZEFz9bLQrwy1LYAR5lK200A9Gjbg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.4.tgz", - "integrity": "sha512-Sd0qFUJv8Tj0PukAYbCCDbmXcMkbIuhnTeHm9m4ZGjCf6kt7E/RMs55Pd3R5ePjOkN7dJEuxYBehawTR/aPDSQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.4.tgz", - "integrity": "sha512-rt/vv/vg/ZGGkrkKcuJ0LyliRdbskQU+91bje+PgoYmxTZf/tYs6IfbmgudBJk6gH3QnjHWbkphDdRQrseRefQ==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.4.tgz", - "integrity": "sha512-DQ20JEfTBZAgF8QCjYfJhv2/279M6onxFjdG/+5B0Cyj00/EdBxiWb2eGGFgQhrBbNv/lsvzFbbi0Ptf8Vw/bg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } + "node_modules/@lottiefiles/dotlottie-web": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-web/-/dotlottie-web-0.38.2.tgz", + "integrity": "sha512-01d+UjJ8NG7ZStYQxtb8FPzknzGmauG7gEkcH+wHfSdiSQJY9PoBNVSTB9V6F5hAnmFqOxaocTtd7TIEEnzMnA==", + "license": "MIT" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -4399,6 +1863,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -4408,6 +1873,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -4416,167 +1882,175 @@ "node": ">= 8" } }, - "node_modules/@octokit/auth-token": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.4.tgz", - "integrity": "sha512-TWFX7cZF2LXoCvdmJWY7XVPi74aSY0+FfBZNSXEXFkMpjcqsQwDSYVv5FhRFaI0V1ECnwbz4j59T/G+rXNWaIQ==", + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 14" + "node": ">=12.4.0" + } + }, + "node_modules/@octokit/auth-token": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz", + "integrity": "sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==", + "license": "MIT", + "engines": { + "node": ">= 18" } }, "node_modules/@octokit/core": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.2.4.tgz", - "integrity": "sha512-rYKilwgzQ7/imScn3M9/pFfUf4I1AZEH3KhyJmtPdE2zfaXAn2mFfUy4FbKewzc2We5y/LlKLj36fWJLKC2SIQ==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz", + "integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==", + "license": "MIT", "dependencies": { - "@octokit/auth-token": "^3.0.0", - "@octokit/graphql": "^5.0.0", - "@octokit/request": "^6.0.0", - "@octokit/request-error": "^3.0.0", - "@octokit/types": "^9.0.0", - "before-after-hook": "^2.2.0", - "universal-user-agent": "^6.0.0" + "@octokit/auth-token": "^5.0.0", + "@octokit/graphql": "^8.0.0", + "@octokit/request": "^9.0.0", + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^3.0.2", + "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 14" + "node": ">= 18" } }, "node_modules/@octokit/endpoint": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.6.tgz", - "integrity": "sha512-5L4fseVRUsDFGR00tMWD/Trdeeihn999rTMGRMC1G/Ldi1uWlWJzI98H4Iak5DB/RVvQuyMYKqSK/R6mbSOQyg==", + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.3.tgz", + "integrity": "sha512-nBRBMpKPhQUxCsQQeW+rCJ/OPSMcj3g0nfHn01zGYZXuNDvvXudF/TYY6APj5THlurerpFN4a/dQAIAaM6BYhA==", + "license": "MIT", "dependencies": { - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/types": "^13.6.2", + "universal-user-agent": "^7.0.2" }, "engines": { - "node": ">= 14" + "node": ">= 18" } }, "node_modules/@octokit/graphql": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.6.tgz", - "integrity": "sha512-Fxyxdy/JH0MnIB5h+UQ3yCoh1FG4kWXfFKkpWqjZHw/p+Kc8Y44Hu/kCgNBT6nU1shNumEchmW/sUO1JuQnPcw==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz", + "integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==", + "license": "MIT", "dependencies": { - "@octokit/request": "^6.0.0", - "@octokit/types": "^9.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/request": "^9.0.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 14" + "node": ">= 18" } }, "node_modules/@octokit/openapi-types": { - "version": "18.1.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", - "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==" + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" }, "node_modules/@octokit/plugin-paginate-rest": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-6.1.2.tgz", - "integrity": "sha512-qhrmtQeHU/IivxucOV1bbI/xZyC/iOBhclokv7Sut5vnejAIAEXVcGQeRpQlU39E0WwK9lNvJHphHri/DB6lbQ==", + "version": "11.6.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.6.0.tgz", + "integrity": "sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==", + "license": "MIT", "dependencies": { - "@octokit/tsconfig": "^1.0.2", - "@octokit/types": "^9.2.3" + "@octokit/types": "^13.10.0" }, "engines": { - "node": ">= 14" + "node": ">= 18" }, "peerDependencies": { - "@octokit/core": ">=4" + "@octokit/core": ">=6" } }, "node_modules/@octokit/plugin-request-log": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", - "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz", + "integrity": "sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, "peerDependencies": { - "@octokit/core": ">=3" + "@octokit/core": ">=6" } }, "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-7.2.3.tgz", - "integrity": "sha512-I5Gml6kTAkzVlN7KCtjOM+Ruwe/rQppp0QU372K1GP7kNOYEKe8Xn5BW4sE62JAHdwpq95OQK/qGNyKQMUzVgA==", + "version": "13.2.6", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.2.6.tgz", + "integrity": "sha512-wMsdyHMjSfKjGINkdGKki06VEkgdEldIGstIEyGX0wbYHGByOwN/KiM+hAAlUwAtPkP3gvXtVQA9L3ITdV2tVw==", + "license": "MIT", "dependencies": { - "@octokit/types": "^10.0.0" + "@octokit/types": "^13.6.1" }, "engines": { - "node": ">= 14" + "node": ">= 18" }, "peerDependencies": { - "@octokit/core": ">=3" - } - }, - "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-10.0.0.tgz", - "integrity": "sha512-Vm8IddVmhCgU1fxC1eyinpwqzXPEYu0NrYzD3YZjlGjyftdLBTeqNblRC0jmJmgxbJIsQlyogVeGnrNaaMVzIg==", - "dependencies": { - "@octokit/openapi-types": "^18.0.0" + "@octokit/core": ">=6" } }, "node_modules/@octokit/request": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.8.tgz", - "integrity": "sha512-ow4+pkVQ+6XVVsekSYBzJC0VTVvh/FCTUUgTsboGq+DTeWdyIFV8WSCdo0RIxk6wSkBTHqIK1mYuY7nOBXOchw==", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.2.tgz", + "integrity": "sha512-dZl0ZHx6gOQGcffgm1/Sf6JfEpmh34v3Af2Uci02vzUYz6qEN6zepoRtmybWXIGXFIK8K9ylE3b+duCWqhArtg==", + "license": "MIT", "dependencies": { - "@octokit/endpoint": "^7.0.0", - "@octokit/request-error": "^3.0.0", - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", - "universal-user-agent": "^6.0.0" + "@octokit/endpoint": "^10.1.3", + "@octokit/request-error": "^6.1.7", + "@octokit/types": "^13.6.2", + "fast-content-type-parse": "^2.0.0", + "universal-user-agent": "^7.0.2" }, "engines": { - "node": ">= 14" + "node": ">= 18" } }, "node_modules/@octokit/request-error": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz", - "integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==", + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.7.tgz", + "integrity": "sha512-69NIppAwaauwZv6aOzb+VVLwt+0havz9GT5YplkeJv7fG7a40qpLt/yZKyiDxAhgz0EtgNdNcb96Z0u+Zyuy2g==", + "license": "MIT", "dependencies": { - "@octokit/types": "^9.0.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" + "@octokit/types": "^13.6.2" }, "engines": { - "node": ">= 14" + "node": ">= 18" } }, "node_modules/@octokit/rest": { - "version": "19.0.13", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-19.0.13.tgz", - "integrity": "sha512-/EzVox5V9gYGdbAI+ovYj3nXQT1TtTHRT+0eZPcuC05UFSWO3mdO9UY1C0i2eLF9Un1ONJkAk+IEtYGAC+TahA==", + "version": "21.0.2", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.0.2.tgz", + "integrity": "sha512-+CiLisCoyWmYicH25y1cDfCrv41kRSvTq6pPWtRroRJzhsCZWZyCqGyI8foJT5LmScADSwRAnr/xo+eewL04wQ==", + "license": "MIT", "dependencies": { - "@octokit/core": "^4.2.1", - "@octokit/plugin-paginate-rest": "^6.1.2", - "@octokit/plugin-request-log": "^1.0.4", - "@octokit/plugin-rest-endpoint-methods": "^7.1.2" + "@octokit/core": "^6.1.2", + "@octokit/plugin-paginate-rest": "^11.0.0", + "@octokit/plugin-request-log": "^5.3.1", + "@octokit/plugin-rest-endpoint-methods": "^13.0.0" }, "engines": { - "node": ">= 14" + "node": ">= 18" } }, - "node_modules/@octokit/tsconfig": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@octokit/tsconfig/-/tsconfig-1.0.2.tgz", - "integrity": "sha512-I0vDR0rdtP8p2lGMzvsJzbhdOWy405HcGovrspJ8RRibHnyRgggUSNO5AIox5LmqiwmatHKYsvj6VGFHkqS7lA==" - }, "node_modules/@octokit/types": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz", - "integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==", + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^18.0.0" + "@octokit/openapi-types": "^24.2.0" } }, "node_modules/@peculiar/asn1-cms": { "version": "2.3.13", "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.3.13.tgz", "integrity": "sha512-joqu8A7KR2G85oLPq+vB+NFr2ro7Ls4ol13Zcse/giPSzUNN0n2k3v8kMpf6QdGUhI13e5SzQYN8AKP8sJ8v4w==", + "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.3.13", "@peculiar/asn1-x509": "^2.3.13", @@ -4589,6 +2063,7 @@ "version": "2.3.13", "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.3.13.tgz", "integrity": "sha512-+JtFsOUWCw4zDpxp1LbeTYBnZLlGVOWmHHEhoFdjM5yn4wCn+JiYQ8mghOi36M2f6TPQ17PmhNL6/JfNh7/jCA==", + "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.3.13", "@peculiar/asn1-x509": "^2.3.13", @@ -4597,9 +2072,10 @@ } }, "node_modules/@peculiar/asn1-ecc": { - "version": "2.3.13", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.3.13.tgz", - "integrity": "sha512-3dF2pQcrN/WJEMq+9qWLQ0gqtn1G81J4rYqFl6El6QV367b4IuhcRv+yMA84tNNyHOJn9anLXV5radnpPiG3iA==", + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.3.14.tgz", + "integrity": "sha512-zWPyI7QZto6rnLv6zPniTqbGaLh6zBpJyI46r1yS/bVHJXT2amdMHCRRnbV5yst2H8+ppXG6uXu/M6lKakiQ8w==", + "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.3.13", "@peculiar/asn1-x509": "^2.3.13", @@ -4611,6 +2087,7 @@ "version": "2.3.13", "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.3.13.tgz", "integrity": "sha512-fypYxjn16BW+5XbFoY11Rm8LhZf6euqX/C7BTYpqVvLem1GvRl7A+Ro1bO/UPwJL0z+1mbvXEnkG0YOwbwz2LA==", + "license": "MIT", "dependencies": { "@peculiar/asn1-cms": "^2.3.13", "@peculiar/asn1-pkcs8": "^2.3.13", @@ -4624,6 +2101,7 @@ "version": "2.3.13", "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.3.13.tgz", "integrity": "sha512-VP3PQzbeSSjPjKET5K37pxyf2qCdM0dz3DJ56ZCsol3FqAXGekb4sDcpoL9uTLGxAh975WcdvUms9UcdZTuGyQ==", + "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.3.13", "@peculiar/asn1-x509": "^2.3.13", @@ -4635,6 +2113,7 @@ "version": "2.3.13", "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.3.13.tgz", "integrity": "sha512-rIwQXmHpTo/dgPiWqUgby8Fnq6p1xTJbRMxCiMCk833kQCeZrC5lbSKg6NDnJTnX2kC6IbXBB9yCS2C73U2gJg==", + "license": "MIT", "dependencies": { "@peculiar/asn1-cms": "^2.3.13", "@peculiar/asn1-pfx": "^2.3.13", @@ -4650,6 +2129,7 @@ "version": "2.3.13", "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.3.13.tgz", "integrity": "sha512-wBNQqCyRtmqvXkGkL4DR3WxZhHy8fDiYtOjTeCd7SFE5F6GBeafw3EJ94PX/V0OJJrjQ40SkRY2IZu3ZSyBqcg==", + "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.3.13", "@peculiar/asn1-x509": "^2.3.13", @@ -4661,6 +2141,7 @@ "version": "2.3.13", "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.13.tgz", "integrity": "sha512-3Xq3a01WkHRZL8X04Zsfg//mGaA21xlL4tlVn4v2xGT0JStiztATRkMwa5b+f/HXmY2smsiLXYK46Gwgzvfg3g==", + "license": "MIT", "dependencies": { "asn1js": "^3.0.5", "pvtsutils": "^1.3.5", @@ -4671,6 +2152,7 @@ "version": "2.3.13", "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.3.13.tgz", "integrity": "sha512-PfeLQl2skXmxX2/AFFCVaWU8U6FKW1Db43mgBhShCOFS1bVxqtvusq1hVjfuEcuSQGedrLdCSvTgabluwN/M9A==", + "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.3.13", "asn1js": "^3.0.5", @@ -4683,6 +2165,7 @@ "version": "2.3.13", "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.3.13.tgz", "integrity": "sha512-WpEos6CcnUzJ6o2Qb68Z7Dz5rSjRGv/DtXITCNBtjZIRWRV12yFVci76SVfOX8sisL61QWMhpLKQibrG8pi2Pw==", + "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.3.13", "@peculiar/asn1-x509": "^2.3.13", @@ -4690,29 +2173,22 @@ "tslib": "^2.6.2" } }, - "node_modules/@peculiar/asn1-x509/node_modules/ipaddr.js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", - "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", - "engines": { - "node": ">= 10" - } - }, "node_modules/@peculiar/x509": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.11.0.tgz", - "integrity": "sha512-8rdxE//tsWLb2Yo2TYO2P8gieStbrHK/huFMV5PPfwX8I5HmtOus+Ox6nTKrPA9o+WOPaa5xKenee+QdmHBd5g==", + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.12.3.tgz", + "integrity": "sha512-+Mzq+W7cNEKfkNZzyLl6A6ffqc3r21HGZUezgfKxpZrkORfOqgRXnS80Zu0IV6a9Ue9QBJeKD7kN0iWfc3bhRQ==", + "license": "MIT", "dependencies": { - "@peculiar/asn1-cms": "^2.3.8", - "@peculiar/asn1-csr": "^2.3.8", - "@peculiar/asn1-ecc": "^2.3.8", - "@peculiar/asn1-pkcs9": "^2.3.8", - "@peculiar/asn1-rsa": "^2.3.8", - "@peculiar/asn1-schema": "^2.3.8", - "@peculiar/asn1-x509": "^2.3.8", + "@peculiar/asn1-cms": "^2.3.13", + "@peculiar/asn1-csr": "^2.3.13", + "@peculiar/asn1-ecc": "^2.3.14", + "@peculiar/asn1-pkcs9": "^2.3.13", + "@peculiar/asn1-rsa": "^2.3.13", + "@peculiar/asn1-schema": "^2.3.13", + "@peculiar/asn1-x509": "^2.3.13", "pvtsutils": "^1.3.5", "reflect-metadata": "^0.2.2", - "tslib": "^2.6.2", + "tslib": "^2.7.0", "tsyringe": "^4.8.0" } }, @@ -4721,121 +2197,58 @@ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">=14" } }, - "node_modules/@pmmmwh/react-refresh-webpack-plugin": { - "version": "0.5.11", - "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.11.tgz", - "integrity": "sha512-7j/6vdTym0+qZ6u4XbSAxrWBGYSdCfTzySkj7WAFgDLmSyWlOrWvpyzxlFh5jtw9dn0oL/jtW+06XfFiisN3JQ==", + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", "dev": true, - "dependencies": { - "ansi-html-community": "^0.0.8", - "common-path-prefix": "^3.0.0", - "core-js-pure": "^3.23.3", - "error-stack-parser": "^2.0.6", - "find-up": "^5.0.0", - "html-entities": "^2.1.0", - "loader-utils": "^2.0.4", - "schema-utils": "^3.0.0", - "source-map": "^0.7.3" - }, + "license": "MIT", "engines": { - "node": ">= 10.13" + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, - "peerDependencies": { - "@types/webpack": "4.x || 5.x", - "react-refresh": ">=0.10.0 <1.0.0", - "sockjs-client": "^1.4.0", - "type-fest": ">=0.17.0 <5.0.0", - "webpack": ">=4.43.0 <6.0.0", - "webpack-dev-server": "3.x || 4.x", - "webpack-hot-middleware": "2.x", - "webpack-plugin-serve": "0.x || 1.x" - }, - "peerDependenciesMeta": { - "@types/webpack": { - "optional": true - }, - "sockjs-client": { - "optional": true - }, - "type-fest": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - }, - "webpack-hot-middleware": { - "optional": true - }, - "webpack-plugin-serve": { - "optional": true - } - } - }, - "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dev": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true, - "engines": { - "node": ">= 8" + "funding": { + "url": "https://opencollective.com/unts" } }, "node_modules/@radix-ui/number": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", - "integrity": "sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==", - "dependencies": { - "@babel/runtime": "^7.13.10" - } + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", + "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==", + "license": "MIT" }, "node_modules/@radix-ui/primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", - "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", - "dependencies": { - "@babel/runtime": "^7.13.10" - } + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", + "license": "MIT" }, "node_modules/@radix-ui/react-accordion": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.1.2.tgz", - "integrity": "sha512-fDG7jcoNKVjSK6yfmuAs0EnPDro0WMXIhMtXdTBWqEioVW206ku+4Lw07e+13lUkFkpoEQ2PdeMIAGpdqEAmDg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.2.tgz", + "integrity": "sha512-b1oh54x4DMCdGsB4/7ahiSrViXxaBwRPotiZNnYXjLha9vfuURSAZErki6qjDoSIV0eXx5v57XnTGVtGwnfp2g==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collapsible": "1.0.3", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1" + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collapsible": "1.1.2", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -4847,23 +2260,23 @@ } }, "node_modules/@radix-ui/react-alert-dialog": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.0.5.tgz", - "integrity": "sha512-OrVIOcZL0tl6xibeuGt5/+UxoT2N27KCFOPjFyfXMnchxSHZ/OW7cCX2nGlIYJrbHK/fczPcFzAwvNBB6XBNMA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.3.tgz", + "integrity": "sha512-5xzWppXTNZe6zFrTTwAJIoMJeZmdFe0l8ZqQrPGKAVvhdyOWR4r53/G7SZqx6/uf1J441oxK7GzmTkrrWDroHA==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dialog": "1.0.5", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2" + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dialog": "1.1.3", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -4875,18 +2288,18 @@ } }, "node_modules/@radix-ui/react-arrow": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz", - "integrity": "sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz", + "integrity": "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" + "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -4898,25 +2311,25 @@ } }, "node_modules/@radix-ui/react-checkbox": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz", - "integrity": "sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.3.tgz", + "integrity": "sha512-HD7/ocp8f1B3e6OHygH0n7ZKjONkhciy1Nh0yuBgObqThc3oyx+vuMfFHKAknXRHHWVE9XvXStxJFyjUmB8PIw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-previous": "1.0.1", - "@radix-ui/react-use-size": "1.0.1" + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -4928,26 +2341,25 @@ } }, "node_modules/@radix-ui/react-collapsible": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz", - "integrity": "sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.2.tgz", + "integrity": "sha512-PliMB63vxz7vggcyq0IxNYk8vGDrLXVWw4+W4B8YnwI1s18x7YZYqlG9PLX7XxAJUi0g2DxP4XKJMFHh/iVh9A==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1" + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -4959,21 +2371,21 @@ } }, "node_modules/@radix-ui/react-collection": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", - "integrity": "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz", + "integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2" + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -4985,15 +2397,13 @@ } }, "node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", - "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "license": "MIT", "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -5002,15 +2412,13 @@ } }, "node_modules/@radix-ui/react-context": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", - "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "license": "MIT", "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -5019,31 +2427,31 @@ } }, "node_modules/@radix-ui/react-dialog": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", - "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.3.tgz", + "integrity": "sha512-ujGvqQNkZ0J7caQyl8XuZRj2/TIrYcOGwqz5TeD1OMcCdfBuEMP0D12ve+8J5F9XuNUth3FAKFWo/wt0E/GJrQ==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.4", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.2", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" + "react-remove-scroll": "2.6.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -5055,15 +2463,13 @@ } }, "node_modules/@radix-ui/react-direction": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", - "integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "license": "MIT", "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -5072,22 +2478,22 @@ } }, "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", - "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.2.tgz", + "integrity": "sha512-kEHnlhv7wUggvhuJPkyw4qspXLJOdYoAP4dO2c8ngGuXTq1w/HZp1YeVB+NQ2KbH1iEG+pvOCGYSqh9HZOz6hg==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-escape-keydown": "1.0.3" + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -5099,24 +2505,24 @@ } }, "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz", - "integrity": "sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.3.tgz", + "integrity": "sha512-eKyAfA9e4HOavzyGJC6kiDIlHMPzAU0zqSqTg+VwS0Okvb9nkTo7L4TugkCUqM3I06ciSpdtYQ73cgB7tyUgVw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-menu": "2.0.6", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1" + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-menu": "2.1.3", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -5128,15 +2534,13 @@ } }, "node_modules/@radix-ui/react-focus-guards": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", - "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "license": "MIT", "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -5145,20 +2549,20 @@ } }, "node_modules/@radix-ui/react-focus-scope": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", - "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz", + "integrity": "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1" + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -5170,26 +2574,26 @@ } }, "node_modules/@radix-ui/react-hover-card": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.0.7.tgz", - "integrity": "sha512-OcUN2FU0YpmajD/qkph3XzMcK/NmSk9hGWnjV68p6QiZMgILugusgQwnLSDs3oFSJYGKf3Y49zgFedhGh04k9A==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.3.tgz", + "integrity": "sha512-D+o67Fd7fjkW10ycdsse1sYuGV9dNQKOhoVii7ksSfUYqQiTPxz9bP/Vu1g6huJ1651/2j8q7JGGWSIBIuGO1Q==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-popper": "1.1.3", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1" + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.2", + "@radix-ui/react-popper": "1.2.1", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -5201,16 +2605,16 @@ } }, "node_modules/@radix-ui/react-id": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", - "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" + "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -5219,18 +2623,18 @@ } }, "node_modules/@radix-ui/react-label": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.2.tgz", - "integrity": "sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.1.tgz", + "integrity": "sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" + "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -5242,35 +2646,35 @@ } }, "node_modules/@radix-ui/react-menu": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.6.tgz", - "integrity": "sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.3.tgz", + "integrity": "sha512-wY5SY6yCiJYP+DMIy7RrjF4shoFpB9LJltliVwejBm8T2yepWDJgKBhIFYOGWYR/lFHOCtbstN9duZFu6gmveQ==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.4", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.3", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-roving-focus": "1.0.4", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.2", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.1", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-roving-focus": "1.1.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-callback-ref": "1.1.0", "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" + "react-remove-scroll": "2.6.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -5282,32 +2686,32 @@ } }, "node_modules/@radix-ui/react-popover": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.7.tgz", - "integrity": "sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.3.tgz", + "integrity": "sha512-MBDKFwRe6fi0LT8m/Jl4V8J3WbS/UfXJtsgg8Ym5w5AyPG3XfHH4zhBp1P8HmZK83T8J7UzVm6/JpDE3WMl1Dw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.4", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.3", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.2", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.1", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" + "react-remove-scroll": "2.6.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -5319,27 +2723,27 @@ } }, "node_modules/@radix-ui/react-popper": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz", - "integrity": "sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz", + "integrity": "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-rect": "1.0.1", - "@radix-ui/react-use-size": "1.0.1", - "@radix-ui/rect": "1.0.1" + "@radix-ui/react-arrow": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -5351,18 +2755,19 @@ } }, "node_modules/@radix-ui/react-portal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", - "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz", + "integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -5374,19 +2779,19 @@ } }, "node_modules/@radix-ui/react-presence": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", - "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1" + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -5398,18 +2803,18 @@ } }, "node_modules/@radix-ui/react-primitive": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", - "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.2" + "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -5421,19 +2826,19 @@ } }, "node_modules/@radix-ui/react-progress": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.0.3.tgz", - "integrity": "sha512-5G6Om/tYSxjSeEdrb1VfKkfZfn/1IlPWd731h2RfPuSbIfNUgfqAwbKfJCg/PP6nuUCTrYzalwHSpSinoWoCag==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.1.tgz", + "integrity": "sha512-6diOawA84f/eMxFHcWut0aE1C2kyE9dOyCTQOMRR2C/qPiXz/X0SaiA/RLbapQaXUCmy0/hLMf9meSccD1N0pA==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3" + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -5445,27 +2850,27 @@ } }, "node_modules/@radix-ui/react-radio-group": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz", - "integrity": "sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.2.2.tgz", + "integrity": "sha512-E0MLLGfOP0l8P/NxgVzfXJ8w3Ch8cdO6UDzJfDChu4EJDy+/WdO5LqpdY8PYnCErkmZH3gZhDL1K7kQ41fAHuQ==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-roving-focus": "1.0.4", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-previous": "1.0.1", - "@radix-ui/react-use-size": "1.0.1" + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-roving-focus": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -5477,26 +2882,26 @@ } }, "node_modules/@radix-ui/react-roving-focus": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", - "integrity": "sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz", + "integrity": "sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1" + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -5508,62 +2913,38 @@ } }, "node_modules/@radix-ui/react-select": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.0.0.tgz", - "integrity": "sha512-RH5b7af4oHtkcHS7pG6Sgv5rk5Wxa7XI8W5gvB1N/yiuDGZxko1ynvOiVhFM7Cis2A8zxF9bTOUVbRDzPepe6w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.3.tgz", + "integrity": "sha512-tlLwaewTfrKetiex8iW9wwME/qrYlzlH0qcgYmos7xS54MO00SiPHasLoAykg/yVrjf41GQptPPi4oXzrP+sgg==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/number": "1.0.1", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.4", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.3", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-previous": "1.0.1", - "@radix-ui/react-visually-hidden": "1.0.3", + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.2", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.1", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.1", "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" + "react-remove-scroll": "2.6.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-separator": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.0.3.tgz", - "integrity": "sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -5575,16 +2956,16 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" + "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -5593,24 +2974,24 @@ } }, "node_modules/@radix-ui/react-switch": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.0.3.tgz", - "integrity": "sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.2.tgz", + "integrity": "sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-previous": "1.0.1", - "@radix-ui/react-use-size": "1.0.1" + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -5622,25 +3003,25 @@ } }, "node_modules/@radix-ui/react-tabs": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz", - "integrity": "sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.2.tgz", + "integrity": "sha512-9u/tQJMcC2aGq7KXpGivMm1mgq7oRJKXphDwdypPd/j21j/2znamPU8WkXgnhUaTrSFNIt8XhOyCAupg8/GbwQ==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-roving-focus": "1.0.4", - "@radix-ui/react-use-controllable-state": "1.0.1" + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-roving-focus": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -5652,115 +3033,29 @@ } }, "node_modules/@radix-ui/react-toast": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.5.tgz", - "integrity": "sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.3.tgz", + "integrity": "sha512-oB8irs7CGAml6zWbum7MNySTH/sR7PM1ZQyLV8reO946u73sU83yZUKijrMLNbm4hTOrJY4tE8Oa/XUKrOr2Wg==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-visually-hidden": "1.0.3" + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.2", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.0.3.tgz", - "integrity": "sha512-Pkqg3+Bc98ftZGsl60CLANXQBBQ4W3mTFS9EJvNxKMZ7magklKV69/id1mlAlOFDDfHvlCms0fx8fA4CMKDJHg==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.0.4.tgz", - "integrity": "sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-roving-focus": "1.0.4", - "@radix-ui/react-toggle": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toolbar": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.0.4.tgz", - "integrity": "sha512-tBgmM/O7a07xbaEkYJWYTXkIdU/1pW4/KZORR43toC/4XWyBCURK0ei9kMUdp+gTPPKBgYLxXmRSH1EVcIDp8Q==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-roving-focus": "1.0.4", - "@radix-ui/react-separator": "1.0.3", - "@radix-ui/react-toggle-group": "1.0.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -5772,29 +3067,29 @@ } }, "node_modules/@radix-ui/react-tooltip": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz", - "integrity": "sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.5.tgz", + "integrity": "sha512-IucoQPcK5nwUuztaxBQvudvYwH58wtRcJlv1qvaMSyIbL9dEBfFN0vRf/D8xDbu6HmAJLlNGty4z8Na+vIqe9Q==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.3", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-visually-hidden": "1.0.3" + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.1", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -5806,15 +3101,13 @@ } }, "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", - "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "license": "MIT", "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -5823,16 +3116,16 @@ } }, "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", - "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" + "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -5841,16 +3134,16 @@ } }, "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", - "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" + "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -5859,15 +3152,13 @@ } }, "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", - "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "license": "MIT", "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -5876,15 +3167,13 @@ } }, "node_modules/@radix-ui/react-use-previous": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz", - "integrity": "sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", + "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", + "license": "MIT", "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -5893,16 +3182,16 @@ } }, "node_modules/@radix-ui/react-use-rect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz", - "integrity": "sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", + "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/rect": "1.0.1" + "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -5911,16 +3200,16 @@ } }, "node_modules/@radix-ui/react-use-size": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz", - "integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" + "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -5929,18 +3218,18 @@ } }, "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz", - "integrity": "sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz", + "integrity": "sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" + "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -5952,2269 +3241,451 @@ } }, "node_modules/@radix-ui/rect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.1.tgz", - "integrity": "sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==", - "dependencies": { - "@babel/runtime": "^7.13.10" - } + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", + "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", + "license": "MIT" }, - "node_modules/@reduxjs/toolkit": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.7.tgz", - "integrity": "sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ==", + "node_modules/@rollup/plugin-inject": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz", + "integrity": "sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==", + "dev": true, + "license": "MIT", "dependencies": { - "immer": "^9.0.21", - "redux": "^4.2.1", - "redux-thunk": "^2.4.2", - "reselect": "^4.1.8" + "@rollup/pluginutils": "^5.0.1", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" }, "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18", - "react-redux": "^7.2.1 || ^8.0.2" + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { + "rollup": { "optional": true } } }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.6.1.tgz", - "integrity": "sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw==", - "dev": true + "node_modules/@rollup/plugin-virtual": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz", + "integrity": "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, - "node_modules/@sindresorhus/slugify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-1.1.0.tgz", - "integrity": "sha512-ujZRbmmizX26yS/HnB3P9QNlNa4+UvHh+rIse3RbOXLp8yl6n1TxB4t7NHggtVgS8QmmOtzXo48kCxZGACpkPw==", + "node_modules/@rollup/pluginutils": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.3.tgz", + "integrity": "sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==", + "dev": true, "license": "MIT", "dependencies": { - "@sindresorhus/transliterate": "^0.1.1", - "escape-string-regexp": "^4.0.0" + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" }, "engines": { - "node": ">=10" + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz", + "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz", + "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz", + "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz", + "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz", + "integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz", + "integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz", + "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz", + "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz", + "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz", + "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz", + "integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz", + "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz", + "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz", + "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz", + "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz", + "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz", + "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz", + "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz", + "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/slugify": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz", + "integrity": "sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==", + "license": "MIT", + "dependencies": { + "@sindresorhus/transliterate": "^1.0.0", + "escape-string-regexp": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sindresorhus/slugify/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@sindresorhus/transliterate": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-0.1.2.tgz", - "integrity": "sha512-5/kmIOY9FF32nicXH+5yLNTX4NJ4atl7jRgqAJuIn/iyDFXBktOKDxCvyGE/EzmF4ngSUvjXxQUQlQiZ5lfw+w==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-1.6.0.tgz", + "integrity": "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==", "license": "MIT", "dependencies": { - "escape-string-regexp": "^2.0.0", - "lodash.deburr": "^4.1.0" + "escape-string-regexp": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@sindresorhus/transliterate/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "license": "MIT", "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/addon-actions": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-7.6.8.tgz", - "integrity": "sha512-/KQlr/nLsAazJuSVUoMjQdwAeeXkKEtElKdqXrqI1LVOi5a7kMgB+bmn9aKX+7VBQLfQ36Btyty+FaY7bRtehQ==", - "dev": true, - "dependencies": { - "@storybook/core-events": "7.6.8", - "@storybook/global": "^5.0.0", - "@types/uuid": "^9.0.1", - "dequal": "^2.0.2", - "polished": "^4.2.2", - "uuid": "^9.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/addon-actions/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@storybook/addon-backgrounds": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-7.6.8.tgz", - "integrity": "sha512-b+Oj41z2W/Pv6oCXmcjGdNkOStbVItrlDoIeUGyDKrngzH9Kpv5u2XZTHkZWGWusLhOVq8ENBDqj6ENRL6kDtw==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0", - "memoizerific": "^1.11.3", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/addon-controls": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-7.6.8.tgz", - "integrity": "sha512-vjBwO1KbjB3l74qOVvLvks4LJjAIStr2n4j7Grdhqf2eeQvj122gT51dXstndtMNFqNHD4y3eImwNAbuaYrrnw==", - "dev": true, - "dependencies": { - "@storybook/blocks": "7.6.8", - "lodash": "^4.17.21", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/addon-docs": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-7.6.8.tgz", - "integrity": "sha512-vl7jNKT8x8Hnwn38l5cUr6TQZFCmx09VxarGUrMEO4mwTOoVRL2ofoh9JKFXhCiCHlMI9R0lnupGB/LAplWgPg==", - "dev": true, - "dependencies": { - "@jest/transform": "^29.3.1", - "@mdx-js/react": "^2.1.5", - "@storybook/blocks": "7.6.8", - "@storybook/client-logger": "7.6.8", - "@storybook/components": "7.6.8", - "@storybook/csf-plugin": "7.6.8", - "@storybook/csf-tools": "7.6.8", - "@storybook/global": "^5.0.0", - "@storybook/mdx2-csf": "^1.0.0", - "@storybook/node-logger": "7.6.8", - "@storybook/postinstall": "7.6.8", - "@storybook/preview-api": "7.6.8", - "@storybook/react-dom-shim": "7.6.8", - "@storybook/theming": "7.6.8", - "@storybook/types": "7.6.8", - "fs-extra": "^11.1.0", - "remark-external-links": "^8.0.0", - "remark-slug": "^6.0.0", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@storybook/addon-essentials": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-7.6.8.tgz", - "integrity": "sha512-UoRZWPkDYL/UWsfAJk4q4nn5nayYdOvPApVsF/ZDnGsiv1zB2RpqbkiD1bfxPlGEVCoB+NQIN2s867gEpf+DjA==", - "dev": true, - "dependencies": { - "@storybook/addon-actions": "7.6.8", - "@storybook/addon-backgrounds": "7.6.8", - "@storybook/addon-controls": "7.6.8", - "@storybook/addon-docs": "7.6.8", - "@storybook/addon-highlight": "7.6.8", - "@storybook/addon-measure": "7.6.8", - "@storybook/addon-outline": "7.6.8", - "@storybook/addon-toolbars": "7.6.8", - "@storybook/addon-viewport": "7.6.8", - "@storybook/core-common": "7.6.8", - "@storybook/manager-api": "7.6.8", - "@storybook/node-logger": "7.6.8", - "@storybook/preview-api": "7.6.8", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@storybook/addon-highlight": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-7.6.8.tgz", - "integrity": "sha512-3mUfdLxaegCKWSm0i245RhnmEgkE+uLnOkE7h2kiztrWGqYuzGBKjgfZuVrftqsEWWc7LlJ1xdDZsIgs5Z06gA==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/addon-interactions": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/addon-interactions/-/addon-interactions-7.6.8.tgz", - "integrity": "sha512-E1ZMrJ/4larCPW92AFuY71I9s8Ri+DEdwNtVnU/WV55NA+E9oRKt5/qOrJLcjQorViwh9KOHeeuc8kagA2hjnA==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0", - "@storybook/types": "7.6.8", - "jest-mock": "^27.0.6", - "polished": "^4.2.2", - "ts-dedent": "^2.2.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/addon-links": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-7.6.8.tgz", - "integrity": "sha512-lw+xMvzfhyOR5I5792rGCf31OfVsiNG+uCc6CEewjKdC+e4GZDXzAkLIrLVUvbf6iUvHzERD63Y5nKz2bt5yZA==", - "dev": true, - "dependencies": { - "@storybook/csf": "^0.1.2", - "@storybook/global": "^5.0.0", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - } - } - }, - "node_modules/@storybook/addon-measure": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-7.6.8.tgz", - "integrity": "sha512-76ItcwATq3BRPEtGV5Apby3E+7tOn6d5dtNpBYBZOdjUsj6E+uFtdmfHrc1Bt1ersJ7hRDCgsHArqOGXeLuDrw==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0", - "tiny-invariant": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/addon-outline": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-7.6.8.tgz", - "integrity": "sha512-eTHreyvxYLIPt5AbMyDO3CEgGClQFt+CtA/RgSjpyv9MgYXPsZp/h1ZHpYYhSPRYnRE4//YnPMuk7eLf4udaag==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/addon-styling": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/@storybook/addon-styling/-/addon-styling-1.3.7.tgz", - "integrity": "sha512-JSBZMOrSw/3rlq5YoEI7Qyq703KSNP0Jd+gxTWu3/tP6245mpjn2dXnR8FvqVxCi+FG4lt2kQyPzgsuwEw1SSA==", - "dev": true, - "dependencies": { - "@babel/template": "^7.20.7", - "@babel/types": "^7.21.5", - "@storybook/api": "^7.0.12", - "@storybook/components": "^7.0.12", - "@storybook/core-common": "^7.0.12", - "@storybook/core-events": "^7.0.12", - "@storybook/manager-api": "^7.0.12", - "@storybook/node-logger": "^7.0.12", - "@storybook/preview-api": "^7.0.12", - "@storybook/theming": "^7.0.12", - "@storybook/types": "^7.0.12", - "css-loader": "^6.7.3", - "less-loader": "^11.1.0", - "postcss-loader": "^7.2.4", - "prettier": "^2.8.0", - "resolve-url-loader": "^5.0.0", - "sass-loader": "^13.2.2", - "style-loader": "^3.3.2" - }, - "bin": { - "addon-styling-setup": "postinstall.js" - }, - "peerDependencies": { - "less": "^3.5.0 || ^4.0.0", - "postcss": "^7.0.0 || ^8.0.1", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "less": { - "optional": true - }, - "postcss": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/@storybook/addon-toolbars": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-7.6.8.tgz", - "integrity": "sha512-Akr9Pfw+AzQBRPVdo8yjcdS4IiOyEIBPVn/OAcbLi6a2zLYBdn99yKi21P0o03TJjNy32A254iAQQ7zyjIwEtA==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/addon-viewport": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-7.6.8.tgz", - "integrity": "sha512-9fvaTudqTA7HYygOWq8gnlmR5XLLjMgK4RoZqMP8OhzX0Vkkg72knPI8lyrnHwze/yMcR1e2lmbdLm55rPq6QA==", - "dev": true, - "dependencies": { - "memoizerific": "^1.11.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/addons": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-7.6.8.tgz", - "integrity": "sha512-M8VXkUxD+7HLKjEQT3FNk3CoOtOw4ANhxayIu5lQ4PiKwJ61YVw1r/laPyOYaIMItH/40K1yBSCSV5DDQcN/QA==", - "dev": true, - "dependencies": { - "@storybook/manager-api": "7.6.8", - "@storybook/preview-api": "7.6.8", - "@storybook/types": "7.6.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/api": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/api/-/api-7.6.8.tgz", - "integrity": "sha512-cuc4O75n3ZNnc6880hM1Tj3ieSP12v4WgmZbTv+HlEafp/5d7gqyMSLh8KusJpkbI7CHMCJ/7vv1qmLrvHK9iw==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.8", - "@storybook/manager-api": "7.6.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/blocks": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-7.6.8.tgz", - "integrity": "sha512-9cjwqj+VLmVHD8lU1xIGbZiu2xPQ3A+cAobmam045wvEB/wYhcrF0K0lBwHLqUWTcNdOzZy5uaoaCu/1G5AmDg==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.8", - "@storybook/client-logger": "7.6.8", - "@storybook/components": "7.6.8", - "@storybook/core-events": "7.6.8", - "@storybook/csf": "^0.1.2", - "@storybook/docs-tools": "7.6.8", - "@storybook/global": "^5.0.0", - "@storybook/manager-api": "7.6.8", - "@storybook/preview-api": "7.6.8", - "@storybook/theming": "7.6.8", - "@storybook/types": "7.6.8", - "@types/lodash": "^4.14.167", - "color-convert": "^2.0.1", - "dequal": "^2.0.2", - "lodash": "^4.17.21", - "markdown-to-jsx": "^7.1.8", - "memoizerific": "^1.11.3", - "polished": "^4.2.2", - "react-colorful": "^5.1.2", - "telejson": "^7.2.0", - "tocbot": "^4.20.1", - "ts-dedent": "^2.0.0", - "util-deprecate": "^1.0.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@storybook/builder-manager": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/builder-manager/-/builder-manager-7.6.20.tgz", - "integrity": "sha512-e2GzpjLaw6CM/XSmc4qJRzBF8GOoOyotyu3JrSPTYOt4RD8kjUsK4QlismQM1DQRu8i39aIexxmRbiJyD74xzQ==", - "dev": true, - "dependencies": { - "@fal-works/esbuild-plugin-global-externals": "^2.1.2", - "@storybook/core-common": "7.6.20", - "@storybook/manager": "7.6.20", - "@storybook/node-logger": "7.6.20", - "@types/ejs": "^3.1.1", - "@types/find-cache-dir": "^3.2.1", - "@yarnpkg/esbuild-plugin-pnp": "^3.0.0-rc.10", - "browser-assert": "^1.2.1", - "ejs": "^3.1.8", - "esbuild": "^0.18.0", - "esbuild-plugin-alias": "^0.2.1", - "express": "^4.17.3", - "find-cache-dir": "^3.0.0", - "fs-extra": "^11.1.0", - "process": "^0.11.10", - "util": "^0.12.4" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/builder-manager/node_modules/@storybook/channels": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.20.tgz", - "integrity": "sha512-4hkgPSH6bJclB2OvLnkZOGZW1WptJs09mhQ6j6qLjgBZzL/ZdD6priWSd7iXrmPiN5TzUobkG4P4Dp7FjkiO7A==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.20", - "@storybook/core-events": "7.6.20", - "@storybook/global": "^5.0.0", - "qs": "^6.10.0", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/builder-manager/node_modules/@storybook/client-logger": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.20.tgz", - "integrity": "sha512-NwG0VIJQCmKrSaN5GBDFyQgTAHLNishUPLW1NrzqTDNAhfZUoef64rPQlinbopa0H4OXmlB+QxbQIb3ubeXmSQ==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/builder-manager/node_modules/@storybook/core-common": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.20.tgz", - "integrity": "sha512-8H1zPWPjcmeD4HbDm4FDD0WLsfAKGVr566IZ4hG+h3iWVW57II9JW9MLBtiR2LPSd8u7o0kw64lwRGmtCO1qAw==", - "dev": true, - "dependencies": { - "@storybook/core-events": "7.6.20", - "@storybook/node-logger": "7.6.20", - "@storybook/types": "7.6.20", - "@types/find-cache-dir": "^3.2.1", - "@types/node": "^18.0.0", - "@types/node-fetch": "^2.6.4", - "@types/pretty-hrtime": "^1.0.0", - "chalk": "^4.1.0", - "esbuild": "^0.18.0", - "esbuild-register": "^3.5.0", - "file-system-cache": "2.3.0", - "find-cache-dir": "^3.0.0", - "find-up": "^5.0.0", - "fs-extra": "^11.1.0", - "glob": "^10.0.0", - "handlebars": "^4.7.7", - "lazy-universal-dotenv": "^4.0.0", - "node-fetch": "^2.0.0", - "picomatch": "^2.3.0", - "pkg-dir": "^5.0.0", - "pretty-hrtime": "^1.0.3", - "resolve-from": "^5.0.0", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/builder-manager/node_modules/@storybook/core-events": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.20.tgz", - "integrity": "sha512-tlVDuVbDiNkvPDFAu+0ou3xBBYbx9zUURQz4G9fAq0ScgBOs/bpzcRrFb4mLpemUViBAd47tfZKdH4MAX45KVQ==", - "dev": true, - "dependencies": { - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/builder-manager/node_modules/@storybook/node-logger": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.20.tgz", - "integrity": "sha512-l2i4qF1bscJkOplNffcRTsgQWYR7J51ewmizj5YrTM8BK6rslWT1RntgVJWB1RgPqvx6VsCz1gyP3yW1oKxvYw==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/builder-manager/node_modules/@storybook/types": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.20.tgz", - "integrity": "sha512-GncdY3x0LpbhmUAAJwXYtJDUQEwfF175gsjH0/fxPkxPoV7Sef9TM41jQLJW/5+6TnZoCZP/+aJZTJtq3ni23Q==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.20", - "@types/babel__core": "^7.0.0", - "@types/express": "^4.7.0", - "file-system-cache": "2.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/builder-webpack5": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-7.6.8.tgz", - "integrity": "sha512-g4gYcHrrV/8Xve4Q/DJfXk8Bxkq5cxzy7KIBkb8PK5h+MFUiS/xoZc5qXk/WuX256zj2JnZRV//2yf61OhNd6g==", - "dev": true, - "dependencies": { - "@babel/core": "^7.23.2", - "@storybook/channels": "7.6.8", - "@storybook/client-logger": "7.6.8", - "@storybook/core-common": "7.6.8", - "@storybook/core-events": "7.6.8", - "@storybook/core-webpack": "7.6.8", - "@storybook/node-logger": "7.6.8", - "@storybook/preview": "7.6.8", - "@storybook/preview-api": "7.6.8", - "@swc/core": "^1.3.82", - "@types/node": "^18.0.0", - "@types/semver": "^7.3.4", - "babel-loader": "^9.0.0", - "browser-assert": "^1.2.1", - "case-sensitive-paths-webpack-plugin": "^2.4.0", - "constants-browserify": "^1.0.0", - "css-loader": "^6.7.1", - "es-module-lexer": "^1.4.1", - "express": "^4.17.3", - "fork-ts-checker-webpack-plugin": "^8.0.0", - "fs-extra": "^11.1.0", - "html-webpack-plugin": "^5.5.0", - "magic-string": "^0.30.5", - "path-browserify": "^1.0.1", - "process": "^0.11.10", - "semver": "^7.3.7", - "style-loader": "^3.3.1", - "swc-loader": "^0.2.3", - "terser-webpack-plugin": "^5.3.1", - "ts-dedent": "^2.0.0", - "url": "^0.11.0", - "util": "^0.12.4", - "util-deprecate": "^1.0.2", - "webpack": "5", - "webpack-dev-middleware": "^6.1.1", - "webpack-hot-middleware": "^2.25.1", - "webpack-virtual-modules": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@storybook/builder-webpack5/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@storybook/builder-webpack5/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@storybook/builder-webpack5/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/@storybook/channels": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.8.tgz", - "integrity": "sha512-aPgQcSjeyZDhAfr/slCphVfYGCihxuFCaCVlZuJA4uTaGEUkn+kPW2jP0yLtlSN33J79wFXsMLPQYwIS3aQ4Ew==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.8", - "@storybook/core-events": "7.6.8", - "@storybook/global": "^5.0.0", - "qs": "^6.10.0", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/cli": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/cli/-/cli-7.6.20.tgz", - "integrity": "sha512-ZlP+BJyqg7HlnXf7ypjG2CKMI/KVOn03jFIiClItE/jQfgR6kRFgtjRU7uajh427HHfjv9DRiur8nBzuO7vapA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.23.2", - "@babel/preset-env": "^7.23.2", - "@babel/types": "^7.23.0", - "@ndelangen/get-tarball": "^3.0.7", - "@storybook/codemod": "7.6.20", - "@storybook/core-common": "7.6.20", - "@storybook/core-events": "7.6.20", - "@storybook/core-server": "7.6.20", - "@storybook/csf-tools": "7.6.20", - "@storybook/node-logger": "7.6.20", - "@storybook/telemetry": "7.6.20", - "@storybook/types": "7.6.20", - "@types/semver": "^7.3.4", - "@yarnpkg/fslib": "2.10.3", - "@yarnpkg/libzip": "2.3.0", - "chalk": "^4.1.0", - "commander": "^6.2.1", - "cross-spawn": "^7.0.3", - "detect-indent": "^6.1.0", - "envinfo": "^7.7.3", - "execa": "^5.0.0", - "express": "^4.17.3", - "find-up": "^5.0.0", - "fs-extra": "^11.1.0", - "get-npm-tarball-url": "^2.0.3", - "get-port": "^5.1.1", - "giget": "^1.0.0", - "globby": "^11.0.2", - "jscodeshift": "^0.15.1", - "leven": "^3.1.0", - "ora": "^5.4.1", - "prettier": "^2.8.0", - "prompts": "^2.4.0", - "puppeteer-core": "^2.1.1", - "read-pkg-up": "^7.0.1", - "semver": "^7.3.7", - "strip-json-comments": "^3.0.1", - "tempy": "^1.0.1", - "ts-dedent": "^2.0.0", - "util-deprecate": "^1.0.2" - }, - "bin": { - "getstorybook": "bin/index.js", - "sb": "bin/index.js" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/cli/node_modules/@storybook/channels": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.20.tgz", - "integrity": "sha512-4hkgPSH6bJclB2OvLnkZOGZW1WptJs09mhQ6j6qLjgBZzL/ZdD6priWSd7iXrmPiN5TzUobkG4P4Dp7FjkiO7A==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.20", - "@storybook/core-events": "7.6.20", - "@storybook/global": "^5.0.0", - "qs": "^6.10.0", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/cli/node_modules/@storybook/client-logger": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.20.tgz", - "integrity": "sha512-NwG0VIJQCmKrSaN5GBDFyQgTAHLNishUPLW1NrzqTDNAhfZUoef64rPQlinbopa0H4OXmlB+QxbQIb3ubeXmSQ==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/cli/node_modules/@storybook/core-common": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.20.tgz", - "integrity": "sha512-8H1zPWPjcmeD4HbDm4FDD0WLsfAKGVr566IZ4hG+h3iWVW57II9JW9MLBtiR2LPSd8u7o0kw64lwRGmtCO1qAw==", - "dev": true, - "dependencies": { - "@storybook/core-events": "7.6.20", - "@storybook/node-logger": "7.6.20", - "@storybook/types": "7.6.20", - "@types/find-cache-dir": "^3.2.1", - "@types/node": "^18.0.0", - "@types/node-fetch": "^2.6.4", - "@types/pretty-hrtime": "^1.0.0", - "chalk": "^4.1.0", - "esbuild": "^0.18.0", - "esbuild-register": "^3.5.0", - "file-system-cache": "2.3.0", - "find-cache-dir": "^3.0.0", - "find-up": "^5.0.0", - "fs-extra": "^11.1.0", - "glob": "^10.0.0", - "handlebars": "^4.7.7", - "lazy-universal-dotenv": "^4.0.0", - "node-fetch": "^2.0.0", - "picomatch": "^2.3.0", - "pkg-dir": "^5.0.0", - "pretty-hrtime": "^1.0.3", - "resolve-from": "^5.0.0", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/cli/node_modules/@storybook/core-events": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.20.tgz", - "integrity": "sha512-tlVDuVbDiNkvPDFAu+0ou3xBBYbx9zUURQz4G9fAq0ScgBOs/bpzcRrFb4mLpemUViBAd47tfZKdH4MAX45KVQ==", - "dev": true, - "dependencies": { - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/cli/node_modules/@storybook/csf-tools": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.20.tgz", - "integrity": "sha512-rwcwzCsAYh/m/WYcxBiEtLpIW5OH1ingxNdF/rK9mtGWhJxXRDV8acPkFrF8rtFWIVKoOCXu5USJYmc3f2gdYQ==", - "dev": true, - "dependencies": { - "@babel/generator": "^7.23.0", - "@babel/parser": "^7.23.0", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0", - "@storybook/csf": "^0.1.2", - "@storybook/types": "7.6.20", - "fs-extra": "^11.1.0", - "recast": "^0.23.1", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/cli/node_modules/@storybook/node-logger": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.20.tgz", - "integrity": "sha512-l2i4qF1bscJkOplNffcRTsgQWYR7J51ewmizj5YrTM8BK6rslWT1RntgVJWB1RgPqvx6VsCz1gyP3yW1oKxvYw==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/cli/node_modules/@storybook/types": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.20.tgz", - "integrity": "sha512-GncdY3x0LpbhmUAAJwXYtJDUQEwfF175gsjH0/fxPkxPoV7Sef9TM41jQLJW/5+6TnZoCZP/+aJZTJtq3ni23Q==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.20", - "@types/babel__core": "^7.0.0", - "@types/express": "^4.7.0", - "file-system-cache": "2.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/cli/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/@storybook/cli/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@storybook/cli/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "node_modules/@stylistic/eslint-plugin": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.12.1.tgz", + "integrity": "sha512-fubZKIHSPuo07FgRTn6S4Nl0uXPRPYVNpyZzIDGfp7Fny6JjNus6kReLD7NI380JXi4HtUTSOZ34LBuNPO1XLQ==", "dev": true, - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/@storybook/cli/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, - "bin": { - "semver": "bin/semver.js" + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.13.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.2" }, "engines": { - "node": ">=10" - } - }, - "node_modules/@storybook/client-api": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/client-api/-/client-api-7.6.8.tgz", - "integrity": "sha512-1sTxQN6VbRH7z+L077B/8nxs+nGvrHiYcDhbyGxwM0e6Vn2/ZffdcKt1MXseDhfqIxNgQgOjhKIGsOlbOVPEDw==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.8", - "@storybook/preview-api": "7.6.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/client-logger": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.8.tgz", - "integrity": "sha512-WyK+RNSYk+sy0pxk8np1MnUXSWFdy54WqtT7u64vDFs9Jxfa1oMZ+Vl6XhaFQYR++tKC7VabLcI6vZ0pOoE9Jw==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/codemod": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-7.6.20.tgz", - "integrity": "sha512-8vmSsksO4XukNw0TmqylPmk7PxnfNfE21YsxFa7mnEBmEKQcZCQsNil4ZgWfG0IzdhTfhglAN4r++Ew0WE+PYA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.23.2", - "@babel/preset-env": "^7.23.2", - "@babel/types": "^7.23.0", - "@storybook/csf": "^0.1.2", - "@storybook/csf-tools": "7.6.20", - "@storybook/node-logger": "7.6.20", - "@storybook/types": "7.6.20", - "@types/cross-spawn": "^6.0.2", - "cross-spawn": "^7.0.3", - "globby": "^11.0.2", - "jscodeshift": "^0.15.1", - "lodash": "^4.17.21", - "prettier": "^2.8.0", - "recast": "^0.23.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/codemod/node_modules/@storybook/channels": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.20.tgz", - "integrity": "sha512-4hkgPSH6bJclB2OvLnkZOGZW1WptJs09mhQ6j6qLjgBZzL/ZdD6priWSd7iXrmPiN5TzUobkG4P4Dp7FjkiO7A==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.20", - "@storybook/core-events": "7.6.20", - "@storybook/global": "^5.0.0", - "qs": "^6.10.0", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/codemod/node_modules/@storybook/client-logger": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.20.tgz", - "integrity": "sha512-NwG0VIJQCmKrSaN5GBDFyQgTAHLNishUPLW1NrzqTDNAhfZUoef64rPQlinbopa0H4OXmlB+QxbQIb3ubeXmSQ==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/codemod/node_modules/@storybook/core-events": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.20.tgz", - "integrity": "sha512-tlVDuVbDiNkvPDFAu+0ou3xBBYbx9zUURQz4G9fAq0ScgBOs/bpzcRrFb4mLpemUViBAd47tfZKdH4MAX45KVQ==", - "dev": true, - "dependencies": { - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/codemod/node_modules/@storybook/csf-tools": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.20.tgz", - "integrity": "sha512-rwcwzCsAYh/m/WYcxBiEtLpIW5OH1ingxNdF/rK9mtGWhJxXRDV8acPkFrF8rtFWIVKoOCXu5USJYmc3f2gdYQ==", - "dev": true, - "dependencies": { - "@babel/generator": "^7.23.0", - "@babel/parser": "^7.23.0", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0", - "@storybook/csf": "^0.1.2", - "@storybook/types": "7.6.20", - "fs-extra": "^11.1.0", - "recast": "^0.23.1", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/codemod/node_modules/@storybook/node-logger": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.20.tgz", - "integrity": "sha512-l2i4qF1bscJkOplNffcRTsgQWYR7J51ewmizj5YrTM8BK6rslWT1RntgVJWB1RgPqvx6VsCz1gyP3yW1oKxvYw==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/codemod/node_modules/@storybook/types": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.20.tgz", - "integrity": "sha512-GncdY3x0LpbhmUAAJwXYtJDUQEwfF175gsjH0/fxPkxPoV7Sef9TM41jQLJW/5+6TnZoCZP/+aJZTJtq3ni23Q==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.20", - "@types/babel__core": "^7.0.0", - "@types/express": "^4.7.0", - "file-system-cache": "2.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/components": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/components/-/components-7.6.8.tgz", - "integrity": "sha512-ghrQkws7F2s9xwdiQq2ezQoOozCiYF9g/vnh+qttd4UgKqXDWoILb8LJGKtS7C0u0vV/Ui59EYUyDIVBT6wHlw==", - "dev": true, - "dependencies": { - "@radix-ui/react-select": "^1.2.2", - "@radix-ui/react-toolbar": "^1.0.4", - "@storybook/client-logger": "7.6.8", - "@storybook/csf": "^0.1.2", - "@storybook/global": "^5.0.0", - "@storybook/theming": "7.6.8", - "@storybook/types": "7.6.8", - "memoizerific": "^1.11.3", - "use-resize-observer": "^9.1.0", - "util-deprecate": "^1.0.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + "eslint": ">=8.40.0" } }, - "node_modules/@storybook/components/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.4.tgz", - "integrity": "sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==", + "node_modules/@stylistic/eslint-plugin/node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-escape-keydown": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@storybook/components/node_modules/@radix-ui/react-focus-scope": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.3.tgz", - "integrity": "sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@storybook/components/node_modules/@radix-ui/react-popper": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.2.tgz", - "integrity": "sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.13.10", - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-rect": "1.0.1", - "@radix-ui/react-use-size": "1.0.1", - "@radix-ui/rect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@storybook/components/node_modules/@radix-ui/react-portal": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.3.tgz", - "integrity": "sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@storybook/components/node_modules/@radix-ui/react-select": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-1.2.2.tgz", - "integrity": "sha512-zI7McXr8fNaSrUY9mZe4x/HC0jTLY9fWNhO1oLWYMQGDXuV4UCivIGTxwioSzO0ZCYX9iSLyWmAh/1TOmX3Cnw==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/number": "1.0.1", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.4", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.3", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.2", - "@radix-ui/react-portal": "1.0.3", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-previous": "1.0.1", - "@radix-ui/react-visually-hidden": "1.0.3", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@storybook/core-client": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/core-client/-/core-client-7.6.8.tgz", - "integrity": "sha512-Avt0R0F9U+PEndPS23LHyIBxbwVCeF/VCIuIfD1eTYwE9nSLzvJXqlxARfFyhYV43LQcC5fIKjxfrsyUjM5vbQ==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.8", - "@storybook/preview-api": "7.6.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-common": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.8.tgz", - "integrity": "sha512-TRbiv5AF2m88ixyh31yqn6FgWDYZO6e6IxbJolRvEKD4b9opfPJ5e1ocb/QPz9sBUmsrX59ghMjO8R6dDYzdwA==", - "dev": true, - "dependencies": { - "@storybook/core-events": "7.6.8", - "@storybook/node-logger": "7.6.8", - "@storybook/types": "7.6.8", - "@types/find-cache-dir": "^3.2.1", - "@types/node": "^18.0.0", - "@types/node-fetch": "^2.6.4", - "@types/pretty-hrtime": "^1.0.0", - "chalk": "^4.1.0", - "esbuild": "^0.18.0", - "esbuild-register": "^3.5.0", - "file-system-cache": "2.3.0", - "find-cache-dir": "^3.0.0", - "find-up": "^5.0.0", - "fs-extra": "^11.1.0", - "glob": "^10.0.0", - "handlebars": "^4.7.7", - "lazy-universal-dotenv": "^4.0.0", - "node-fetch": "^2.0.0", - "picomatch": "^2.3.0", - "pkg-dir": "^5.0.0", - "pretty-hrtime": "^1.0.3", - "resolve-from": "^5.0.0", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-events": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.8.tgz", - "integrity": "sha512-c1onJHG71JKbU4hMZC31rVTSbcfhcXaB0ikGnb7rJzlUZ1YkWnb0wf0/ikQR0seDOpR3HS+WQ0M3FIpqANyETg==", - "dev": true, - "dependencies": { - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-server": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-7.6.20.tgz", - "integrity": "sha512-qC5BdbqqwMLTdCwMKZ1Hbc3+3AaxHYWLiJaXL9e8s8nJw89xV8c8l30QpbJOGvcDmsgY6UTtXYaJ96OsTr7MrA==", - "dev": true, - "dependencies": { - "@aw-web-design/x-default-browser": "1.4.126", - "@discoveryjs/json-ext": "^0.5.3", - "@storybook/builder-manager": "7.6.20", - "@storybook/channels": "7.6.20", - "@storybook/core-common": "7.6.20", - "@storybook/core-events": "7.6.20", - "@storybook/csf": "^0.1.2", - "@storybook/csf-tools": "7.6.20", - "@storybook/docs-mdx": "^0.1.0", - "@storybook/global": "^5.0.0", - "@storybook/manager": "7.6.20", - "@storybook/node-logger": "7.6.20", - "@storybook/preview-api": "7.6.20", - "@storybook/telemetry": "7.6.20", - "@storybook/types": "7.6.20", - "@types/detect-port": "^1.3.0", - "@types/node": "^18.0.0", - "@types/pretty-hrtime": "^1.0.0", - "@types/semver": "^7.3.4", - "better-opn": "^3.0.2", - "chalk": "^4.1.0", - "cli-table3": "^0.6.1", - "compression": "^1.7.4", - "detect-port": "^1.3.0", - "express": "^4.17.3", - "fs-extra": "^11.1.0", - "globby": "^11.0.2", - "lodash": "^4.17.21", - "open": "^8.4.0", - "pretty-hrtime": "^1.0.3", - "prompts": "^2.4.0", - "read-pkg-up": "^7.0.1", - "semver": "^7.3.7", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1", - "ts-dedent": "^2.0.0", - "util": "^0.12.4", - "util-deprecate": "^1.0.2", - "watchpack": "^2.2.0", - "ws": "^8.2.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-server/node_modules/@storybook/channels": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.20.tgz", - "integrity": "sha512-4hkgPSH6bJclB2OvLnkZOGZW1WptJs09mhQ6j6qLjgBZzL/ZdD6priWSd7iXrmPiN5TzUobkG4P4Dp7FjkiO7A==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.20", - "@storybook/core-events": "7.6.20", - "@storybook/global": "^5.0.0", - "qs": "^6.10.0", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-server/node_modules/@storybook/client-logger": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.20.tgz", - "integrity": "sha512-NwG0VIJQCmKrSaN5GBDFyQgTAHLNishUPLW1NrzqTDNAhfZUoef64rPQlinbopa0H4OXmlB+QxbQIb3ubeXmSQ==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-server/node_modules/@storybook/core-common": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.20.tgz", - "integrity": "sha512-8H1zPWPjcmeD4HbDm4FDD0WLsfAKGVr566IZ4hG+h3iWVW57II9JW9MLBtiR2LPSd8u7o0kw64lwRGmtCO1qAw==", - "dev": true, - "dependencies": { - "@storybook/core-events": "7.6.20", - "@storybook/node-logger": "7.6.20", - "@storybook/types": "7.6.20", - "@types/find-cache-dir": "^3.2.1", - "@types/node": "^18.0.0", - "@types/node-fetch": "^2.6.4", - "@types/pretty-hrtime": "^1.0.0", - "chalk": "^4.1.0", - "esbuild": "^0.18.0", - "esbuild-register": "^3.5.0", - "file-system-cache": "2.3.0", - "find-cache-dir": "^3.0.0", - "find-up": "^5.0.0", - "fs-extra": "^11.1.0", - "glob": "^10.0.0", - "handlebars": "^4.7.7", - "lazy-universal-dotenv": "^4.0.0", - "node-fetch": "^2.0.0", - "picomatch": "^2.3.0", - "pkg-dir": "^5.0.0", - "pretty-hrtime": "^1.0.3", - "resolve-from": "^5.0.0", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-server/node_modules/@storybook/core-events": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.20.tgz", - "integrity": "sha512-tlVDuVbDiNkvPDFAu+0ou3xBBYbx9zUURQz4G9fAq0ScgBOs/bpzcRrFb4mLpemUViBAd47tfZKdH4MAX45KVQ==", - "dev": true, - "dependencies": { - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-server/node_modules/@storybook/csf-tools": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.20.tgz", - "integrity": "sha512-rwcwzCsAYh/m/WYcxBiEtLpIW5OH1ingxNdF/rK9mtGWhJxXRDV8acPkFrF8rtFWIVKoOCXu5USJYmc3f2gdYQ==", - "dev": true, - "dependencies": { - "@babel/generator": "^7.23.0", - "@babel/parser": "^7.23.0", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0", - "@storybook/csf": "^0.1.2", - "@storybook/types": "7.6.20", - "fs-extra": "^11.1.0", - "recast": "^0.23.1", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-server/node_modules/@storybook/node-logger": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.20.tgz", - "integrity": "sha512-l2i4qF1bscJkOplNffcRTsgQWYR7J51ewmizj5YrTM8BK6rslWT1RntgVJWB1RgPqvx6VsCz1gyP3yW1oKxvYw==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-server/node_modules/@storybook/preview-api": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.6.20.tgz", - "integrity": "sha512-3ic2m9LDZEPwZk02wIhNc3n3rNvbi7VDKn52hDXfAxnL5EYm7yDICAkaWcVaTfblru2zn0EDJt7ROpthscTW5w==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.20", - "@storybook/client-logger": "7.6.20", - "@storybook/core-events": "7.6.20", - "@storybook/csf": "^0.1.2", - "@storybook/global": "^5.0.0", - "@storybook/types": "7.6.20", - "@types/qs": "^6.9.5", - "dequal": "^2.0.2", - "lodash": "^4.17.21", - "memoizerific": "^1.11.3", - "qs": "^6.10.0", - "synchronous-promise": "^2.0.15", - "ts-dedent": "^2.0.0", - "util-deprecate": "^1.0.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-server/node_modules/@storybook/types": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.20.tgz", - "integrity": "sha512-GncdY3x0LpbhmUAAJwXYtJDUQEwfF175gsjH0/fxPkxPoV7Sef9TM41jQLJW/5+6TnZoCZP/+aJZTJtq3ni23Q==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.20", - "@types/babel__core": "^7.0.0", - "@types/express": "^4.7.0", - "file-system-cache": "2.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-server/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, - "bin": { - "semver": "bin/semver.js" + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" }, "engines": { - "node": ">=10" - } - }, - "node_modules/@storybook/core-webpack": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-7.6.8.tgz", - "integrity": "sha512-UOTW2WhKmB8baCLc1eRssmz11sBv+iDRyS2WFK+WONkiGy3pQrpxfq2OVXXMFYkSHGXqj/jSKfKXSmNQBbkyAQ==", - "dev": true, - "dependencies": { - "@storybook/core-common": "7.6.8", - "@storybook/node-logger": "7.6.8", - "@storybook/types": "7.6.8", - "@types/node": "^18.0.0", - "ts-dedent": "^2.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" + "url": "https://opencollective.com/eslint" } }, - "node_modules/@storybook/csf": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.1.2.tgz", - "integrity": "sha512-ePrvE/pS1vsKR9Xr+o+YwdqNgHUyXvg+1Xjx0h9LrVx7Zq4zNe06pd63F5EvzTbCbJsHj7GHr9tkiaqm7U8WRA==", - "dev": true, - "dependencies": { - "type-fest": "^2.19.0" - } - }, - "node_modules/@storybook/csf-plugin": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-7.6.8.tgz", - "integrity": "sha512-KYh7VwTHhXz/V9weuGY3pK9messE56TJHUD+0SO9dF2BVNKsKpAOVcjzrE6masiAFX35Dz/t9ywy8iFcfAo0dg==", - "dev": true, - "dependencies": { - "@storybook/csf-tools": "7.6.8", - "unplugin": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/csf-tools": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.8.tgz", - "integrity": "sha512-ea6QnQRvhPOpSUbfioLlJYRLpJldNZcocgUJwOJ/e3TM6M67BZBzeDnVOJkuUKejrp++KF22GEIkbGAWErIlnA==", - "dev": true, - "dependencies": { - "@babel/generator": "^7.23.0", - "@babel/parser": "^7.23.0", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0", - "@storybook/csf": "^0.1.2", - "@storybook/types": "7.6.8", - "fs-extra": "^11.1.0", - "recast": "^0.23.1", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/docs-mdx": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@storybook/docs-mdx/-/docs-mdx-0.1.0.tgz", - "integrity": "sha512-JDaBR9lwVY4eSH5W8EGHrhODjygPd6QImRbwjAuJNEnY0Vw4ie3bPkeGfnacB3OBW6u/agqPv2aRlR46JcAQLg==", - "dev": true - }, - "node_modules/@storybook/docs-tools": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/docs-tools/-/docs-tools-7.6.8.tgz", - "integrity": "sha512-zIbrje4JLFpfK05y3SkDNtIth/vTOEaJVa/zaHuwS1gUX73Pq3jwF2eMGVabeVWi6hvxGeZXhnIsymh/Hpbn5w==", - "dev": true, - "dependencies": { - "@storybook/core-common": "7.6.8", - "@storybook/preview-api": "7.6.8", - "@storybook/types": "7.6.8", - "@types/doctrine": "^0.0.3", - "assert": "^2.1.0", - "doctrine": "^3.0.0", - "lodash": "^4.17.21" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/global": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@storybook/global/-/global-5.0.0.tgz", - "integrity": "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==", - "dev": true - }, - "node_modules/@storybook/manager": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/manager/-/manager-7.6.20.tgz", - "integrity": "sha512-0Cf6WN0t7yEG2DR29tN5j+i7H/TH5EfPppg9h9/KiQSoFHk+6KLoy2p5do94acFU+Ro4+zzxvdCGbcYGKuArpg==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/manager-api": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-7.6.8.tgz", - "integrity": "sha512-BGVZb0wMTd8Hi8rUYPRzdIhWRw73qXlEupwEYyGtH63sg+aD67wyAo8/pMEpQBH4kVss7VheWY2JGpRJeFVUxw==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.8", - "@storybook/client-logger": "7.6.8", - "@storybook/core-events": "7.6.8", - "@storybook/csf": "^0.1.2", - "@storybook/global": "^5.0.0", - "@storybook/router": "7.6.8", - "@storybook/theming": "7.6.8", - "@storybook/types": "7.6.8", - "dequal": "^2.0.2", - "lodash": "^4.17.21", - "memoizerific": "^1.11.3", - "store2": "^2.14.2", - "telejson": "^7.2.0", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/mdx2-csf": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@storybook/mdx2-csf/-/mdx2-csf-1.1.0.tgz", - "integrity": "sha512-TXJJd5RAKakWx4BtpwvSNdgTDkKM6RkXU8GK34S/LhidQ5Pjz3wcnqb0TxEkfhK/ztbP8nKHqXFwLfa2CYkvQw==", - "dev": true - }, - "node_modules/@storybook/nextjs": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/nextjs/-/nextjs-7.6.8.tgz", - "integrity": "sha512-17n2k7h5Eg6LGZpQbEpBXa949+QY5Zv0u5zvtXpMShpnrcuX2E3e9AQabwUmIvU7WiLaRw9rxNC0HrW6GUB3zg==", - "dev": true, - "dependencies": { - "@babel/core": "^7.23.2", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.22.5", - "@babel/plugin-transform-class-properties": "^7.22.5", - "@babel/plugin-transform-export-namespace-from": "^7.22.11", - "@babel/plugin-transform-numeric-separator": "^7.22.11", - "@babel/plugin-transform-object-rest-spread": "^7.22.15", - "@babel/plugin-transform-runtime": "^7.23.2", - "@babel/preset-env": "^7.23.2", - "@babel/preset-react": "^7.22.15", - "@babel/preset-typescript": "^7.23.2", - "@babel/runtime": "^7.23.2", - "@storybook/addon-actions": "7.6.8", - "@storybook/builder-webpack5": "7.6.8", - "@storybook/core-common": "7.6.8", - "@storybook/core-events": "7.6.8", - "@storybook/node-logger": "7.6.8", - "@storybook/preset-react-webpack": "7.6.8", - "@storybook/preview-api": "7.6.8", - "@storybook/react": "7.6.8", - "@types/node": "^18.0.0", - "@types/semver": "^7.3.4", - "css-loader": "^6.7.3", - "find-up": "^5.0.0", - "fs-extra": "^11.1.0", - "image-size": "^1.0.0", - "loader-utils": "^3.2.1", - "node-polyfill-webpack-plugin": "^2.0.1", - "pnp-webpack-plugin": "^1.7.0", - "postcss": "^8.4.21", - "postcss-loader": "^7.0.2", - "resolve-url-loader": "^5.0.0", - "sass-loader": "^12.4.0", - "semver": "^7.3.5", - "sharp": "^0.32.6", - "style-loader": "^3.3.1", - "styled-jsx": "5.1.1", - "ts-dedent": "^2.0.0", - "tsconfig-paths": "^4.0.0", - "tsconfig-paths-webpack-plugin": "^4.0.1" - }, - "engines": { - "node": ">=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "@next/font": "^13.0.0|| ^14.0.0", - "next": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@next/font": { - "optional": true - }, - "typescript": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/@storybook/nextjs/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@storybook/nextjs/node_modules/sass-loader": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", - "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==", - "dev": true, - "dependencies": { - "klona": "^2.0.4", - "neo-async": "^2.6.2" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "fibers": ">= 3.1.0", - "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", - "sass": "^1.3.0", - "sass-embedded": "*", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "fibers": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - } - } - }, - "node_modules/@storybook/nextjs/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@storybook/nextjs/node_modules/sharp": { - "version": "0.32.6", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", - "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.2", - "node-addon-api": "^6.1.0", - "prebuild-install": "^7.1.1", - "semver": "^7.5.4", - "simple-get": "^4.0.1", - "tar-fs": "^3.0.4", - "tunnel-agent": "^0.6.0" - }, - "engines": { - "node": ">=14.15.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@storybook/nextjs/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/@storybook/node-logger": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.8.tgz", - "integrity": "sha512-SVvwZAcOLdkstqnAbE5hVYsriXh6OXjLcwFEBpAYi1meQ0R70iNALVSPEfIDK1r7M163Jngsq2hRnHvbLoQNkg==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/postinstall": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/postinstall/-/postinstall-7.6.8.tgz", - "integrity": "sha512-9ixyNpoT1w3WmSooCzndAWDnw4fENA1WUBcdqrzlcgaSBKiAHad1k/Yct/uBAU95l/uQ13NgXK3mx4+S6unx/g==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/preset-react-webpack": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-7.6.8.tgz", - "integrity": "sha512-S7z2IKonfZyvaETPwDHaOsw2hnG6Kny6aVnWj1/oAMHLRkAo08v/uxXc3of27HmCng3sKoPtEKypQa6yV863MA==", - "dev": true, - "dependencies": { - "@babel/preset-flow": "^7.22.15", - "@babel/preset-react": "^7.22.15", - "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", - "@storybook/core-webpack": "7.6.8", - "@storybook/docs-tools": "7.6.8", - "@storybook/node-logger": "7.6.8", - "@storybook/react": "7.6.8", - "@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0", - "@types/node": "^18.0.0", - "@types/semver": "^7.3.4", - "babel-plugin-add-react-displayname": "^0.0.5", - "fs-extra": "^11.1.0", - "magic-string": "^0.30.5", - "react-docgen": "^7.0.0", - "react-refresh": "^0.14.0", - "semver": "^7.3.7", - "webpack": "5" - }, - "engines": { - "node": ">=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "@babel/core": "^7.22.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/@storybook/preset-react-webpack/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@storybook/preset-react-webpack/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@storybook/preset-react-webpack/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/@storybook/preview": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/preview/-/preview-7.6.8.tgz", - "integrity": "sha512-f54EXmJcIkc5A7nQmtnCUtNFNfEOoTuPYFK7pDfcK/bVU+g63zzWhBAeIUZ8yioLKGqZPTzFEhXkpa+OqsT0Jg==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/preview-api": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.6.8.tgz", - "integrity": "sha512-rtP9Yo8ZV1NWhtA3xCOAb1vU70KCV3D2U4E3rOb2prqJ2CEQ/MQbrB7KUTDRSQdT7VFbjsLQWVCTUcNo29U8JQ==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.8", - "@storybook/client-logger": "7.6.8", - "@storybook/core-events": "7.6.8", - "@storybook/csf": "^0.1.2", - "@storybook/global": "^5.0.0", - "@storybook/types": "7.6.8", - "@types/qs": "^6.9.5", - "dequal": "^2.0.2", - "lodash": "^4.17.21", - "memoizerific": "^1.11.3", - "qs": "^6.10.0", - "synchronous-promise": "^2.0.15", - "ts-dedent": "^2.0.0", - "util-deprecate": "^1.0.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/react": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/react/-/react-7.6.8.tgz", - "integrity": "sha512-yMqcCNskCxqoYSGWO1qu6Jdju9zhEEwd8tOC7AgIC8sAB7K8FTxZu0d6+QFpeg9fGq+hyAmRM4GrT9Fq9IKwwQ==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.8", - "@storybook/core-client": "7.6.8", - "@storybook/docs-tools": "7.6.8", - "@storybook/global": "^5.0.0", - "@storybook/preview-api": "7.6.8", - "@storybook/react-dom-shim": "7.6.8", - "@storybook/types": "7.6.8", - "@types/escodegen": "^0.0.6", - "@types/estree": "^0.0.51", - "@types/node": "^18.0.0", - "acorn": "^7.4.1", - "acorn-jsx": "^5.3.1", - "acorn-walk": "^7.2.0", - "escodegen": "^2.1.0", - "html-tags": "^3.1.0", - "lodash": "^4.17.21", - "prop-types": "^15.7.2", - "react-element-to-jsx-string": "^15.0.0", - "ts-dedent": "^2.0.0", - "type-fest": "~2.19", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@storybook/react-docgen-typescript-plugin": { - "version": "1.0.6--canary.9.0c3f3b7.0", - "resolved": "https://registry.npmjs.org/@storybook/react-docgen-typescript-plugin/-/react-docgen-typescript-plugin-1.0.6--canary.9.0c3f3b7.0.tgz", - "integrity": "sha512-KUqXC3oa9JuQ0kZJLBhVdS4lOneKTOopnNBK4tUAgoxWQ3u/IjzdueZjFr7gyBrXMoU6duutk3RQR9u8ZpYJ4Q==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "endent": "^2.0.1", - "find-cache-dir": "^3.3.1", - "flat-cache": "^3.0.4", - "micromatch": "^4.0.2", - "react-docgen-typescript": "^2.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "typescript": ">= 4.x", - "webpack": ">= 4" - } - }, - "node_modules/@storybook/react-dom-shim": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-7.6.8.tgz", - "integrity": "sha512-NIvtjdXCTwd0VA/zCaCuCYv7L35nze7qDsFW6JhSHyqB7fKyIEMSbluktO2VISotHOSkgZ2zA+rGpk3O8yh6lg==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@storybook/router": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/router/-/router-7.6.8.tgz", - "integrity": "sha512-pFoq22w1kEwduqMpGX3FPSSukdWLMX6UQa2Cw4MDW+hzp3vhC7+3MVaBG5ShQAjGv46NNcSgsIUkyarlU5wd/A==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.8", - "memoizerific": "^1.11.3", - "qs": "^6.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/telemetry": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/telemetry/-/telemetry-7.6.20.tgz", - "integrity": "sha512-dmAOCWmOscYN6aMbhCMmszQjoycg7tUPRVy2kTaWg6qX10wtMrvEtBV29W4eMvqdsoRj5kcvoNbzRdYcWBUOHQ==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.20", - "@storybook/core-common": "7.6.20", - "@storybook/csf-tools": "7.6.20", - "chalk": "^4.1.0", - "detect-package-manager": "^2.0.1", - "fetch-retry": "^5.0.2", - "fs-extra": "^11.1.0", - "read-pkg-up": "^7.0.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/telemetry/node_modules/@storybook/channels": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.20.tgz", - "integrity": "sha512-4hkgPSH6bJclB2OvLnkZOGZW1WptJs09mhQ6j6qLjgBZzL/ZdD6priWSd7iXrmPiN5TzUobkG4P4Dp7FjkiO7A==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.20", - "@storybook/core-events": "7.6.20", - "@storybook/global": "^5.0.0", - "qs": "^6.10.0", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/telemetry/node_modules/@storybook/client-logger": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.20.tgz", - "integrity": "sha512-NwG0VIJQCmKrSaN5GBDFyQgTAHLNishUPLW1NrzqTDNAhfZUoef64rPQlinbopa0H4OXmlB+QxbQIb3ubeXmSQ==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/telemetry/node_modules/@storybook/core-common": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.20.tgz", - "integrity": "sha512-8H1zPWPjcmeD4HbDm4FDD0WLsfAKGVr566IZ4hG+h3iWVW57II9JW9MLBtiR2LPSd8u7o0kw64lwRGmtCO1qAw==", - "dev": true, - "dependencies": { - "@storybook/core-events": "7.6.20", - "@storybook/node-logger": "7.6.20", - "@storybook/types": "7.6.20", - "@types/find-cache-dir": "^3.2.1", - "@types/node": "^18.0.0", - "@types/node-fetch": "^2.6.4", - "@types/pretty-hrtime": "^1.0.0", - "chalk": "^4.1.0", - "esbuild": "^0.18.0", - "esbuild-register": "^3.5.0", - "file-system-cache": "2.3.0", - "find-cache-dir": "^3.0.0", - "find-up": "^5.0.0", - "fs-extra": "^11.1.0", - "glob": "^10.0.0", - "handlebars": "^4.7.7", - "lazy-universal-dotenv": "^4.0.0", - "node-fetch": "^2.0.0", - "picomatch": "^2.3.0", - "pkg-dir": "^5.0.0", - "pretty-hrtime": "^1.0.3", - "resolve-from": "^5.0.0", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/telemetry/node_modules/@storybook/core-events": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.20.tgz", - "integrity": "sha512-tlVDuVbDiNkvPDFAu+0ou3xBBYbx9zUURQz4G9fAq0ScgBOs/bpzcRrFb4mLpemUViBAd47tfZKdH4MAX45KVQ==", - "dev": true, - "dependencies": { - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/telemetry/node_modules/@storybook/csf-tools": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.20.tgz", - "integrity": "sha512-rwcwzCsAYh/m/WYcxBiEtLpIW5OH1ingxNdF/rK9mtGWhJxXRDV8acPkFrF8rtFWIVKoOCXu5USJYmc3f2gdYQ==", - "dev": true, - "dependencies": { - "@babel/generator": "^7.23.0", - "@babel/parser": "^7.23.0", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0", - "@storybook/csf": "^0.1.2", - "@storybook/types": "7.6.20", - "fs-extra": "^11.1.0", - "recast": "^0.23.1", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/telemetry/node_modules/@storybook/node-logger": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.20.tgz", - "integrity": "sha512-l2i4qF1bscJkOplNffcRTsgQWYR7J51ewmizj5YrTM8BK6rslWT1RntgVJWB1RgPqvx6VsCz1gyP3yW1oKxvYw==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/telemetry/node_modules/@storybook/types": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.20.tgz", - "integrity": "sha512-GncdY3x0LpbhmUAAJwXYtJDUQEwfF175gsjH0/fxPkxPoV7Sef9TM41jQLJW/5+6TnZoCZP/+aJZTJtq3ni23Q==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.20", - "@types/babel__core": "^7.0.0", - "@types/express": "^4.7.0", - "file-system-cache": "2.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/testing-library": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@storybook/testing-library/-/testing-library-0.2.2.tgz", - "integrity": "sha512-L8sXFJUHmrlyU2BsWWZGuAjv39Jl1uAqUHdxmN42JY15M4+XCMjGlArdCCjDe1wpTSW6USYISA9axjZojgtvnw==", - "dev": true, - "dependencies": { - "@testing-library/dom": "^9.0.0", - "@testing-library/user-event": "^14.4.0", - "ts-dedent": "^2.2.0" - } - }, - "node_modules/@storybook/theming": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.6.8.tgz", - "integrity": "sha512-0ervBgeYGieifjISlFS7x5QZF9vNgLtHHlYKdkrAsACTK+VfB0JglVwFdLrgzAKxQRlVompaxl3TecFGWlvhtw==", - "dev": true, - "dependencies": { - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", - "@storybook/client-logger": "7.6.8", - "@storybook/global": "^5.0.0", - "memoizerific": "^1.11.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@storybook/types": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.8.tgz", - "integrity": "sha512-+mABX20OhwJjqULocG5Betfidwrlk+Kq+grti+LAYwYsdBwxctBNSrqK8P9r8XDFL6PbppZeExGiHKwGu6WsKQ==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.8", - "@types/babel__core": "^7.0.0", - "@types/express": "^4.7.0", - "file-system-cache": "2.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@stripe/react-stripe-js": { - "version": "1.16.5", - "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-1.16.5.tgz", - "integrity": "sha512-lVPW3IfwdacyS22pP+nBB6/GNFRRhT/4jfgAK6T2guQmtzPwJV1DogiGGaBNhiKtSY18+yS8KlHSu+PvZNclvQ==", - "dependencies": { - "prop-types": "^15.7.2" - }, - "peerDependencies": { - "@stripe/stripe-js": "^1.44.1", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@stripe/stripe-js": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-1.54.2.tgz", - "integrity": "sha512-R1PwtDvUfs99cAjfuQ/WpwJ3c92+DAMy9xGApjqlWQMj0FKQabUAys2swfTRNzuYAYJh7NqK2dzcYVNkKLEKUg==" - }, "node_modules/@swc/core": { - "version": "1.3.103", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.103.tgz", - "integrity": "sha512-PYtt8KzRXIFDwxeD7BA9ylmXNQ4hRVcmDVuAmL3yvL9rgx7Tn3qn6T37wiMVZnP1OjqGyhuHRPNycd+ssr+byw==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.1.tgz", + "integrity": "sha512-rQ4dS6GAdmtzKiCRt3LFVxl37FaY1cgL9kSUTnhQ2xc3fmHOd7jdJK/V4pSZMG1ruGTd0bsi34O2R0Olg9Zo/w==", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@swc/counter": "^0.1.1", - "@swc/types": "^0.1.5" + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.17" }, "engines": { "node": ">=10" @@ -8224,19 +3695,19 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.3.103", - "@swc/core-darwin-x64": "1.3.103", - "@swc/core-linux-arm-gnueabihf": "1.3.103", - "@swc/core-linux-arm64-gnu": "1.3.103", - "@swc/core-linux-arm64-musl": "1.3.103", - "@swc/core-linux-x64-gnu": "1.3.103", - "@swc/core-linux-x64-musl": "1.3.103", - "@swc/core-win32-arm64-msvc": "1.3.103", - "@swc/core-win32-ia32-msvc": "1.3.103", - "@swc/core-win32-x64-msvc": "1.3.103" + "@swc/core-darwin-arm64": "1.10.1", + "@swc/core-darwin-x64": "1.10.1", + "@swc/core-linux-arm-gnueabihf": "1.10.1", + "@swc/core-linux-arm64-gnu": "1.10.1", + "@swc/core-linux-arm64-musl": "1.10.1", + "@swc/core-linux-x64-gnu": "1.10.1", + "@swc/core-linux-x64-musl": "1.10.1", + "@swc/core-win32-arm64-msvc": "1.10.1", + "@swc/core-win32-ia32-msvc": "1.10.1", + "@swc/core-win32-x64-msvc": "1.10.1" }, "peerDependencies": { - "@swc/helpers": "^0.5.0" + "@swc/helpers": "*" }, "peerDependenciesMeta": { "@swc/helpers": { @@ -8245,13 +3716,14 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.3.103", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.103.tgz", - "integrity": "sha512-Dqqz48mvdm/3PHPPA6YeAEofkF9H5Krgqd/baPf0dXcarzng6U9Ilv2aCtDjq7dfI9jfkVCW5zuwq98PE2GEdw==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.10.1.tgz", + "integrity": "sha512-NyELPp8EsVZtxH/mEqvzSyWpfPJ1lugpTQcSlMduZLj1EASLO4sC8wt8hmL1aizRlsbjCX+r0PyL+l0xQ64/6Q==", "cpu": [ "arm64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "darwin" @@ -8261,13 +3733,14 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.3.103", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.103.tgz", - "integrity": "sha512-mhUVSCEAyFLqtrDtwr9qPbe891J8cKxq53CD873/ZsUnyasHMPyWXzTvy9qjmbYyfDIArm6fGqjF5YsDKwGGNg==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.10.1.tgz", + "integrity": "sha512-L4BNt1fdQ5ZZhAk5qoDfUnXRabDOXKnXBxMDJ+PWLSxOGBbWE6aJTnu4zbGjJvtot0KM46m2LPAPY8ttknqaZA==", "cpu": [ "x64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "darwin" @@ -8277,13 +3750,14 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.3.103", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.103.tgz", - "integrity": "sha512-rYLmwxr01ZHOI6AzooqwB0DOkMm0oU8Jznk6uutV1lHgcwyxsNiC1Css8yf77Xr/sYTvKvuTfBjThqa5H716pA==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.10.1.tgz", + "integrity": "sha512-Y1u9OqCHgvVp2tYQAJ7hcU9qO5brDMIrA5R31rwWQIAKDkJKtv3IlTHF0hrbWk1wPR0ZdngkQSJZple7G+Grvw==", "cpu": [ "arm" ], "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -8293,13 +3767,14 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.3.103", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.103.tgz", - "integrity": "sha512-w+5XFpUqxiAGUBiyRyYR28Ghddp5uVyo+dHAkCnY1u3V6RsZkY3vRwmoXT7/HxVGV7csodJ1P9Cp9VaRnNvTKA==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.10.1.tgz", + "integrity": "sha512-tNQHO/UKdtnqjc7o04iRXng1wTUXPgVd8Y6LI4qIbHVoVPwksZydISjMcilKNLKIwOoUQAkxyJ16SlOAeADzhQ==", "cpu": [ "arm64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -8309,13 +3784,14 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.3.103", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.103.tgz", - "integrity": "sha512-lS5p8ewAIar7adX6t0OrkICTcw92PXrn3ZmYyG5hvfjUg4RPQFjMfFMDQSne32ZJhGXHBf0LVm1R8wHwkcpwgA==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.10.1.tgz", + "integrity": "sha512-x0L2Pd9weQ6n8dI1z1Isq00VHFvpBClwQJvrt3NHzmR+1wCT/gcYl1tp9P5xHh3ldM8Cn4UjWCw+7PaUgg8FcQ==", "cpu": [ "arm64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -8325,13 +3801,14 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.3.103", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.103.tgz", - "integrity": "sha512-Lf2cHDoEPNB6TwexHBEZCsAO2C7beb0YljhtQS+QfjWLLVqCiwt5LRCPuKN2Bav7el9KZXOI5baXedUeFj0oFg==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.10.1.tgz", + "integrity": "sha512-yyYEwQcObV3AUsC79rSzN9z6kiWxKAVJ6Ntwq2N9YoZqSPYph+4/Am5fM1xEQYf/kb99csj0FgOelomJSobxQA==", "cpu": [ "x64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -8341,13 +3818,14 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.3.103", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.103.tgz", - "integrity": "sha512-HR1Y9iiLEO3F49P47vjbHczBza9RbdXWRWC8NpcOcGJ4Wnw0c2DLWAh416fGH3VYCF/19EuglLEXhvSj0NXGuA==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.10.1.tgz", + "integrity": "sha512-tcaS43Ydd7Fk7sW5ROpaf2Kq1zR+sI5K0RM+0qYLYYurvsJruj3GhBCaiN3gkzd8m/8wkqNqtVklWaQYSDsyqA==", "cpu": [ "x64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -8357,13 +3835,14 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.3.103", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.103.tgz", - "integrity": "sha512-3/GfROD1GPyf2hi6R0l4iZ5nrrKG8IU29hYhZCb7r0ZqhL/58kktVPlkib8X/EAJI8xbhM/NMl76h8ElrnyH5w==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.10.1.tgz", + "integrity": "sha512-D3Qo1voA7AkbOzQ2UGuKNHfYGKL6eejN8VWOoQYtGHHQi1p5KK/Q7V1ku55oxXBsj79Ny5FRMqiRJpVGad7bjQ==", "cpu": [ "arm64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" @@ -8373,13 +3852,14 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.3.103", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.103.tgz", - "integrity": "sha512-9ejEFjfgPi0ibNmtuiRbYq9p4RRV6oH1DN9XjkYM8zh2qHlpZHKQZ3n4eHS0VtJO4rEGZxL8ebcnTNs62wqJig==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.10.1.tgz", + "integrity": "sha512-WalYdFoU3454Og+sDKHM1MrjvxUGwA2oralknXkXL8S0I/8RkWZOB++p3pLaGbTvOO++T+6znFbQdR8KRaa7DA==", "cpu": [ "ia32" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" @@ -8389,13 +3869,14 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.3.103", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.103.tgz", - "integrity": "sha512-/1RvaOmZolXurWAUdnELYynVlFUiT0hj3PyTPoo+YK6+KV7er4EqUalRsoUf3zzGepQuhKFZFDpQn6Xi9kJX1A==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.1.tgz", + "integrity": "sha512-JWobfQDbTnoqaIwPKQ3DVSywihVXlQMbDuwik/dDWlj33A8oEHcjPOGs4OqcA3RHv24i+lfCQpM3Mn4FAMfacA==", "cpu": [ "x64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" @@ -8405,33 +3886,28 @@ } }, "node_modules/@swc/counter": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.2.tgz", - "integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==", - "dev": true - }, - "node_modules/@swc/helpers": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", - "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } + "license": "Apache-2.0" }, "node_modules/@swc/types": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", - "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", - "dev": true + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz", + "integrity": "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } }, "node_modules/@tailwindcss/typography": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.10.tgz", - "integrity": "sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw==", + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.15.tgz", + "integrity": "sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==", "dev": true, + "license": "MIT", "dependencies": { "lodash.castarray": "^4.4.0", "lodash.isplainobject": "^4.0.6", @@ -8439,117 +3915,302 @@ "postcss-selector-parser": "6.0.10" }, "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders" + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20" + } + }, + "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@tanstack/eslint-plugin-router": { + "version": "1.87.6", + "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-router/-/eslint-plugin-router-1.87.6.tgz", + "integrity": "sha512-HoJYMI8Jcsdk4Q357bSFykDIpmU+PCAhm9IQpbcPF+wuRITHBBivLy6poaM9X184ng6FDHUOTbt6L8ZF6dYfVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.18.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@tanstack/history": { + "version": "1.95.0", + "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.95.0.tgz", + "integrity": "sha512-w1/yWuIBqmG0Z0MPMf1OuOCce7FXyVH4L4dIA4rvpnjIUCH8qRUgloFAVg37nTMUbOmhMsY2NZDxCpKBv+CLJg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/query-core": { - "version": "4.36.1", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.36.1.tgz", - "integrity": "sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==", + "version": "5.62.7", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.62.7.tgz", + "integrity": "sha512-fgpfmwatsrUal6V+8EC2cxZIQVl9xvL7qYa03gsdsCy985UTUlS4N+/3hCzwR0PclYDqisca2AqR1BVgJGpUDA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/react-query": { - "version": "4.36.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.36.1.tgz", - "integrity": "sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==", + "version": "5.62.7", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.62.7.tgz", + "integrity": "sha512-+xCtP4UAFDTlRTYyEjLx0sRtWyr5GIk7TZjZwBu4YaNahi3Rt2oMyRqfpfVrtwsqY2sayP4iXVCwmC+ZqqFmuw==", + "license": "MIT", "dependencies": { - "@tanstack/query-core": "4.36.1", - "use-sync-external-store": "^1.2.0" + "@tanstack/query-core": "5.62.7" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-native": "*" + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-router": { + "version": "1.95.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.95.1.tgz", + "integrity": "sha512-P5x4yNhcdkYsCEoYeGZP8Q9Jlxf0WXJa4G/xvbmM905seZc9FqJqvCSRvX3dWTPOXRABhl4g+8DHqfft0c/AvQ==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.95.0", + "@tanstack/react-store": "^0.7.0", + "jsesc": "^3.0.2", + "tiny-invariant": "^1.3.3", + "tiny-warning": "^1.0.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@tanstack/react-store": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.7.0.tgz", + "integrity": "sha512-S/Rq17HaGOk+tQHV/yrePMnG1xbsKZIl/VsNWnNXt4XW+tTY8dTlvpJH2ZQ3GRALsusG5K6Q3unAGJ2pd9W/Ng==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "0.7.0", + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.1.tgz", + "integrity": "sha512-orn2QNe5tF6SqjucHJ6cKTKcRDe3GG7bcYqPNn72Yejj7noECdzgAyRfGt2pGDPemhYim3d1HIR/dgruCnLfUA==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.10.9" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/router-devtools": { + "version": "1.87.9", + "resolved": "https://registry.npmjs.org/@tanstack/router-devtools/-/router-devtools-1.87.9.tgz", + "integrity": "sha512-8IY/j8nRqpHZiyC8YgtNZ0oTCxig4TpbpebgOh1L6Vf/Y92T0zSsXX+qFXiJZXTkfhSkuaUI4BSAdQgyphmbMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-router": "^1.87.9", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@tanstack/router-generator": { + "version": "1.95.1", + "resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.95.1.tgz", + "integrity": "sha512-bUymh20C9AdtwLdZkgfx04S3N9yvm8S60xFFJu1dyYI7gn5g4aPSPYaQKYFmPhc+kQJ67ZbmWVkFBKHZ8YGYvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tanstack/virtual-file-routes": "^1.87.6", + "prettier": "^3.4.2", + "tsx": "^4.19.2", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-router": "^1.95.1" }, "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { + "@tanstack/react-router": { "optional": true } } }, - "node_modules/@tanstack/react-virtual": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.0.1.tgz", - "integrity": "sha512-IFOFuRUTaiM/yibty9qQ9BfycQnYXIDHGP2+cU+0LrFFGNhVxCXSQnaY6wkX8uJVteFEBjUondX0Hmpp7TNcag==", + "node_modules/@tanstack/router-plugin": { + "version": "1.95.1", + "resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.95.1.tgz", + "integrity": "sha512-d8iIaehb/6fAUdhHOuIP/9B9pEXLy0AWTX8yWIK9PB3r/kSShWqNDrx7Qj0O6dg3xK3yiwHSNqPQMtA03s0vag==", + "dev": true, + "license": "MIT", "dependencies": { - "@tanstack/virtual-core": "3.0.0" + "@babel/core": "^7.26.0", + "@babel/generator": "^7.26.3", + "@babel/parser": "^7.26.3", + "@babel/plugin-syntax-jsx": "^7.25.9", + "@babel/plugin-syntax-typescript": "^7.25.9", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.26.4", + "@babel/types": "^7.26.3", + "@tanstack/router-generator": "^1.95.1", + "@tanstack/virtual-file-routes": "^1.87.6", + "@types/babel__core": "^7.20.5", + "@types/babel__generator": "^7.6.8", + "@types/babel__template": "^7.4.4", + "@types/babel__traverse": "^7.20.6", + "babel-dead-code-elimination": "^1.0.8", + "chokidar": "^3.6.0", + "unplugin": "^1.16.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=12" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + "@rsbuild/core": ">=1.0.2", + "vite": ">=5.0.0 || >=6.0.0", + "webpack": ">=5.92.0" + }, + "peerDependenciesMeta": { + "@rsbuild/core": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/@tanstack/store": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.0.tgz", + "integrity": "sha512-CNIhdoUsmD2NolYuaIs8VfWM467RK6oIBAW4nPEKZhg1smZ+/CwtCdpURgp7nxSqOaV9oKkzdWD80+bC66F/Jg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/virtual-core": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.0.0.tgz", - "integrity": "sha512-SYXOBTjJb05rXa2vl55TTwO40A6wKu0R5i1qQwhJYNDIqaIGF7D0HsLw+pJAyi2OvntlEIVusx3xtbbgSUi6zg==", + "version": "3.10.9", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.9.tgz", + "integrity": "sha512-kBknKOKzmeR7lN+vSadaKWXaLS0SZZG+oqpQ/k80Q6g9REn6zRHS/ZYdrIzHnpHgy/eWs00SujveUN/GJT2qTw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, - "node_modules/@testing-library/dom": { - "version": "9.3.4", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", - "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.1.3", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, + "node_modules/@tanstack/virtual-file-routes": { + "version": "1.87.6", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-file-routes/-/virtual-file-routes-1.87.6.tgz", + "integrity": "sha512-PTpeM8SHL7AJM0pJOacFvHribbUODS51qe9NsMqku4mogh6BWObY1EeVmeGnp9o3VngAEsf+rJMs2zqIVz3WFA==", + "license": "MIT", "engines": { - "node": ">=14" + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" } }, - "node_modules/@testing-library/user-event": { - "version": "14.5.2", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", - "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", - "dev": true, + "node_modules/@tanstack/zod-adapter": { + "version": "1.91.0", + "resolved": "https://registry.npmjs.org/@tanstack/zod-adapter/-/zod-adapter-1.91.0.tgz", + "integrity": "sha512-OccU1RwZ7svIyztcWrVreurWUwaTv98wAWTjzAOS6fe8Ajl9oLT9+mvj4vdi5gn5ycKyu3xkrIPhLIp5fGLZcQ==", + "license": "MIT", "engines": { - "node": ">=12", - "npm": ">=6" + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@testing-library/dom": ">=7.21.4" + "@tanstack/react-router": ">=1.43.2", + "zod": "^3.23.8" } }, "node_modules/@types/argon2-browser": { "version": "1.18.4", "resolved": "https://registry.npmjs.org/@types/argon2-browser/-/argon2-browser-1.18.4.tgz", - "integrity": "sha512-K/PHAEKzdCY4mCRhgUTBcuTxeaJyLoPcd5pJ1UFSTb/FAPjj3TCK4EM/DvNmVtDzkQBMD5peJjtch3kVQDf4YQ==" - }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true + "integrity": "sha512-K/PHAEKzdCY4mCRhgUTBcuTxeaJyLoPcd5pJ1UFSTb/FAPjj3TCK4EM/DvNmVtDzkQBMD5peJjtch3kVQDf4YQ==", + "dev": true, + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -8563,6 +4224,7 @@ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" } @@ -8572,48 +4234,77 @@ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, + "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__traverse": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", - "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.20.7" } }, - "node_modules/@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", "dependencies": { - "@types/connect": "*", - "@types/node": "*" + "@types/d3-selection": "*" } }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", "dependencies": { - "@types/node": "*" + "@types/d3-color": "*" } }, - "node_modules/@types/cross-spawn": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.6.tgz", - "integrity": "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==", - "dev": true, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", "dependencies": { - "@types/node": "*" + "@types/d3-selection": "*" } }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/dagre": { + "version": "0.7.52", + "resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.52.tgz", + "integrity": "sha512-XKJdy+OClLk3hketHi9Qg6gTfe1F3y+UFnHxKA2rn9Dw+oXa4Gb378Ztz9HlMgZKSxpPmn4BNVh9wgkpvrK1uw==", + "license": "MIT" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -8622,494 +4313,232 @@ "@types/ms": "*" } }, - "node_modules/@types/detect-port": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/detect-port/-/detect-port-1.3.5.tgz", - "integrity": "sha512-Rf3/lB9WkDfIL9eEKaSYKc+1L/rNVYBjThk22JTqQw0YozXarX8YljFAz+HCoC6h4B4KwCMsBPZHaFezwT4BNA==", - "dev": true - }, - "node_modules/@types/doctrine": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.3.tgz", - "integrity": "sha512-w5jZ0ee+HaPOaX25X2/2oGR/7rgAQSYII7X7pp0m9KgBfMP7uKfMfTvcpl5Dj+eDBbpxKGiqE+flqDr6XTd2RA==", - "dev": true - }, - "node_modules/@types/ejs": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz", - "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", - "dev": true - }, - "node_modules/@types/emscripten": { - "version": "1.39.13", - "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.13.tgz", - "integrity": "sha512-cFq+fO/isvhvmuP/+Sl4K4jtU6E23DoivtbO4r50e3odaxAiVdbfSYRDdJ4gCdxx+3aRjhphS5ZMwIH4hFy/Cw==", - "dev": true - }, - "node_modules/@types/escodegen": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/escodegen/-/escodegen-0.0.6.tgz", - "integrity": "sha512-AjwI4MvWx3HAOaZqYsjKWyEObT9lcVV0Y0V8nXo6cXzN8ZiMxVhf6F3d/UNvXVGKrEzL/Dluc5p+y9GkzlTWig==", - "dev": true - }, "node_modules/@types/estree": { - "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", - "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", - "dev": true + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "license": "MIT" }, - "node_modules/@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "dev": true, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.17.41", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", - "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" + "@types/estree": "*" } }, "node_modules/@types/file-saver": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", - "dev": true - }, - "node_modules/@types/find-cache-dir": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@types/find-cache-dir/-/find-cache-dir-3.2.1.tgz", - "integrity": "sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw==", - "dev": true - }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", "dev": true, - "dependencies": { - "@types/node": "*" - } + "license": "MIT" }, "node_modules/@types/hast": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.9.tgz", - "integrity": "sha512-pTHyNlaMD/oKJmS+ZZUyFUcsZeBZpC0lmGquw98CqRVNgAdJZJeD7GoeLiT6Xbx5rU9VCjSt0RwEvDgzh4obFw==", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/@types/hoist-non-react-statics": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", - "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", - "dependencies": { - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0" - } - }, - "node_modules/@types/html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", - "dev": true - }, - "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "dependencies": { - "@types/istanbul-lib-report": "*" + "@types/unist": "*" } }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true - }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/jsrp": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/@types/jsrp/-/jsrp-0.2.6.tgz", "integrity": "sha512-2h3tFvkbHksiNcDiUdcJ08gXWG10fnahp30GJ2Tbt4vd4pfsbfkoKTaTbYykFoppaJ6DL3914nQ3PU1vVIlBRQ==", - "dev": true - }, - "node_modules/@types/lodash": { - "version": "4.14.202", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", - "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==" + "dev": true, + "license": "MIT" }, "node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", "dependencies": { - "@types/unist": "^2" + "@types/unist": "*" } }, - "node_modules/@types/mdx": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.10.tgz", - "integrity": "sha512-Rllzc5KHk0Al5/WANwgSPl1/CwjqCy+AZrGd78zuK+jO9aDM6ffblZ+zIjgPNAaEBmlO0RYDvLNh7wD0zKVgEg==", - "dev": true - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true - }, - "node_modules/@types/mime-types": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", - "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", - "dev": true - }, "node_modules/@types/ms": { "version": "0.7.34", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", - "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "license": "MIT" }, "node_modules/@types/node": { - "version": "18.19.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.7.tgz", - "integrity": "sha512-IGRJfoNX10N/PfrReRZ1br/7SQ+2vF/tK3KXNwzXz82D32z5dMQEoOlFew18nLSN+vMNcLY4GrKfzwi/yWI8/w==", + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, - "node_modules/@types/node-fetch": { - "version": "2.6.10", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.10.tgz", - "integrity": "sha512-PPpPK6F9ALFTn59Ka3BaL+qGuipRfxNE8qVgkp0bVixeiR2c2/L+IVOiBdu9JhhT22sWnQEp6YyHGI2b2+CMcA==", - "dev": true, - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.0" - } - }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "dev": true + "node_modules/@types/nprogress": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@types/nprogress/-/nprogress-0.2.3.tgz", + "integrity": "sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==", + "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", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" }, "node_modules/@types/picomatch": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.3.tgz", - "integrity": "sha512-Yll76ZHikRFCyz/pffKGjrCwe/le2CDwOP5F210KQo27kpRE46U2rDnzikNlVn6/ezH3Mhn46bJMTfeVTtcYMg==", - "dev": true - }, - "node_modules/@types/pretty-hrtime": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha512-nj39q0wAIdhwn7DGUyT9irmsKK1tV0bd5WFEhgpqNTMFZ8cE+jieuTphCW0tfdm47S2zVT5mr09B28b1chmQMA==", - "dev": true + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-3.0.1.tgz", + "integrity": "sha512-1MRgzpzY0hOp9pW/kLRxeQhUWwil6gnrUYd3oEpeYBqp/FexhaCPv3F8LsYr47gtUU45fO2cm1dbwkSrHEo8Uw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/prop-types": { - "version": "15.7.11", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", - "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "license": "MIT" }, - "node_modules/@types/qs": { - "version": "6.9.11", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", - "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==", - "dev": true + "node_modules/@types/qrcode": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz", + "integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } }, "node_modules/@types/raf": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", "optional": true }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true - }, "node_modules/@types/react": { - "version": "18.2.48", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.48.tgz", - "integrity": "sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==", + "version": "18.3.16", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.16.tgz", + "integrity": "sha512-oh8AMIC4Y2ciKufU8hnKgs+ufgbA/dhPTACaZPM86AbwX9QwnFtSoPWEeRUj8fge+v6kFt78BXcDhAU1SrrAsw==", + "license": "MIT", "dependencies": { "@types/prop-types": "*", - "@types/scheduler": "*", "csstype": "^3.0.2" } }, - "node_modules/@types/react-redux": { - "version": "7.1.33", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", - "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==", - "dependencies": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" + "node_modules/@types/react-dom": { + "version": "18.3.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz", + "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" } }, - "node_modules/@types/react-transition-group": { - "version": "4.4.11", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", - "integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==", + "node_modules/@types/react-helmet": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.11.tgz", + "integrity": "sha512-0QcdGLddTERotCXo3VFlUSWO3ztraw8nZ6e3zJSgG7apwV5xt+pJUS8ewPBqT4NYB1optGLprNQzFleIY84u/g==", + "dev": true, "license": "MIT", "dependencies": { "@types/react": "*" } }, - "node_modules/@types/resolve": { - "version": "1.20.6", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", - "integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==", - "dev": true - }, - "node_modules/@types/sanitize-html": { - "version": "2.9.5", - "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.9.5.tgz", - "integrity": "sha512-2Sr1vd8Dw+ypsg/oDDfZ57OMSG2Befs+l2CMyCC5bVSK3CpE7lTB2aNlbbWzazgVA+Qqfuholwom6x/mWd1qmw==", - "dev": true, - "dependencies": { - "htmlparser2": "^8.0.0" + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" } }, - "node_modules/@types/scheduler": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" - }, - "node_modules/@types/semver": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", - "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", - "dev": true - }, - "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", - "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", - "dev": true, - "dependencies": { - "@types/http-errors": "*", - "@types/mime": "*", - "@types/node": "*" - } - }, - "node_modules/@types/sinonjs__fake-timers": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", - "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", - "dev": true - }, - "node_modules/@types/sizzle": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", - "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", - "dev": true + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "optional": true }, "node_modules/@types/unist": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", - "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" - }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", - "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" - }, - "node_modules/@types/uuid": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz", - "integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==", - "dev": true - }, - "node_modules/@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "optional": true, - "dependencies": { - "@types/node": "*" - } + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", - "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.0.tgz", + "integrity": "sha512-NR2yS7qUqCL7AIxdJUQf2MKKNDVNaig/dEB0GBLU7D+ZdHgK1NoH/3wsgO3OnPVipn51tG3MAwaODEGil70WEw==", "dev": true, + "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/type-utils": "5.62.0", - "@typescript-eslint/utils": "5.62.0", - "debug": "^4.3.4", + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.18.0", + "@typescript-eslint/type-utils": "8.18.0", + "@typescript-eslint/utils": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0", "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/parser": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", - "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.0.tgz", + "integrity": "sha512-hgUZ3kTEpVzKaK3uNibExUYm6SKKOmTU2BOxBSvOYwtJEPdVQ70kZJpPjstlnhCHcuc2WGfSbpKlb/69ttyN5Q==", "dev": true, + "license": "MITClause", "dependencies": { - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/scope-manager": "8.18.0", + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/typescript-estree": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0", "debug": "^4.3.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", - "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.0.tgz", + "integrity": "sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0" + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -9117,39 +4546,37 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", - "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.0.tgz", + "integrity": "sha512-er224jRepVAVLnMF2Q7MZJCq5CsdH2oqjP4dT7K6ij09Kyd+R21r7UVJrF0buMVdZS5QRhDzpvzAxHxabQadow==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@typescript-eslint/typescript-estree": "8.18.0", + "@typescript-eslint/utils": "8.18.0", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.0.tgz", + "integrity": "sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA==", "dev": true, + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -9157,135 +4584,94 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", - "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.0.tgz", + "integrity": "sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0", + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/@typescript-eslint/utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", - "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "typescript": ">=4.8.4 <5.8.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/@typescript-eslint/typescript-estree/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": { - "yallist": "^4.0.0" + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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": ">=10" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "node_modules/@typescript-eslint/utils": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.0.tgz", + "integrity": "sha512-p6GLdY383i7h5b0Qrfbix3Vc3+J2k6QWw6UMUeY5JGfm3C5LbZ4QIZzJNoNOfgyRe0uuYKjvVOsO/jD4SJO+xg==", "dev": true, + "license": "MIT", "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.18.0", + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/typescript-estree": "8.18.0" }, "engines": { - "node": ">=10" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.0.tgz", + "integrity": "sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "8.18.0", + "eslint-visitor-keys": "^4.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -9295,12 +4681,14 @@ "node_modules/@ucast/core": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz", - "integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==" + "integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==", + "license": "Apache-2.0" }, "node_modules/@ucast/js": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.3.tgz", - "integrity": "sha512-jBBqt57T5WagkAjqfCIIE5UYVdaXYgGkOFYv2+kjq2AVpZ2RIbwCo/TujJpDlwTVluUI+WpnRpoGU2tSGlEvFQ==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.4.tgz", + "integrity": "sha512-TgG1aIaCMdcaEyckOZKQozn1hazE0w90SVdlpIJ/er8xVumE11gYAtSbw/LBeUnA4fFnFWTcw3t6reqseeH/4Q==", + "license": "Apache-2.0", "dependencies": { "@ucast/core": "^1.0.0" } @@ -9309,6 +4697,7 @@ "version": "2.4.3", "resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.3.tgz", "integrity": "sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==", + "license": "Apache-2.0", "dependencies": { "@ucast/core": "^1.4.1" } @@ -9317,6 +4706,7 @@ "version": "1.3.4", "resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.4.tgz", "integrity": "sha512-ahazOr1HtelA5AC1KZ9x0UwPMqqimvfmtSm/PRRSeKKeE5G2SCqTgwiNzO7i9jS8zA3dzXpKVPpXMkcYLnyItA==", + "license": "Apache-2.0", "dependencies": { "@ucast/core": "^1.6.1", "@ucast/js": "^3.0.0", @@ -9324,252 +4714,88 @@ } }, "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.1.tgz", + "integrity": "sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==", + "license": "ISC" }, - "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.2.tgz", + "integrity": "sha512-y0byko2b2tSVVf5Gpng1eEhX1OvPC7x8yns1Fx8jDzlJp4LS6CMkCPfLw47cjyoMrshQDoQw4qcgjsU9VvlCew==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", - "dev": true, - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", - "dev": true, - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", - "dev": true, - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", - "dev": true - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@xtuc/long": "4.2.2" - } - }, - "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 - }, - "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 - }, - "node_modules/@yarnpkg/esbuild-plugin-pnp": { - "version": "3.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@yarnpkg/esbuild-plugin-pnp/-/esbuild-plugin-pnp-3.0.0-rc.15.tgz", - "integrity": "sha512-kYzDJO5CA9sy+on/s2aIW0411AklfCi8Ck/4QDivOqsMKpStZA2SsR+X27VTggGwpStWaLrjJcDcdDMowtG8MA==", - "dev": true, - "dependencies": { - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=14.15.0" + "@swc/core": "^1.7.26" }, "peerDependencies": { - "esbuild": ">=0.10.0" + "vite": "^4 || ^5 || ^6" } }, - "node_modules/@yarnpkg/fslib": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@yarnpkg/fslib/-/fslib-2.10.3.tgz", - "integrity": "sha512-41H+Ga78xT9sHvWLlFOZLIhtU6mTGZ20pZ29EiZa97vnxdohJD2AF42rCoAoWfqUz486xY6fhjMH+DYEM9r14A==", - "dev": true, + "node_modules/@xyflow/react": { + "version": "12.4.4", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.4.4.tgz", + "integrity": "sha512-9RZ9dgKZNJOlbrXXST5HPb5TcXPOIDGondjwcjDro44OQRPl1E0ZRPTeWPGaQtVjbg4WpR4BUYwOeshNI2TuVg==", + "license": "MIT", "dependencies": { - "@yarnpkg/libzip": "^2.3.0", - "tslib": "^1.13.0" + "@xyflow/system": "0.0.52", + "classcat": "^5.0.3", + "zustand": "^4.4.0" }, - "engines": { - "node": ">=12 <14 || 14.2 - 14.9 || >14.10.0" + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" } }, - "node_modules/@yarnpkg/fslib/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "node_modules/@yarnpkg/libzip": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/libzip/-/libzip-2.3.0.tgz", - "integrity": "sha512-6xm38yGVIa6mKm/DUCF2zFFJhERh/QWp1ufm4cNUvxsONBmfPg8uZ9pZBdOmF6qFGr/HlT6ABBkCSx/dlEtvWg==", - "dev": true, + "node_modules/@xyflow/react/node_modules/zustand": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz", + "integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==", + "license": "MIT", "dependencies": { - "@types/emscripten": "^1.39.6", - "tslib": "^1.13.0" + "use-sync-external-store": "^1.2.2" }, "engines": { - "node": ">=12 <14 || 14.2 - 14.9 || >14.10.0" + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } } }, - "node_modules/@yarnpkg/libzip/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dev": true, + "node_modules/@xyflow/system": { + "version": "0.0.52", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.52.tgz", + "integrity": "sha512-pJBMaoh/GEebIABWEIxAai0yf57dm+kH7J/Br+LnLFPuJL87Fhcmm4KFWd/bCUy/kCWUg+2/yFAGY0AUHRPOnQ==", + "license": "MIT", "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" + "@types/d3-drag": "^3.0.7", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" } }, "node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -9582,98 +4808,17 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-node": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", - "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", - "dev": true, - "dependencies": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "xtend": "^4.0.2" - } - }, - "node_modules/acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/add": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/add/-/add-2.0.6.tgz", - "integrity": "sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q==" - }, - "node_modules/address": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", - "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/adjust-sourcemap-loader": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", - "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", - "dev": true, - "dependencies": { - "loader-utils": "^2.0.0", - "regex-parser": "^2.2.11" - }, - "engines": { - "node": ">=8.9" - } - }, - "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dev": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/agent-base": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", - "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", - "dev": true, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -9685,107 +4830,11 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-html-community": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", - "dev": true, - "engines": [ - "node >= 0.8.0" - ], - "bin": { - "ansi-html": "bin/ansi-html" - } - }, "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" } @@ -9794,7 +4843,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -9805,11 +4854,19 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -9818,55 +4875,44 @@ "node": ">= 8" } }, - "node_modules/app-root-dir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/app-root-dir/-/app-root-dir-1.0.2.tgz", - "integrity": "sha512-jlpIfsOoNoafl92Sz//64uQHGSyMrD2vYG5d8o2a4qGvyNCvXur7bzIsWtAC/6flI2RYAp3kv8rsfBtaLm7w0g==", - "dev": true - }, - "node_modules/arch": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", - "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "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", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, "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 + "dev": true, + "license": "MIT" }, "node_modules/argon2-browser": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/argon2-browser/-/argon2-browser-1.18.0.tgz", - "integrity": "sha512-ImVAGIItnFnvET1exhsQB7apRztcoC5TnlSqernMJDUjbc/DLq3UEYeXFrLPrlaIl8cVfwnXb6wX2KpFf2zxHw==" + "integrity": "sha512-ImVAGIItnFnvET1exhsQB7apRztcoC5TnlSqernMJDUjbc/DLq3UEYeXFrLPrlaIl8cVfwnXb6wX2KpFf2zxHw==", + "license": "MIT" }, "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dependencies": { - "sprintf-js": "~1.0.2" - } + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" }, "node_modules/aria-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz", - "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, @@ -9875,43 +4921,45 @@ } }, "node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, - "dependencies": { - "deep-equal": "^2.0.5" + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">= 0.4" } }, "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", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true - }, "node_modules/array-includes": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", - "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" }, "engines": { @@ -9921,26 +4969,41 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/array.prototype.findlastindex": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", - "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -9954,6 +5017,7 @@ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -9972,6 +5036,7 @@ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -9986,30 +5051,37 @@ } }, "node_modules/array.prototype.tosorted": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.2.tgz", - "integrity": "sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", - "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", "dev": true, + "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", "is-shared-array-buffer": "^1.0.2" }, "engines": { @@ -10019,37 +5091,30 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "dev": true, - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, "node_modules/asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", "dev": true, + "license": "MIT", "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" + "minimalistic-assert": "^1.0.0" } }, "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "dev": true, + "license": "MIT" }, "node_modules/asn1js": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz", "integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==", + "license": "BSD-3-Clause", "dependencies": { "pvtsutils": "^1.3.2", "pvutils": "^1.1.3", @@ -10064,6 +5129,7 @@ "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "is-nan": "^1.3.2", @@ -10072,81 +5138,25 @@ "util": "^0.12.5" } }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/ast-types": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", - "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", - "dev": true, - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", - "dev": true - }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", - "dev": true - }, - "node_modules/async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", - "dev": true - }, - "node_modules/asynciterator.prototype": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", - "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.3" - } + "license": "MIT", + "peer": true }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } + "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" }, @@ -10155,9 +5165,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.16", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", - "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", "dev": true, "funding": [ { @@ -10173,12 +5183,13 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "browserslist": "^4.21.10", - "caniuse-lite": "^1.0.30001538", - "fraction.js": "^4.3.6", + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -10192,10 +5203,14 @@ } }, "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==", "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -10203,286 +5218,57 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", - "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", - "dev": true - }, "node_modules/axe-core": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz", - "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==", + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.2.tgz", + "integrity": "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==", "dev": true, + "license": "MPL-2.0", + "peer": true, "engines": { "node": ">=4" } }, "node_modules/axios": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.28.0.tgz", - "integrity": "sha512-Tu7NYoGY4Yoc7I+Npf9HhUMtEEpV7ZiLH9yndTCoNhcpBH0kwcvFbzYN9/u5QKI5A6uefjsNNWaz5olJVYS62Q==", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", + "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", + "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, - "node_modules/axios-auth-refresh": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/axios-auth-refresh/-/axios-auth-refresh-3.3.6.tgz", - "integrity": "sha512-2CeBUce/SxIfFxow5/n8vApJ97yYF6qoV4gh1UrswT7aEOnlOdBLxxyhOI4IaxGs6BY0l8YujU2jlc4aCmK17Q==", - "peerDependencies": { - "axios": ">= 0.18 < 0.19.0 || >= 0.19.1" - } - }, - "node_modules/axios/node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "node_modules/axobject-query": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", - "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/b4a": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", - "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", - "dev": true - }, - "node_modules/babel-core": { - "version": "7.0.0-bridge.0", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", - "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==", - "dev": true, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-loader": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", - "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", - "dev": true, - "dependencies": { - "find-cache-dir": "^4.0.0", - "schema-utils": "^4.0.0" - }, + "license": "Apache-2.0", + "peer": true, "engines": { - "node": ">= 14.15.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0", - "webpack": ">=5" + "node": ">= 0.4" } }, - "node_modules/babel-loader/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "node_modules/babel-dead-code-elimination": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.8.tgz", + "integrity": "sha512-og6HQERk0Cmm+nTT4Od2wbPtgABXFMPaHACjbKLulZIFMkYyXZLkUGuAxdgpMJBrxyt/XFpSz++lNzjbcMnPkQ==", "dev": true, + "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/babel-loader/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, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/babel-loader/node_modules/find-cache-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", - "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", - "dev": true, - "dependencies": { - "common-path-prefix": "^3.0.0", - "pkg-dir": "^7.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/babel-loader/node_modules/find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", - "dev": true, - "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/babel-loader/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/babel-loader/node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", - "dev": true, - "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/babel-loader/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/babel-loader/node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", - "dev": true, - "dependencies": { - "p-limit": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/babel-loader/node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/babel-loader/node_modules/pkg-dir": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", - "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", - "dev": true, - "dependencies": { - "find-up": "^6.3.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/babel-loader/node_modules/schema-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", - "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/babel-loader/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/babel-plugin-add-react-displayname": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/babel-plugin-add-react-displayname/-/babel-plugin-add-react-displayname-0.0.5.tgz", - "integrity": "sha512-LY3+Y0XVDYcShHHorshrDbt4KFWL4bSeniCtl4SYZbask+Syngk1uMPCeN9+nSiZo6zX5s0RTq/J9Pnaaf/KHw==", - "dev": true - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" + "@babel/core": "^7.23.7", + "@babel/parser": "^7.23.6", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6" } }, "node_modules/babel-plugin-macros": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", @@ -10493,60 +5279,6 @@ "npm": ">=6" } }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.7.tgz", - "integrity": "sha512-LidDk/tEGDfuHW2DWh/Hgo4rmnw3cduK6ZkOI1NPFceSK3n/yAGeOsNT7FLnSGHkXj3RHGSEVkN3FsCTY6w2CQ==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.4", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.7", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.7.tgz", - "integrity": "sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA==", - "dev": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.4", - "core-js-compat": "^3.33.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.4.tgz", - "integrity": "sha512-S/x2iOCvDaCASLYsOOgWOq4bCfKYVqvO/uxjkaYyZ3rVsVE3CeAI/c84NpyuBBymEgNvHgjEot3a9/Z/kXvqsg==", - "dev": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.4" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-styled-components": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz", - "integrity": "sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", - "@babel/plugin-syntax-jsx": "^7.22.5", - "lodash": "^4.17.21", - "picomatch": "^2.3.1" - }, - "peerDependencies": { - "styled-components": ">= 2" - } - }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -10560,49 +5292,14 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/bare-events": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.0.tgz", - "integrity": "sha512-Yyyqff4PIFfSuthCZqLlPISTWHmnQxoPuAvkmgzsJEmG3CesdIv6Xweayl0JkCZJSB2yYIdJyEz97tpxNhgjbg==", "dev": true, - "optional": true - }, - "node_modules/bare-fs": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.1.5.tgz", - "integrity": "sha512-5t0nlecX+N2uJqdxe9d18A98cp2u9BETelbjKpiVgQqzzmVNFYWEAjQHqS+2Khgto1vcwhik9cXucaj5ve2WWA==", - "dev": true, - "optional": true, - "dependencies": { - "bare-events": "^2.0.0", - "bare-os": "^2.0.0", - "bare-path": "^2.0.0", - "streamx": "^2.13.0" - } - }, - "node_modules/bare-os": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.2.0.tgz", - "integrity": "sha512-hD0rOPfYWOMpVirTACt4/nK8mC55La12K5fY1ij8HAdfQakD62M+H4o4tpfKzVGLgRDTuk3vjA4GqGXXCeFbag==", - "dev": true, - "optional": true - }, - "node_modules/bare-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.0.tgz", - "integrity": "sha512-DIIg7ts8bdRKwJRJrUMy/PICEaQZaPGZ26lsSx9MJSwIhSrcdHn7/C8W+XmnG/rKi6BaRcz+JO00CjZteybDtw==", - "dev": true, - "optional": true, - "dependencies": { - "bare-os": "^2.1.0" - } + "license": "MIT" }, "node_modules/base64-arraybuffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", "optional": true, "engines": { "node": ">= 0.6.0" @@ -10626,192 +5323,41 @@ "type": "consulting", "url": "https://feross.org/support" } - ] - }, - "node_modules/base64-loader": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/base64-loader/-/base64-loader-1.0.0.tgz", - "integrity": "sha512-p32+F8dg+ANGx7s8QsZS74ZPHfIycmC2yZcoerzFgbersIYWitPbbF39G6SBx3gyvzyLH5nt1ooocxr0IHuWKA==" - }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "dev": true, - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, - "node_modules/bcrypt-pbkdf/node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "dev": true + ], + "license": "MIT" }, "node_modules/before-after-hook": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", - "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" - }, - "node_modules/better-opn": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz", - "integrity": "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==", - "dev": true, - "dependencies": { - "open": "^8.0.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/big-integer": { - "version": "1.6.52", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", - "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true, - "engines": { - "node": "*" - } + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", + "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", + "license": "Apache-2.0" }, "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" - } - }, - "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, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bl/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, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" }, - "engines": { - "node": ">= 6" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/blob-util": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", - "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", - "dev": true - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true - }, "node_modules/bn.js": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true - }, - "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dev": true, - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dev": true, - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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 - }, - "node_modules/bplist-parser": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", - "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", - "dev": true, - "dependencies": { - "big-integer": "^1.6.44" - }, - "engines": { - "node": ">= 5.10.0" - } + "license": "MIT" }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -10822,6 +5368,7 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -10833,19 +5380,25 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/browser-assert": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/browser-assert/-/browser-assert-1.2.1.tgz", - "integrity": "sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==", - "dev": true + "node_modules/browser-resolve": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-2.0.0.tgz", + "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.17.0" + } }, "node_modules/browserify-aes": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "dev": true, + "license": "MIT", "dependencies": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", @@ -10860,6 +5413,7 @@ "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", "dev": true, + "license": "MIT", "dependencies": { "browserify-aes": "^1.0.4", "browserify-des": "^1.0.0", @@ -10871,6 +5425,7 @@ "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", "dev": true, + "license": "MIT", "dependencies": { "cipher-base": "^1.0.1", "des.js": "^1.0.0", @@ -10879,62 +5434,118 @@ } }, "node_modules/browserify-rsa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", - "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", "dev": true, + "license": "MIT", "dependencies": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" } }, "node_modules/browserify-sign": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz", - "integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz", + "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==", "dev": true, + "license": "ISC", "dependencies": { "bn.js": "^5.2.1", "browserify-rsa": "^4.1.0", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "elliptic": "^6.5.4", + "elliptic": "^6.5.5", + "hash-base": "~3.0", "inherits": "^2.0.4", - "parse-asn1": "^5.1.6", - "readable-stream": "^3.6.2", + "parse-asn1": "^5.1.7", + "readable-stream": "^2.3.8", "safe-buffer": "^5.2.1" }, "engines": { - "node": ">= 4" + "node": ">= 0.12" } }, - "node_modules/browserify-sign/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/browserify-sign/node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", "dev": true, + "license": "MIT", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" }, "engines": { - "node": ">= 6" + "node": ">= 0.10" } }, + "node_modules/browserify-sign/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/browserify-sign/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==", + "dev": true, + "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/browserify-sign/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/browserify-sign/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/browserify-sign/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==", + "dev": true, + "license": "MIT" + }, "node_modules/browserify-zlib": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", "dev": true, + "license": "MIT", "dependencies": { "pako": "~1.0.5" } }, "node_modules/browserslist": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", - "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "dev": true, "funding": [ { "type": "opencollective", @@ -10949,11 +5560,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001565", - "electron-to-chromium": "^1.4.601", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -10962,19 +5574,11 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "dependencies": { - "node-int64": "^0.4.0" - } - }, "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" }, @@ -11001,64 +5605,71 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/buffer-from": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-0.1.2.tgz", - "integrity": "sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg==" - }, "node_modules/buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/builtin-status-codes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", - "dev": true - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cachedir": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", - "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", - "dev": true, - "engines": { - "node": ">=6" - } + "license": "MIT" }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "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": { - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "get-intrinsic": "^1.2.5" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -11068,25 +5679,16 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/camel-case": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", - "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", - "dev": true, - "dependencies": { - "pascal-case": "^3.1.2", - "tslib": "^2.0.3" - } - }, "node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -11096,22 +5698,16 @@ "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/camelize": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", - "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/caniuse-lite": { - "version": "1.0.30001577", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001577.tgz", - "integrity": "sha512-rs2ZygrG1PNXMfmncM0B5H1hndY5ZCC9b5TkFaVNfZ+AUlyqcMyVIQtc3fsezi0NUCk5XZfDf9WS6WxMxnfdrg==", + "version": "1.0.30001688", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001688.tgz", + "integrity": "sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA==", + "dev": true, "funding": [ { "type": "opencollective", @@ -11125,12 +5721,14 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/canvg": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz", - "integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==", + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", "optional": true, "dependencies": { "@babel/runtime": "^7.12.5", @@ -11150,28 +5748,24 @@ "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", "optional": true }, - "node_modules/case-sensitive-paths-webpack-plugin": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", - "integrity": "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==", - "dev": true, - "engines": { - "node": ">=4" + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", - "dev": true - }, "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" @@ -11192,26 +5786,39 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/check-more-types": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", - "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", - "dev": true, - "engines": { - "node": ">= 0.8.0" + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -11224,6 +5831,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -11233,6 +5843,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -11240,211 +5851,96 @@ "node": ">= 6" } }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "dev": true, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "engines": { - "node": ">=8" - } - }, "node_modules/cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz", + "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==", + "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, - "dependencies": { - "consola": "^3.2.3" - } + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" - }, - "node_modules/clean-css": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", - "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", - "dev": true, - "dependencies": { - "source-map": "~0.6.0" - }, - "engines": { - "node": ">= 10.0" - } - }, - "node_modules/clean-css/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "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, - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-table3": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", - "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0" - }, - "engines": { - "node": "10.* || >= 12.*" - }, - "optionalDependencies": { - "@colors/colors": "1.5.0" - } - }, - "node_modules/cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, - "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, - "engines": { - "node": ">=0.8" + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" } }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/clone-deep/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", "dependencies": { - "isobject": "^3.0.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/clsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", - "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -11455,27 +5951,14 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -11493,178 +5976,28 @@ } }, "node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } }, - "node_modules/common-path-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", - "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", - "dev": true - }, - "node_modules/common-tags": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", - "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", - "dev": true, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dev": true, - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "dev": true, - "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/compression/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/compression/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==", - "dev": true - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "dev": true, - "engines": [ - "node >= 0.8" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/concat-stream/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 - }, - "node_modules/concat-stream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, - "node_modules/concat-stream/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==", - "dev": true, - "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/concat-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==", - "dev": true - }, - "node_modules/concat-stream/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==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/confbox": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", - "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==", - "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", "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", - "dev": true - }, - "node_modules/consola": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", - "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", "dev": true, - "engines": { - "node": "^14.18.0 || >=16.10.0" - } + "license": "MIT" }, "node_modules/console-browserify": { "version": "1.2.0", @@ -11676,104 +6009,22 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", - "dev": true - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dev": true, - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, - "engines": { - "node": ">= 0.6" - } + "license": "MIT" }, "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" - }, - "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "dev": true - }, - "node_modules/cookies": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", - "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", - "dependencies": { - "depd": "~2.0.0", - "keygrip": "~1.1.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/copy-anything": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", - "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", - "dev": true, - "peer": true, - "dependencies": { - "is-what": "^3.14.1" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" - } + "license": "MIT" }, "node_modules/core-js": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.0.tgz", - "integrity": "sha512-ntakECeqg81KqMueeGJ79Q5ZgQNR+6eaE8sxGCx62zMbAIj65q+uYvatToew3m6eAGdU4gNZwpZ34NMe4GYswg==", - "hasInstallScript": true, - "optional": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-compat": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.0.tgz", - "integrity": "sha512-5blwFAddknKeNgsjBzilkdQ0+YK8L1PfqPYq40NOYMYFSS38qj+hpTcLLWwpIwA2A5bje/x5jmVn2tzUMg9IVw==", - "dev": true, - "dependencies": { - "browserslist": "^4.22.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-pure": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.35.0.tgz", - "integrity": "sha512-f+eRYmkou59uh7BPcyJ8MC76DiGhspj1KMxVIcF24tzP8NA9HVa1uC7BTW2tgx7E1QVCzDzsgp7kArrzhlz8Ew==", - "dev": true, + "version": "3.39.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz", + "integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==", "hasInstallScript": true, + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" @@ -11782,12 +6033,15 @@ "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", @@ -11803,6 +6057,7 @@ "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", "engines": { "node": ">= 6" } @@ -11812,21 +6067,24 @@ "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", "dev": true, + "license": "MIT", "dependencies": { "bn.js": "^4.1.0", "elliptic": "^6.5.3" } }, "node_modules/create-ecdh/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "dev": true, + "license": "MIT" }, "node_modules/create-hash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "license": "MIT", "dependencies": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", @@ -11840,6 +6098,7 @@ "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "dev": true, + "license": "MIT", "dependencies": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", @@ -11849,19 +6108,28 @@ "sha.js": "^2.4.8" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-fetch": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "license": "MIT", "dependencies": { "node-fetch": "^2.6.12" } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -11872,215 +6140,62 @@ } }, "node_modules/crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", + "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", "dev": true, + "license": "MIT", "dependencies": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" + "browserify-cipher": "^1.0.1", + "browserify-sign": "^4.2.3", + "create-ecdh": "^4.0.4", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" }, "engines": { - "node": "*" + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "node_modules/crypto-browserify/node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/css-box-model": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", - "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "license": "MIT", "dependencies": { - "tiny-invariant": "^1.0.6" - } - }, - "node_modules/css-color-keywords": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", - "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, "engines": { - "node": ">=4" + "node": ">= 0.10" } }, "node_modules/css-line-break": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", "optional": true, "dependencies": { "utrie": "^1.0.2" } }, - "node_modules/css-loader": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.9.0.tgz", - "integrity": "sha512-3I5Nu4ytWlHvOP6zItjiHlefBNtrH+oehq8tnQa2kO305qpVyx9XNIT1CXIj5bgCJs7qICBCkgCYxQLKPANoLA==", - "dev": true, - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.31", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.3", - "postcss-modules-scope": "^3.1.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/css-loader/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/css-loader/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/css-loader/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-select/node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dev": true, - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/css-select/node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "dev": true, - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/css-select/node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/css-select/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/css-to-react-native": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", - "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", - "dependencies": { - "camelize": "^1.0.0", - "css-color-keywords": "^1.0.0", - "postcss-value-parser": "^4.0.2" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true, - "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" }, @@ -12091,191 +6206,206 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" }, "node_modules/cva": { "name": "class-variance-authority", - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.4.0.tgz", - "integrity": "sha512-74enNN8O9ZNieycac/y8FxqgyzZhZbxmCitAtAeUrLPlxjSd5zA7LfpprmxEcOmQBnaGs5hYhiSGnJ0mqrtBLQ==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, "funding": { - "url": "https://joebell.co.uk" + "url": "https://polar.sh/cva" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" }, "peerDependencies": { - "typescript": ">= 4.5.5 < 5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "d3-selection": "2 - 3" } }, - "node_modules/cypress": { - "version": "13.6.2", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.2.tgz", - "integrity": "sha512-TW3bGdPU4BrfvMQYv1z3oMqj71YI4AlgJgnrycicmPZAXtvywVFZW9DAToshO65D97rCWfG/kqMFsYB6Kp91gQ==", - "dev": true, - "hasInstallScript": true, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", "dependencies": { - "@cypress/request": "^3.0.0", - "@cypress/xvfb": "^1.2.4", - "@types/node": "^18.17.5", - "@types/sinonjs__fake-timers": "8.1.1", - "@types/sizzle": "^2.3.2", - "arch": "^2.2.0", - "blob-util": "^2.0.2", - "bluebird": "^3.7.2", - "buffer": "^5.6.0", - "cachedir": "^2.3.0", - "chalk": "^4.1.0", - "check-more-types": "^2.24.0", - "cli-cursor": "^3.1.0", - "cli-table3": "~0.6.1", - "commander": "^6.2.1", - "common-tags": "^1.8.0", - "dayjs": "^1.10.4", - "debug": "^4.3.4", - "enquirer": "^2.3.6", - "eventemitter2": "6.4.7", - "execa": "4.1.0", - "executable": "^4.1.1", - "extract-zip": "2.0.1", - "figures": "^3.2.0", - "fs-extra": "^9.1.0", - "getos": "^3.2.1", - "is-ci": "^3.0.0", - "is-installed-globally": "~0.4.0", - "lazy-ass": "^1.6.0", - "listr2": "^3.8.3", - "lodash": "^4.17.21", - "log-symbols": "^4.0.0", - "minimist": "^1.2.8", - "ospath": "^1.2.2", - "pretty-bytes": "^5.6.0", - "process": "^0.11.10", - "proxy-from-env": "1.0.0", - "request-progress": "^3.0.0", - "semver": "^7.5.3", - "supports-color": "^8.1.1", - "tmp": "~0.2.1", - "untildify": "^4.0.0", - "yauzl": "^2.10.0" - }, - "bin": { - "cypress": "bin/cypress" + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" }, "engines": { - "node": "^16.0.0 || ^18.0.0 || >=20.0.0" + "node": ">=12" } }, - "node_modules/cypress/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cypress/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cypress/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cypress/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, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/cypress/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", - "dev": true - }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "dev": true, + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "license": "MIT", "dependencies": { - "assert-plus": "^1.0.0" + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" }, "engines": { - "node": ">=0.10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "dependencies": { - "@babel/runtime": "^7.21.0" - }, - "engines": { - "node": ">=0.11" - }, + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" } }, - "node_modules/dayjs": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==", - "dev": true - }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -12286,10 +6416,14 @@ } } }, - "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==" + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, "node_modules/decode-named-character-reference": { "version": "1.0.2", @@ -12303,59 +6437,23 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "MIT" }, - "node_modules/dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", - "dev": true - }, - "node_modules/deep-equal": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", - "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "node_modules/define-data-property": { + "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": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -12364,85 +6462,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/default-browser-id": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", - "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", - "dev": true, - "dependencies": { - "bplist-parser": "^0.2.0", - "untildify": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dev": true, - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -12455,64 +6480,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/defined": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", - "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/defu": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", - "dev": true - }, - "node_modules/del": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", - "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", - "dev": true, - "dependencies": { - "globby": "^11.0.1", - "graceful-fs": "^4.2.4", - "is-glob": "^4.0.1", - "is-path-cwd": "^2.2.0", - "is-path-inside": "^3.0.2", - "p-map": "^4.0.0", - "rimraf": "^3.0.2", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", "engines": { "node": ">=0.4.0" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/deprecation": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" - }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -12526,152 +6502,43 @@ "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-indent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", - "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", - "engines": { - "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==" + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" }, - "node_modules/detect-package-manager": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-package-manager/-/detect-package-manager-2.0.1.tgz", - "integrity": "sha512-j/lJHyoLlWi6G1LDdLgvUtz60Zo5GEj+sVYtTVXnYLDPuzgC3llMxonXym9zIwhhUII8vjdw0LXxavpLqTbl1A==", - "dev": true, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", "dependencies": { - "execa": "^5.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/detect-package-manager/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" + "dequal": "^2.0.0" }, "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/detect-package-manager/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/detect-package-manager/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/detect-port": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.6.1.tgz", - "integrity": "sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==", - "dev": true, - "dependencies": { - "address": "^1.0.1", - "debug": "4" - }, - "bin": { - "detect": "bin/detect-port.js", - "detect-port": "bin/detect-port.js" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/detective": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", - "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", - "dev": true, - "dependencies": { - "acorn-node": "^1.8.2", - "defined": "^1.0.0", - "minimist": "^1.2.6" - }, - "bin": { - "detective": "bin/detective.js" - }, - "engines": { - "node": ">=0.8.0" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "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 - }, - "node_modules/diff": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", - "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", - "engines": { - "node": ">=0.3.1" - } + "dev": true, + "license": "Apache-2.0" }, "node_modules/diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", "dev": true, + "license": "MIT", "dependencies": { "bn.js": "^4.1.0", "miller-rabin": "^4.0.0", @@ -12679,34 +6546,31 @@ } }, "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } + "license": "MIT" + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -12714,21 +6578,6 @@ "node": ">=6.0.0" } }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true - }, - "node_modules/dom-converter": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", - "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", - "dev": true, - "dependencies": { - "utila": "~0.4" - } - }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -12739,24 +6588,12 @@ "csstype": "^3.0.2" } }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, "node_modules/domain-browser": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.23.0.tgz", - "integrity": "sha512-ArzcM/II1wCCujdCNyQjXrAFwS4mrLh4C7DZWlaI8mdh7h3BfKdNd3bKXITfl2PT9FtfQqaGvhi1vPRQPimjGA==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz", + "integrity": "sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -12764,222 +6601,49 @@ "url": "https://bevry.me/fund" } }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, "node_modules/dompurify": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.6.tgz", - "integrity": "sha512-zUTaUBO8pY4+iJMPE1B9XlO2tXVYIcEA4SNGtvDELzTSCQO7RzH+j7S180BmhmJId78lqGU2z19vgVx2Sxs/PQ==", - "license": "(MPL-2.0 OR Apache-2.0)", - "optional": true - }, - "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz", + "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" } }, - "node_modules/dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dev": true, - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" - } - }, - "node_modules/dotenv-expand": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", - "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", - "dependencies": { - "readable-stream": "^2.0.2" - } - }, - "node_modules/duplexer2/node_modules/isarray": { + "node_modules/dunder-proto": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/duplexer2/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==", - "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/duplexer2/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==" - }, - "node_modules/duplexer2/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==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz", + "integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==", "dev": true, + "license": "MIT", "dependencies": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "node_modules/duplexify/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, - "node_modules/duplexify/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==", - "dev": true, - "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/duplexify/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==", - "dev": true - }, - "node_modules/duplexify/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==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" + "call-bind-apply-helpers": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "dev": true, - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "node_modules/ecc-jsbn/node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", - "dev": true - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dev": true, - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.4.632", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.632.tgz", - "integrity": "sha512-JGmudTwg7yxMYvR/gWbalqqQiyu7WTFv2Xu3vw4cJHXPFxNgAk0oy8UHaer8nLF4lZJa+rNoj6GsrKIVJTV6Tw==" + "version": "1.5.73", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.73.tgz", + "integrity": "sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg==", + "dev": true, + "license": "ISC" }, "node_modules/elliptic": { - "version": "6.5.7", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz", - "integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", "dev": true, + "license": "MIT", "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", @@ -12991,60 +6655,25 @@ } }, "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "dev": true, + "license": "MIT" }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/endent": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/endent/-/endent-2.1.0.tgz", - "integrity": "sha512-r8VyPX7XL8U01Xgnb1CjZ3XV+z90cXIJ9JPE/R9SEC9vpw2P6CfsRPJmp20DppC5N7ZAMCmjYkJIa744Iyg96w==", - "dev": true, - "dependencies": { - "dedent": "^0.7.0", - "fast-json-parse": "^1.0.3", - "objectorarray": "^1.0.5" - } + "license": "MIT" }, "node_modules/enhanced-resolve": { "version": "5.17.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -13053,19 +6682,6 @@ "node": ">=10.13.0" } }, - "node_modules/enquirer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", - "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", - "dev": true, - "dependencies": { - "ansi-colors": "^4.1.1", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -13077,94 +6693,68 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/envinfo": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz", - "integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==", - "dev": true, - "bin": { - "envinfo": "dist/cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/errno": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", - "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "prr": "~1.0.1" - }, - "bin": { - "errno": "cli.js" - } - }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } }, - "node_modules/error-stack-parser": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", - "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", - "dev": true, - "dependencies": { - "stackframe": "^1.3.4" - } - }, "node_modules/es-abstract": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", - "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", + "version": "1.23.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.5.tgz", + "integrity": "sha512-vlmniQ0WNPwXqA0BnmwV3Ng7HxiGlh6r5U6JcTMNx8OilcAGqVJBHJcPjqOMaczU9fRuRK5Px2BdVyPRnKMMVQ==", "dev": true, + "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.5", - "es-set-tostringtag": "^2.0.1", + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", "es-to-primitive": "^1.2.1", "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.2", - "get-symbol-description": "^1.0.0", - "globalthis": "^1.0.3", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", "has-symbols": "^1.0.3", - "hasown": "^2.0.0", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", + "is-shared-array-buffer": "^1.0.3", "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", + "is-typed-array": "^1.1.13", "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", + "object-inspect": "^1.13.3", "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.13" + "which-typed-array": "^1.1.15" }, "engines": { "node": ">= 0.4" @@ -13173,63 +6763,77 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" } }, "node_modules/es-iterator-helpers": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", - "integrity": "sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.0.tgz", + "integrity": "sha512-tpxqxncxnpw3c93u8n3VOzACmRFoVmWJqbWXvX/JfKbkhBw1oslgPrUfeSt2psuqyEJFD6N/9lg5i7bsKpoq+Q==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "asynciterator.prototype": "^1.0.0", - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.22.1", - "es-set-tostringtag": "^2.0.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.2.1", - "globalthis": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "iterator.prototype": "^1.1.2", - "safe-array-concat": "^1.0.1" + "internal-slot": "^1.0.7", + "iterator.prototype": "^1.1.3", + "safe-array-concat": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" } }, - "node_modules/es-module-lexer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", - "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", - "dev": true + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/es-set-tostringtag": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", - "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.2", - "has-tostringtag": "^1.0.0", - "hasown": "^2.0.0" + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" }, "engines": { "node": ">= 0.4" @@ -13240,19 +6844,21 @@ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", "dev": true, + "license": "MIT", "dependencies": { "hasown": "^2.0.0" } }, "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, + "license": "MIT", "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" }, "engines": { "node": ">= 0.4" @@ -13262,11 +6868,12 @@ } }, "node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -13274,66 +6881,46 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, - "node_modules/esbuild-plugin-alias": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/esbuild-plugin-alias/-/esbuild-plugin-alias-0.2.1.tgz", - "integrity": "sha512-jyfL/pwPqaFXyKnj8lP8iLk6Z0m099uXR45aSN8Av1XD4vhvQutxxPzgA2bTcAwQpa1zCXDcWOlhFgyP3GKqhQ==", - "dev": true - }, - "node_modules/esbuild-register": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.5.0.tgz", - "integrity": "sha512-+4G/XmakeBAsvJuDugJvtyF1x+XJT4FMocynNpxrvEBViirpfUn2PgNpCHedfWhF4WokNsO/OvMKrmJOIJsI5A==", - "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, - "peerDependencies": { - "esbuild": ">=0.12 <1" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -13341,48 +6928,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/escodegen/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -13432,6 +6990,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz", "integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==", "dev": true, + "license": "MIT", "dependencies": { "eslint-config-airbnb-base": "^15.0.0", "object.assign": "^4.1.2", @@ -13453,6 +7012,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", "dev": true, + "license": "MIT", "dependencies": { "confusing-browser-globals": "^1.0.10", "object.assign": "^4.1.2", @@ -13467,52 +7027,22 @@ "eslint-plugin-import": "^2.25.2" } }, - "node_modules/eslint-config-airbnb-typescript": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-17.1.0.tgz", - "integrity": "sha512-GPxI5URre6dDpJ0CtcthSZVBAfI+Uw7un5OYNVxP2EYi3H81Jw701yFP7AU+/vCE7xBtFmjge7kfhhk4+RAiig==", + "node_modules/eslint-config-airbnb-base/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "dependencies": { - "eslint-config-airbnb-base": "^15.0.0" - }, - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^5.13.0 || ^6.0.0", - "@typescript-eslint/parser": "^5.0.0 || ^6.0.0", - "eslint": "^7.32.0 || ^8.2.0", - "eslint-plugin-import": "^2.25.3" - } - }, - "node_modules/eslint-config-next": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.5.6.tgz", - "integrity": "sha512-o8pQsUHTo9aHqJ2YiZDym5gQAMRf7O2HndHo/JZeY7TDD+W4hk6Ma8Vw54RHiBeb7OWWO5dPirQB+Is/aVQ7Kg==", - "dev": true, - "dependencies": { - "@next/eslint-plugin-next": "13.5.6", - "@rushstack/eslint-patch": "^1.3.3", - "@typescript-eslint/parser": "^5.4.2 || ^6.0.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" - }, - "peerDependencies": { - "eslint": "^7.23.0 || ^8.0.0", - "typescript": ">=3.3.1" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, "node_modules/eslint-config-prettier": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", - "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, + "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -13525,6 +7055,7 @@ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", @@ -13536,23 +7067,26 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, "node_modules/eslint-import-resolver-typescript": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz", - "integrity": "sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.7.0.tgz", + "integrity": "sha512-Vrwyi8HHxY97K5ebydMtffsWAn1SCR9eol49eCd5fJS4O1WV7PaAjbcjmbfJJSMz/t4Mal212Uz/fQZrOB8mow==", "dev": true, + "license": "ISC", "dependencies": { - "debug": "^4.3.4", - "enhanced-resolve": "^5.12.0", - "eslint-module-utils": "^2.7.4", - "fast-glob": "^3.3.1", - "get-tsconfig": "^4.5.0", - "is-core-module": "^2.11.0", - "is-glob": "^4.0.3" + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.3.7", + "enhanced-resolve": "^5.15.0", + "fast-glob": "^3.3.2", + "get-tsconfig": "^4.7.5", + "is-bun-module": "^1.0.2", + "is-glob": "^4.0.3", + "stable-hash": "^0.0.4" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -13562,14 +7096,24 @@ }, "peerDependencies": { "eslint": "*", - "eslint-plugin-import": "*" + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } } }, "node_modules/eslint-module-utils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", - "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^3.2.7" }, @@ -13587,39 +7131,43 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, "node_modules/eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, + "license": "MIT", "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", "array.prototype.flat": "^1.3.2", "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "node_modules/eslint-plugin-import/node_modules/debug": { @@ -13627,6 +7175,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } @@ -13636,6 +7185,7 @@ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -13643,125 +7193,118 @@ "node": ">=0.10.0" } }, - "node_modules/eslint-plugin-import/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "dependencies": { - "minimist": "^1.2.0" - }, + "license": "ISC", "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/eslint-plugin-import/node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" + "semver": "bin/semver.js" } }, "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz", - "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==", + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "@babel/runtime": "^7.23.2", - "aria-query": "^5.3.0", - "array-includes": "^3.1.7", + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", - "axe-core": "=4.7.0", - "axobject-query": "^3.2.1", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", - "es-iterator-helpers": "^1.0.15", - "hasown": "^2.0.0", + "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", - "object.entries": "^1.1.7", - "object.fromentries": "^2.0.7" + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" }, "engines": { "node": ">=4.0" }, "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "dependencies": { - "dequal": "^2.0.3" + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "node_modules/eslint-plugin-prettier": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", - "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", "dev": true, + "license": "MIT", "dependencies": { - "prettier-linter-helpers": "^1.0.0" + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" }, "engines": { - "node": ">=12.0.0" + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" }, "peerDependencies": { - "eslint": ">=7.28.0", - "prettier": ">=2.0.0" + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" }, "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, "eslint-config-prettier": { "optional": true } } }, "node_modules/eslint-plugin-react": { - "version": "7.33.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", - "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", + "version": "7.37.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.2.tgz", + "integrity": "sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flatmap": "^1.3.1", - "array.prototype.tosorted": "^1.1.1", + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.12", + "es-iterator-helpers": "^1.1.0", "estraverse": "^5.3.0", + "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.6", - "object.fromentries": "^2.0.6", - "object.hasown": "^1.1.2", - "object.values": "^1.1.6", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.0", "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.4", + "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.8" + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", - "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -13769,11 +7312,23 @@ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" } }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.16.tgz", + "integrity": "sha512-slterMlxAhov/DZO8NScf6mEeMBBXodFUolijDvrtTxyezyLoTQaa73FyYus/VbTdftd8wBgBxPMRk3poleXNQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, "node_modules/eslint-plugin-react/node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, + "license": "Apache-2.0", + "peer": true, "dependencies": { "esutils": "^2.0.2" }, @@ -13786,6 +7341,8 @@ "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -13798,69 +7355,37 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-plugin-simple-import-sort": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-8.0.0.tgz", - "integrity": "sha512-bXgJQ+lqhtQBCuWY/FUWdB27j4+lqcvXv5rUARkzbeWLwea+S5eBZEQrhnO+WgX3ZoJHVj0cn943iyXwByHHQw==", + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-simple-import-sort": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz", + "integrity": "sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==", + "dev": true, + "license": "MIT", "peerDependencies": { "eslint": ">=5.0.0" } }, - "node_modules/eslint-plugin-storybook": { - "version": "0.6.15", - "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-0.6.15.tgz", - "integrity": "sha512-lAGqVAJGob47Griu29KXYowI4G7KwMoJDOkEip8ujikuDLxU+oWJ1l0WL6F2oDO4QiyUFXvtDkEkISMOPzo+7w==", - "dev": true, - "dependencies": { - "@storybook/csf": "^0.0.1", - "@typescript-eslint/utils": "^5.45.0", - "requireindex": "^1.1.0", - "ts-dedent": "^2.2.0" - }, - "engines": { - "node": "12.x || 14.x || >= 16" - }, - "peerDependencies": { - "eslint": ">=6" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/@storybook/csf": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.0.1.tgz", - "integrity": "sha512-USTLkZze5gkel8MYCujSRBVIrUQ3YPBrLOx7GNk/0wttvVtlzWXAq9eLbQ4p/NicGxP+3T7KPEMVV//g+yubpw==", - "dev": true, - "dependencies": { - "lodash": "^4.17.15" - } - }, "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==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" + "estraverse": "^5.2.0" }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/eslint-scope/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -13868,21 +7393,59 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -13895,6 +7458,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -13905,35 +7469,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/eslint/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -13946,35 +7487,25 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/espree/node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "bin": { - "acorn": "bin/acorn" - }, + "license": "Apache-2.0", "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, - "engines": { - "node": ">=4" + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -13987,6 +7518,7 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -13999,53 +7531,43 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/eventemitter2": { - "version": "6.4.7", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", - "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", - "dev": true - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" - }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.x" } @@ -14055,200 +7577,53 @@ "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", "dev": true, + "license": "MIT", "dependencies": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" } }, - "node_modules/execa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", - "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/executable": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", - "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", - "dev": true, - "dependencies": { - "pify": "^2.2.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", - "dev": true, - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.2", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/express/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dev": true, - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, - "node_modules/extend-shallow": { + "node_modules/fast-content-type-parse": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", - "dev": true, - "engines": [ - "node >=0.6.0" - ] + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", + "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-diff": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true - }, - "node_modules/fast-equals": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz", - "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==" - }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -14265,6 +7640,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -14272,91 +7648,42 @@ "node": ">= 6" } }, - "node_modules/fast-json-parse": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fast-json-parse/-/fast-json-parse-1.0.3.tgz", - "integrity": "sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw==", - "dev": true - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fastq": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", - "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/fetch-retry": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-5.0.6.tgz", - "integrity": "sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==", - "dev": true - }, "node_modules/fflate": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", - "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==" - }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/figures/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, + "license": "MIT", "dependencies": { "flat-cache": "^3.0.4" }, @@ -14367,67 +7694,15 @@ "node_modules/file-saver": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", - "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" - }, - "node_modules/file-system-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/file-system-cache/-/file-system-cache-2.3.0.tgz", - "integrity": "sha512-l4DMNdsIPsVnKrgEXbJwDJsA5mB8rGwHYERMgqQx/xAUtChPJMre1bXBzDEqqVbWv9AIbFezXMxeEkZDSrXUOQ==", - "dev": true, - "dependencies": { - "fs-extra": "11.1.1", - "ramda": "0.29.0" - } - }, - "node_modules/file-system-cache/node_modules/fs-extra": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", - "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dev": true, - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/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, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", + "license": "MIT" }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -14435,139 +7710,18 @@ "node": ">=8" } }, - "node_modules/filter-obj": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-2.0.2.tgz", - "integrity": "sha512-lO3ttPjHZRfjMcxWKb1j1eDhTFsu4meeR3lnMcnBFhk6RuLhvEiuALu2TlfL310ph4lCYYwgF/ElIjdP739tdg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dev": true, - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, - "node_modules/find-cache-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-cache-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-cache-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-cache-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-cache-dir/node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -14584,6 +7738,7 @@ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", @@ -14594,30 +7749,23 @@ } }, "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", - "dev": true - }, - "node_modules/flow-parser": { - "version": "0.239.1", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.239.1.tgz", - "integrity": "sha512-topOrETNxJ6T2gAnQiWqAlzGPj8uI2wtmNOlDIMNB+qyvGJZ6R++STbUOTAYmvPhOMz2gXnXPH0hOvURYmrBow==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", "dev": true, - "engines": { - "node": ">=0.4.0" - } + "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -14632,15 +7780,17 @@ "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" } }, "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.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dev": true, + "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -14652,106 +7802,11 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/foreground-child/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, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/fork-ts-checker-webpack-plugin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz", - "integrity": "sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "chalk": "^4.1.2", - "chokidar": "^3.5.3", - "cosmiconfig": "^7.0.1", - "deepmerge": "^4.2.2", - "fs-extra": "^10.0.0", - "memfs": "^3.4.1", - "minimatch": "^3.0.4", - "node-abort-controller": "^3.0.1", - "schema-utils": "^3.1.1", - "semver": "^7.3.5", - "tapable": "^2.2.1" - }, - "engines": { - "node": ">=12.13.0", - "yarn": ">=1.0.0" - }, - "peerDependencies": { - "typescript": ">3.6.0", - "webpack": "^5.11.0" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -14761,20 +7816,12 @@ "node": ">= 6" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, "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": "*" }, @@ -14784,108 +7831,38 @@ } }, "node_modules/framer-motion": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-6.5.1.tgz", - "integrity": "sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==", + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.14.1.tgz", + "integrity": "sha512-6B7jC54zgnefmUSa2l4gkc/2CrqclHL9AUbDxxRfbFyWKLd+4guUYtEabzoYMU8G5ICZ6CdJdydOLy74Ekd7ag==", + "license": "MIT", "dependencies": { - "@motionone/dom": "10.12.0", - "framesync": "6.0.1", - "hey-listen": "^1.0.8", - "popmotion": "11.0.3", - "style-value-types": "5.0.0", - "tslib": "^2.1.0" - }, - "optionalDependencies": { - "@emotion/is-prop-valid": "^0.8.2" + "motion-dom": "^11.14.1", + "motion-utils": "^11.14.1", + "tslib": "^2.4.0" }, "peerDependencies": { - "react": ">=16.8 || ^17.0.0 || ^18.0.0", - "react-dom": ">=16.8 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/framesync": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/framesync/-/framesync-6.0.1.tgz", - "integrity": "sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.2.tgz", - "integrity": "sha512-YAiVokMCrSIFZiroB1oz51hPiPRVcUtSa4x2U5RYXyhS9VAPdiFigKbPTnOSq7XY8wd3FIVPYmXpo5lMzFmxgg==" - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true - }, - "node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" }, - "engines": { - "node": ">=14.14" + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } } }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs-minipass/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/fs-monkey": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz", - "integrity": "sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==", - "dev": true - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", @@ -14893,6 +7870,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -14905,6 +7883,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -14914,6 +7893,7 @@ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -14932,6 +7912,7 @@ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -14940,20 +7921,41 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "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==", "dev": true, + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "dunder-proto": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -14963,63 +7965,21 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/get-npm-tarball-url": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/get-npm-tarball-url/-/get-npm-tarball-url-2.1.0.tgz", - "integrity": "sha512-ro+DiMu5DXgRBabqXupW38h7WPZ9+Ad8UjwhvsmmN8w1sU7ab0nzAXvVZ4kqYg57OrqomRtJvepX5/xvFKNtjA==", - "dev": true, - "engines": { - "node": ">=12.17" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-port": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", - "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" }, "engines": { "node": ">= 0.4" @@ -15029,10 +7989,11 @@ } }, "node_modules/get-tsconfig": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", - "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", "dev": true, + "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" }, @@ -15040,72 +8001,23 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/getos": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", - "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==", - "dev": true, - "dependencies": { - "async": "^3.2.0" - } - }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "dev": true, - "dependencies": { - "assert-plus": "^1.0.0" - } - }, - "node_modules/giget": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.3.tgz", - "integrity": "sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==", - "dev": true, - "dependencies": { - "citty": "^0.1.6", - "consola": "^3.2.3", - "defu": "^6.1.4", - "node-fetch-native": "^1.6.3", - "nypm": "^0.3.8", - "ohash": "^1.1.3", - "pathe": "^1.1.2", - "tar": "^6.2.0" - }, - "bin": { - "giget": "dist/cli.mjs" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true - }, - "node_modules/github-slugger": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", - "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==", - "dev": true - }, "node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -15116,6 +8028,7 @@ "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" }, @@ -15123,66 +8036,28 @@ "node": ">=10.13.0" } }, - "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 - }, - "node_modules/glob/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==", + "node_modules/globals": { + "version": "15.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.13.0.tgz", + "integrity": "sha512-49TewVEz0UxZjr1WYYsWpPrhyC/B/pA8Bq0fUmet2n+eR7yn0IvNzNaoBwnK6mdkzcN+se7Ez9zUgULTz2QH4g==", "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, + "license": "MIT", "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/global-dirs": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", - "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", - "dev": true, - "dependencies": { - "ini": "2.0.0" - }, - "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", - "engines": { - "node": ">=4" - } - }, "node_modules/globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, + "license": "MIT", "dependencies": { - "define-properties": "^1.1.3" + "define-properties": "^1.2.1", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -15191,33 +8066,31 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "license": "MIT" + }, + "node_modules/goober": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", + "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -15227,141 +8100,22 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "node_modules/gray-matter": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", - "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", - "dependencies": { - "js-yaml": "^3.13.1", - "kind-of": "^6.0.2", - "section-matter": "^1.0.0", - "strip-bom-string": "^1.0.0" - }, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/gunzip-maybe": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz", - "integrity": "sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==", "dev": true, - "dependencies": { - "browserify-zlib": "^0.1.4", - "is-deflate": "^1.0.0", - "is-gzip": "^1.0.0", - "peek-stream": "^1.1.0", - "pumpify": "^1.3.3", - "through2": "^2.0.3" - }, - "bin": { - "gunzip-maybe": "bin.js" - } - }, - "node_modules/gunzip-maybe/node_modules/browserify-zlib": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", - "integrity": "sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==", - "dev": true, - "dependencies": { - "pako": "~0.2.0" - } - }, - "node_modules/gunzip-maybe/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, - "node_modules/gunzip-maybe/node_modules/pako": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", - "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", - "dev": true - }, - "node_modules/gunzip-maybe/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==", - "dev": true, - "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/gunzip-maybe/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==", - "dev": true - }, - "node_modules/gunzip-maybe/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==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/gunzip-maybe/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dev": true, - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/handlebars/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -15371,27 +8125,33 @@ "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/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "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": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -15400,10 +8160,11 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -15412,12 +8173,13 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, + "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -15430,6 +8192,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "license": "MIT", "dependencies": { "inherits": "^2.0.4", "readable-stream": "^3.6.0", @@ -15439,33 +8202,22 @@ "node": ">=4" } }, - "node_modules/hash-base/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==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/hash.js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -15473,34 +8225,148 @@ "node": ">= 0.4" } }, - "node_modules/hast-util-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", - "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "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, - "bin": { - "he": "bin/he" + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/hey-listen": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", - "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==" + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.5.tgz", + "integrity": "sha512-gHD+HoFxOMmmXLuq9f2dZDMQHVcplCVpMfBNRpJsF03yyLZvJGzsFORe8orVuYDX9k2w0VH0uF8oryFd1whqKQ==", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", + "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, "node_modules/hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", "dev": true, + "license": "MIT", "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", @@ -15511,138 +8377,43 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", "dependencies": { "react-is": "^16.7.0" } }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "node_modules/html-entities": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", - "integrity": "sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ] - }, - "node_modules/html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", - "dev": true, - "dependencies": { - "camel-case": "^4.1.2", - "clean-css": "^5.2.2", - "commander": "^8.3.0", - "he": "^1.2.0", - "param-case": "^3.0.4", - "relateurl": "^0.2.7", - "terser": "^5.10.0" - }, - "bin": { - "html-minifier-terser": "cli.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/html-minifier-terser/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, "node_modules/html-parse-stringify": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", "dependencies": { "void-elements": "3.1.0" } }, - "node_modules/html-tags": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", - "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/html-tokenize": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-tokenize/-/html-tokenize-2.0.1.tgz", - "integrity": "sha512-QY6S+hZ0f5m1WT8WffYN+Hg+xm/w5I8XeUcAq/ZYP5wVC8xbKi4Whhru3FtrAebD5EhBW8rmFzkDI6eCAuFe2w==", - "dependencies": { - "buffer-from": "~0.1.1", - "inherits": "~2.0.1", - "minimist": "~1.2.5", - "readable-stream": "~1.0.27-1", - "through2": "~0.4.1" - }, - "bin": { - "html-tokenize": "bin/cmd.js" - } - }, - "node_modules/html-webpack-plugin": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz", - "integrity": "sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw==", - "dev": true, - "dependencies": { - "@types/html-minifier-terser": "^6.0.0", - "html-minifier-terser": "^6.0.2", - "lodash": "^4.17.21", - "pretty-error": "^4.0.0", - "tapable": "^2.0.0" - }, - "engines": { - "node": ">=10.13.0" - }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", "funding": { "type": "opencollective", - "url": "https://opencollective.com/html-webpack-plugin" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.20.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/html2canvas": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", "optional": true, "dependencies": { "css-line-break": "^2.1.0", @@ -15652,99 +8423,17 @@ "node": ">=8.0.0" } }, - "node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/http-signature": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", - "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", - "dev": true, - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^2.0.2", - "sshpk": "^1.14.1" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", - "dev": true - }, - "node_modules/https-proxy-agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", - "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", "dev": true, - "dependencies": { - "agent-base": "5", - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/human-signals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", - "dev": true, - "engines": { - "node": ">=8.12.0" - } + "license": "MIT" }, "node_modules/i18next": { - "version": "22.5.1", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.5.1.tgz", - "integrity": "sha512-8TGPgM3pAD+VRsMtUMNknRz3kzqwp/gPALrWMsDnmC1mKqJwpWyooQRLMcbTwq8z8YwSmuj+ZYvc+xCuEpkssA==", + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.1.0.tgz", + "integrity": "sha512-suKlX82AlptkMUO5YRfaAeH4FQyyKvR66jNaubTMiyPPMx7INU6PXAiy3PGULc0q6K+t9nxmDf/TRj9KjAivmw==", "funding": [ { "type": "individual", @@ -15759,50 +8448,37 @@ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" } ], + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.20.6" + "@babel/runtime": "^7.23.2" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/i18next-browser-languagedetector": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.0.tgz", - "integrity": "sha512-U00DbDtFIYD3wkWsr2aVGfXGAj2TgnELzOX9qv8bT0aJtvPV9CRO77h+vgmHFBMe7LAxdwvT/7VkCWGya6L3tA==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.2.tgz", + "integrity": "sha512-shBvPmnIyZeD2VU5jVGIOWP7u9qNG3Lj7mpaiPFpbJ3LVfHZJvVzKR4v1Cb91wAOFpNw442N+LGPzHOHsten2g==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.2" } }, "node_modules/i18next-http-backend": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.4.2.tgz", - "integrity": "sha512-wKrgGcaFQ4EPjfzBTjzMU0rbFTYpa0S5gv9N/d8WBmWS64+IgJb7cHddMvV+tUkse7vUfco3eVs2lB+nJhPo3w==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.1.tgz", + "integrity": "sha512-XT2lYSkbAtDE55c6m7CtKxxrsfuRQO3rUfHzj8ZyRtY9CkIX3aRGwXGTkUhpGWce+J8n7sfu3J0f2wTzo7Lw0A==", + "license": "MIT", "dependencies": { "cross-fetch": "4.0.0" } }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -15821,45 +8497,24 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "BSD-3-Clause" }, "node_modules/ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } }, - "node_modules/image-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz", - "integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==", - "dev": true, - "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, - "node_modules/immer": { - "version": "9.0.21", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", - "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -15871,64 +8526,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "engines": { - "node": ">=4" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.19" } }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/infisical-node": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/infisical-node/-/infisical-node-1.5.1.tgz", - "integrity": "sha512-4m78/SLvJ29a41vVhnkm6f/R0MqD/IsBE3TlQUJbJCoId+7nD6QjElQVP31+bfOb6Ve7wR2KvsUDM0VcXFWnqQ==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dependencies": { - "axios": "^1.3.3", - "dotenv": "^16.0.3", - "tweetnacl": "^1.0.3", - "tweetnacl-util": "^0.15.1" - } - }, - "node_modules/infisical-node/node_modules/axios": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz", - "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/infisical-node/node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, + "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -15937,29 +8551,22 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ini": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", - "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", - "dev": true, - "engines": { - "node": ">=10" - } + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" }, "node_modules/inline-style-parser": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", - "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==" }, "node_modules/internal-slot": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", - "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.2", + "es-errors": "^1.3.0", "hasown": "^2.0.0", "side-channel": "^1.0.4" }, @@ -15971,36 +8578,51 @@ "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", "dependencies": { "loose-envify": "^1.0.0" } }, "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">= 10" } }, - "node_modules/is-absolute-url": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", - "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", - "dev": true, - "engines": { - "node": ">=8" + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -16010,14 +8632,17 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -16026,13 +8651,15 @@ "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" }, "node_modules/is-async-function": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", "dev": true, + "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -16044,12 +8671,16 @@ } }, "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, + "license": "MIT", "dependencies": { - "has-bigints": "^1.0.1" + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -16060,6 +8691,7 @@ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -16068,13 +8700,14 @@ } }, "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.0.tgz", + "integrity": "sha512-kR5g0+dXf/+kXnqI+lu0URKYPKgICtHGGNCDSB10AaUFj3o/HkB3u7WfpRBJGFopxxY0oH3ux7ZsDjLtK7xqvw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bind": "^1.0.7", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -16083,26 +8716,14 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "engines": { - "node": ">=4" + "node_modules/is-bun-module": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.3.0.tgz", + "integrity": "sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" } }, "node_modules/is-callable": { @@ -16110,6 +8731,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" }, @@ -16117,24 +8739,34 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "dev": true, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "license": "MIT", "dependencies": { - "ci-info": "^3.2.0" + "hasown": "^2.0.2" }, - "bin": { - "is-ci": "bin.js" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", "dependencies": { - "hasown": "^2.0.0" + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -16145,6 +8777,7 @@ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", "dev": true, + "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -16155,33 +8788,13 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-deflate": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-deflate/-/is-deflate-1.0.0.tgz", - "integrity": "sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==", - "dev": true - }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "engines": { - "node": ">=0.10.0" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/is-extglob": { @@ -16189,17 +8802,22 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/is-finalizationregistry": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", - "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.0.tgz", + "integrity": "sha512-qfMdqbAQEwBw78ZyReKnlA8ezmPdb9BemzIIip/JkjaZUhitfXDkkr+3QTboW0JrSXT1QWyYShpvnNHGZ4c4yA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -16209,7 +8827,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -16219,6 +8837,7 @@ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", "dev": true, + "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -16234,6 +8853,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -16241,45 +8861,24 @@ "node": ">=0.10.0" } }, - "node_modules/is-gzip": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-gzip/-/is-gzip-1.0.0.tgz", - "integrity": "sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-installed-globally": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", - "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", - "dev": true, - "dependencies": { - "global-dirs": "^3.0.0", - "is-path-inside": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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, - "engines": { - "node": ">=8" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/is-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -16289,6 +8888,7 @@ "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.0", "define-properties": "^1.1.3" @@ -16301,10 +8901,11 @@ } }, "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -16317,17 +8918,20 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } }, "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.0.tgz", + "integrity": "sha512-KVSZV0Dunv9DTPkhXwcZ3Q+tUc9TsaE1ZwX5J2WMvsSGS6Md8TFPun5uwh0yRdrNerI6vf/tbJxqSx4c1ZI1Lw==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bind": "^1.0.7", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -16336,20 +8940,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -16365,22 +8961,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -16390,45 +8981,43 @@ } }, "node_modules/is-set": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.0.tgz", + "integrity": "sha512-PlfzajuF9vSo5wErv3MJAKD/nqf9ngAs1NFQYm16nUYFO2IzxJ2hcm+IOCg+EEopdykNNUhVq5cz35cAUxU8+g==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bind": "^1.0.7", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -16438,12 +9027,15 @@ } }, "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.0.tgz", + "integrity": "sha512-qS8KkNNXUZ/I+nX6QT8ZS1/Yx0A444yhzdTKxCzKkNjQ9sHErBxJnJAgh+f5YhusYECEcjo4XcyH87hn6+ks0A==", "dev": true, + "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "call-bind": "^1.0.7", + "has-symbols": "^1.0.3", + "safe-regex-test": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -16453,12 +9045,13 @@ } }, "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.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", "dev": true, + "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -16467,29 +9060,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true - }, - "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, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-weakmap": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -16499,6 +9078,7 @@ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2" }, @@ -16507,113 +9087,85 @@ } }, "node_modules/is-weakset": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", - "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-what": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", - "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", - "dev": true, - "peer": true - }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "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 + "dev": true, + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", "dev": true, + "license": "ISC" + }, + "node_modules/isomorphic-timers-promises": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-timers-promises/-/isomorphic-timers-promises-1.0.1.tgz", + "integrity": "sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=10" } }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", - "dev": true - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "license": "MIT", + "peer": true, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" } }, "node_modules/iterator.prototype": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", - "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.4.tgz", + "integrity": "sha512-x4WH0BWmrMmg4oHHl+duwubhrvczGlyuGAZu3nvrf0UXOfPu8IhZObFEr7DE/iv01YgVZrsOiRcqw2srkKEDIA==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "define-properties": "^1.2.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "reflect.getprototypeof": "^1.0.4", - "set-function-name": "^2.0.1" + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "reflect.getprototypeof": "^1.0.8", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": ">=14" - }, "funding": { "url": "https://github.com/sponsors/isaacs" }, @@ -16621,148 +9173,12 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/jake": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz", - "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==", - "dev": true, - "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-haste-map": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-mock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", - "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-mock/node_modules/@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-mock/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "dev": true, - "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "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, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/jiti": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", - "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", "dev": true, + "license": "MIT", "bin": { "jiti": "bin/jiti.js" } @@ -16770,15 +9186,17 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" @@ -16787,148 +9205,61 @@ "node_modules/jsbn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" - }, - "node_modules/jscodeshift": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.15.2.tgz", - "integrity": "sha512-FquR7Okgmc4Sd0aEDwqho3rEiKR3BdvuG9jfdHjLJ6JQoWSMpavug3AoIfnfWhxFlf+5pzQh8qjqz0DWFrNQzA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.23.0", - "@babel/parser": "^7.23.0", - "@babel/plugin-transform-class-properties": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.23.0", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", - "@babel/plugin-transform-optional-chaining": "^7.23.0", - "@babel/plugin-transform-private-methods": "^7.22.5", - "@babel/preset-flow": "^7.22.15", - "@babel/preset-typescript": "^7.23.0", - "@babel/register": "^7.22.15", - "babel-core": "^7.0.0-bridge.0", - "chalk": "^4.1.2", - "flow-parser": "0.*", - "graceful-fs": "^4.2.4", - "micromatch": "^4.0.4", - "neo-async": "^2.5.0", - "node-dir": "^0.1.17", - "recast": "^0.23.3", - "temp": "^0.8.4", - "write-file-atomic": "^2.3.0" - }, - "bin": { - "jscodeshift": "bin/jscodeshift.js" - }, - "peerDependencies": { - "@babel/preset-env": "^7.1.6" - }, - "peerDependenciesMeta": { - "@babel/preset-env": { - "optional": true - } - } - }, - "node_modules/jscodeshift/node_modules/write-file-atomic": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", - "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "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==" - }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "dev": true + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, "bin": { "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" } }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/jsonp/-/jsonp-0.2.1.tgz", - "integrity": "sha512-pfog5gdDxPdV4eP7Kg87M8/bHgshlZ5pybl+yKxAnCZ5O7lCIn7Ixydj03wOlnDQesky2BPyA91SQ+5Y/mNwzw==", - "dependencies": { - "debug": "^2.1.3" - } - }, - "node_modules/jsonp/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/jsonp/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/jspdf": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz", @@ -16947,31 +9278,17 @@ "html2canvas": "^1.0.0-rc.5" } }, - "node_modules/jspdf/node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "license": "MIT" - }, - "node_modules/jsprim": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", - "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - } + "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", "integrity": "sha512-+CjGAhZaj3k2MMXEy+xWYv7xJGnise/SlL1IIvnRuJ1ZiLtNPJJln/dMDCgORQCq1ouXDnW1FBxW5bkBFhK/8g==", + "license": "MIT", "dependencies": { "create-hash": "^1.0.0", "jsbn": "^1.0.0", @@ -16983,6 +9300,8 @@ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", @@ -16994,19 +9313,12 @@ } }, "node_modules/jwt-decode": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", - "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" - }, - "node_modules/keygrip": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", - "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", - "dependencies": { - "tsscmp": "1.0.6" - }, + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=18" } }, "node_modules/keyv": { @@ -17014,47 +9326,26 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "engines": { - "node": ">=0.10.0" - } - }, - "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, - "engines": { - "node": ">=6" - } - }, - "node_modules/klona": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", - "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/language-subtag-registry": { - "version": "0.3.22", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", - "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==", - "dev": true + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0", + "peer": true }, "node_modules/language-tags": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { "language-subtag-registry": "^0.3.20" }, @@ -17062,149 +9353,12 @@ "node": ">=0.10" } }, - "node_modules/lazy-ass": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", - "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==", - "dev": true, - "engines": { - "node": "> 0.8" - } - }, - "node_modules/lazy-universal-dotenv": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/lazy-universal-dotenv/-/lazy-universal-dotenv-4.0.0.tgz", - "integrity": "sha512-aXpZJRnTkpK6gQ/z4nk+ZBLd/Qdp118cvPruLSIQzQNRhKwEcdXCOzXuF55VDqIiuAaY3UGZ10DJtvZzDcvsxg==", - "dev": true, - "dependencies": { - "app-root-dir": "^1.0.2", - "dotenv": "^16.0.0", - "dotenv-expand": "^10.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/less": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", - "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", - "dev": true, - "peer": true, - "dependencies": { - "copy-anything": "^2.0.1", - "parse-node-version": "^1.0.1", - "tslib": "^2.3.0" - }, - "bin": { - "lessc": "bin/lessc" - }, - "engines": { - "node": ">=6" - }, - "optionalDependencies": { - "errno": "^0.1.1", - "graceful-fs": "^4.1.2", - "image-size": "~0.5.0", - "make-dir": "^2.1.0", - "mime": "^1.4.1", - "needle": "^3.1.0", - "source-map": "~0.6.0" - } - }, - "node_modules/less-loader": { - "version": "11.1.4", - "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-11.1.4.tgz", - "integrity": "sha512-6/GrYaB6QcW6Vj+/9ZPgKKs6G10YZai/l/eJ4SLwbzqNTBsAqt5hSLVF47TgsiBxV1P6eAU0GYRH3YRuQU9V3A==", - "dev": true, - "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "less": "^3.5.0 || ^4.0.0", - "webpack": "^5.0.0" - } - }, - "node_modules/less/node_modules/image-size": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", - "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", - "dev": true, - "optional": true, - "peer": true, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/less/node_modules/make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/less/node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/less/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "optional": true, - "peer": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/less/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -17213,95 +9367,59 @@ "node": ">= 0.8.0" } }, - "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, + "node_modules/lexical": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.29.0.tgz", + "integrity": "sha512-eoBHUEn0LmExKeK6x2cFKU0FPaMk2Bc5HgiCzTiv5ymKtwWw7LeKcxaNPmLxRRdQpcWV1IMKjayAbw7Lt/Gu7w==", + "license": "MIT" + }, + "node_modules/lib0": { + "version": "0.2.102", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.102.tgz", + "integrity": "sha512-g70kydI0I1sZU0ChO8mBbhw0oUW/8U0GHzygpvEIx8k+jgOpqnTSb/E+70toYVqHxBhrERD21TwD5QcZJQ40ZQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, "engines": { - "node": ">=10" + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" - }, - "node_modules/linkify-it": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", - "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", - "dependencies": { - "uc.micro": "^1.0.1" - } - }, - "node_modules/listr2": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", - "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", - "dev": true, - "dependencies": { - "cli-truncate": "^2.1.0", - "colorette": "^2.0.16", - "log-update": "^4.0.0", - "p-map": "^4.0.0", - "rfdc": "^1.3.0", - "rxjs": "^7.5.1", - "through": "^2.3.8", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "enquirer": ">= 2.3.0 < 3" - }, - "peerDependenciesMeta": { - "enquirer": { - "optional": true - } - } - }, - "node_modules/listr2/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "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, - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/loader-utils": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", - "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", - "dev": true, - "engines": { - "node": ">= 12.13.0" - } + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -17312,121 +9430,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" - }, "node_modules/lodash.castarray": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", - "dev": true - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true - }, - "node_modules/lodash.deburr": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/lodash.deburr/-/lodash.deburr-4.1.0.tgz", - "integrity": "sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==", + "dev": true, "license": "MIT" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "dev": true - }, - "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, - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", - "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", - "dev": true, - "dependencies": { - "ansi-escapes": "^4.3.0", - "cli-cursor": "^3.1.0", - "slice-ansi": "^4.0.0", - "wrap-ansi": "^6.2.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -17434,213 +9472,131 @@ "loose-envify": "cli.js" } }, - "node_modules/lottie-react": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/lottie-react/-/lottie-react-2.4.0.tgz", - "integrity": "sha512-pDJGj+AQlnlyHvOHFK7vLdsDcvbuqvwPZdMlJ360wrzGFurXeKPr8SiRCjLf3LrNYKANQtSsh5dz9UYQHuqx4w==", - "dependencies": { - "lottie-web": "^5.10.2" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/lottie-web": { - "version": "5.12.2", - "resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.12.2.tgz", - "integrity": "sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg==" - }, - "node_modules/lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dev": true, - "dependencies": { - "tslib": "^2.0.3" - } - }, "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "bin": { - "lz-string": "bin/bin.js" - } + "license": "ISC" }, "node_modules/magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "version": "0.30.15", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.15.tgz", + "integrity": "sha512-zXeaYRgZ6ldS1RJJUrMrYgNJ4fdwnyI6tVqoiIhyCyv5IVTK9BU8Ic2l253GGETQHxI4HNUwhJ3fjDhKqEoaAw==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "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==", "dev": true, - "dependencies": { - "semver": "^6.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/map-or-similar": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/map-or-similar/-/map-or-similar-1.5.0.tgz", - "integrity": "sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==", - "dev": true - }, - "node_modules/markdown-it": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.2.tgz", - "integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==", - "dependencies": { - "argparse": "^2.0.1", - "entities": "~3.0.1", - "linkify-it": "^4.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - }, - "bin": { - "markdown-it": "bin/markdown-it.js" - } - }, - "node_modules/markdown-it/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "node_modules/markdown-it/node_modules/entities": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", - "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/markdown-to-jsx": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.4.0.tgz", - "integrity": "sha512-zilc+MIkVVXPyTb4iIUTIz9yyqfcWjszGXnwF9K/aiBWcHXFcmdEMTkG01/oQhwSCH7SY1BnG6+ev5BzWmbPrg==", - "dev": true, - "engines": { - "node": ">= 10" - }, - "peerDependencies": { - "react": ">= 0.14.0" + "node": ">= 0.4" } }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "license": "MIT", "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1", "safe-buffer": "^5.1.2" } }, - "node_modules/mdast-util-definitions": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz", - "integrity": "sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ==", - "dev": true, - "dependencies": { - "unist-util-visit": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-definitions/node_modules/unist-util-is": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", - "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-definitions/node_modules/unist-util-visit": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", - "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0", - "unist-util-visit-parents": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-definitions/node_modules/unist-util-visit-parents": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", - "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/mdast-util-from-markdown": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", - "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", - "mdast-util-to-string": "^3.1.0", - "micromark": "^3.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-decode-string": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "unist-util-stringify-position": "^3.0.0", - "uvu": "^0.5.0" + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" }, "funding": { "type": "opencollective", @@ -17648,32 +9604,39 @@ } }, "node_modules/mdast-util-to-hast": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz", - "integrity": "sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==", + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", "dependencies": { - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "mdast-util-definitions": "^5.0.0", - "micromark-util-sanitize-uri": "^1.1.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", - "unist-util-generated": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0" + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/mdast-util-to-hast/node_modules/mdast-util-definitions": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", - "integrity": "sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==", + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "unist-util-visit": "^4.0.0" + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" }, "funding": { "type": "opencollective", @@ -17681,91 +9644,37 @@ } }, "node_modules/mdast-util-to-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", - "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", "dependencies": { - "@types/mdast": "^3.0.0" + "@types/mdast": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memfs": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", - "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", - "dev": true, - "dependencies": { - "fs-monkey": "^1.0.4" - }, - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/memoize-one": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" - }, - "node_modules/memoizerific": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz", - "integrity": "sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==", - "dev": true, - "dependencies": { - "map-or-similar": "^1.5.0" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", - "dev": true - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/micromark": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", - "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", "funding": [ { "type": "GitHub Sponsors", @@ -17780,26 +9689,26 @@ "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", - "micromark-core-commonmark": "^1.0.1", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-core-commonmark": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", - "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", "funding": [ { "type": "GitHub Sponsors", @@ -17812,27 +9721,27 @@ ], "dependencies": { "decode-named-character-reference": "^1.0.0", - "micromark-factory-destination": "^1.0.0", - "micromark-factory-label": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-factory-title": "^1.0.0", - "micromark-factory-whitespace": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-classify-character": "^1.0.0", - "micromark-util-html-tag-name": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-factory-destination": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", - "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", "funding": [ { "type": "GitHub Sponsors", @@ -17844,15 +9753,15 @@ } ], "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-factory-label": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", - "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", "funding": [ { "type": "GitHub Sponsors", @@ -17864,16 +9773,16 @@ } ], "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-factory-space": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", - "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", "funding": [ { "type": "GitHub Sponsors", @@ -17885,14 +9794,14 @@ } ], "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-factory-title": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", - "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", "funding": [ { "type": "GitHub Sponsors", @@ -17904,16 +9813,16 @@ } ], "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-factory-whitespace": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", - "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", "funding": [ { "type": "GitHub Sponsors", @@ -17925,16 +9834,16 @@ } ], "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -17946,14 +9855,14 @@ } ], "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-chunked": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", - "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", "funding": [ { "type": "GitHub Sponsors", @@ -17965,13 +9874,13 @@ } ], "dependencies": { - "micromark-util-symbol": "^1.0.0" + "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-classify-character": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", - "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", "funding": [ { "type": "GitHub Sponsors", @@ -17983,15 +9892,15 @@ } ], "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-combine-extensions": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", - "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", "funding": [ { "type": "GitHub Sponsors", @@ -18003,14 +9912,14 @@ } ], "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", - "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", "funding": [ { "type": "GitHub Sponsors", @@ -18022,13 +9931,13 @@ } ], "dependencies": { - "micromark-util-symbol": "^1.0.0" + "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-decode-string": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", - "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", "funding": [ { "type": "GitHub Sponsors", @@ -18041,15 +9950,15 @@ ], "dependencies": { "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-symbol": "^1.0.0" + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", - "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", "funding": [ { "type": "GitHub Sponsors", @@ -18062,9 +9971,9 @@ ] }, "node_modules/micromark-util-html-tag-name": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", - "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", "funding": [ { "type": "GitHub Sponsors", @@ -18077,9 +9986,9 @@ ] }, "node_modules/micromark-util-normalize-identifier": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", - "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", "funding": [ { "type": "GitHub Sponsors", @@ -18091,13 +10000,13 @@ } ], "dependencies": { - "micromark-util-symbol": "^1.0.0" + "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-resolve-all": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", - "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", "funding": [ { "type": "GitHub Sponsors", @@ -18109,13 +10018,13 @@ } ], "dependencies": { - "micromark-util-types": "^1.0.0" + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-sanitize-uri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", - "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", "funding": [ { "type": "GitHub Sponsors", @@ -18127,15 +10036,15 @@ } ], "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-symbol": "^1.0.0" + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-subtokenize": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", - "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", "funding": [ { "type": "GitHub Sponsors", @@ -18147,16 +10056,16 @@ } ], "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -18169,9 +10078,9 @@ ] }, "node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", "funding": [ { "type": "GitHub Sponsors", @@ -18188,6 +10097,7 @@ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -18196,11 +10106,25 @@ "node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/miller-rabin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", "dev": true, + "license": "MIT", "dependencies": { "bn.js": "^4.0.0", "brorand": "^1.0.1" @@ -18210,27 +10134,17 @@ } }, "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } + "license": "MIT" }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -18239,6 +10153,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -18246,53 +10161,26 @@ "node": ">= 0.6" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/minimalistic-crypto-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -18304,129 +10192,64 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "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/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } + "node_modules/motion-dom": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.1.tgz", + "integrity": "sha512-Y68tHWR0d2HxHDskNxpeY3pzUdz7L/m5A8TV7VSE6Sq4XUNJdZV8zXco1aeAQ44o48u0i8UKjt8TGIqkZSQ8ew==", + "license": "MIT" }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true - }, - "node_modules/mlly": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz", - "integrity": "sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==", - "dev": true, - "dependencies": { - "acorn": "^8.11.3", - "pathe": "^1.1.2", - "pkg-types": "^1.1.1", - "ufo": "^1.5.3" - } - }, - "node_modules/mlly/node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "engines": { - "node": ">=4" - } + "node_modules/motion-utils": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.14.1.tgz", + "integrity": "sha512-R6SsehArpkEBUHydkcwQ/8ij8k2PyKWAJ7Y8PN3ztnFwq5RBU3zIamYH6esTp09OgsbwB57mBEZ9DORaN1WTxQ==", + "license": "MIT" }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, - "node_modules/multipipe": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-1.0.2.tgz", - "integrity": "sha512-6uiC9OvY71vzSGX8lZvSqscE7ft9nPupJ8fMjrCNRAUy2LREUW42UL+V/NTrogr6rFgRydUrCX4ZitfpSNkSCQ==", + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", "dependencies": { - "duplexer2": "^0.1.2", - "object-assign": "^4.1.0" + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" } }, - "node_modules/nanoclone": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz", - "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==" - }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -18434,256 +10257,18 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "dev": true - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, - "node_modules/needle": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", - "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "iconv-lite": "^0.6.3", - "sax": "^1.2.4" - }, - "bin": { - "needle": "bin/needle" - }, - "engines": { - "node": ">= 4.4.x" - } - }, - "node_modules/needle/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, - "node_modules/next": { - "version": "12.3.4", - "resolved": "https://registry.npmjs.org/next/-/next-12.3.4.tgz", - "integrity": "sha512-VcyMJUtLZBGzLKo3oMxrEF0stxh8HwuW976pAzlHhI3t8qJ4SROjCrSh1T24bhrbjw55wfZXAbXPGwPt5FLRfQ==", - "dependencies": { - "@next/env": "12.3.4", - "@swc/helpers": "0.4.11", - "caniuse-lite": "^1.0.30001406", - "postcss": "8.4.14", - "styled-jsx": "5.0.7", - "use-sync-external-store": "1.2.0" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": ">=12.22.0" - }, - "optionalDependencies": { - "@next/swc-android-arm-eabi": "12.3.4", - "@next/swc-android-arm64": "12.3.4", - "@next/swc-darwin-arm64": "12.3.4", - "@next/swc-darwin-x64": "12.3.4", - "@next/swc-freebsd-x64": "12.3.4", - "@next/swc-linux-arm-gnueabihf": "12.3.4", - "@next/swc-linux-arm64-gnu": "12.3.4", - "@next/swc-linux-arm64-musl": "12.3.4", - "@next/swc-linux-x64-gnu": "12.3.4", - "@next/swc-linux-x64-musl": "12.3.4", - "@next/swc-win32-arm64-msvc": "12.3.4", - "@next/swc-win32-ia32-msvc": "12.3.4", - "@next/swc-win32-x64-msvc": "12.3.4" - }, - "peerDependencies": { - "fibers": ">= 3.1.0", - "node-sass": "^6.0.0 || ^7.0.0", - "react": "^17.0.2 || ^18.0.0-0", - "react-dom": "^17.0.2 || ^18.0.0-0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "fibers": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/next/node_modules/@swc/helpers": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.11.tgz", - "integrity": "sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw==", - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/next/node_modules/postcss": { - "version": "8.4.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", - "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - } - ], - "dependencies": { - "nanoid": "^3.3.4", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/next/node_modules/styled-jsx": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.7.tgz", - "integrity": "sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA==", - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dev": true, - "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "node_modules/node-abi": { - "version": "3.54.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.54.0.tgz", - "integrity": "sha512-p7eGEiQil0YUV3ItH4/tBb781L5impVmmx2E9FRKF7d18XXzp4PGT2tdYMFY6wQqgxD0IwNZOiSJ0/K0fSi/OA==", - "dev": true, - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-abi/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-abi/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/node-abort-controller": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", - "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", - "dev": true - }, - "node_modules/node-addon-api": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", - "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", - "dev": true - }, - "node_modules/node-dir": { - "version": "0.1.17", - "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", - "integrity": "sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==", - "dev": true, - "dependencies": { - "minimatch": "^3.0.2" - }, - "engines": { - "node": ">= 0.10.5" - } + "license": "MIT" }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -18699,128 +10284,65 @@ } } }, - "node_modules/node-fetch-native": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz", - "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==", - "dev": true - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true - }, - "node_modules/node-polyfill-webpack-plugin": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-2.0.1.tgz", - "integrity": "sha512-ZUMiCnZkP1LF0Th2caY6J/eKKoA0TefpoVa68m/LQU1I/mE8rGt4fNYGgNuCcK+aG8P8P43nbeJ2RqJMOL/Y1A==", + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true, + "license": "MIT" + }, + "node_modules/node-stdlib-browser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/node-stdlib-browser/-/node-stdlib-browser-1.3.0.tgz", + "integrity": "sha512-g/koYzOr9Fb1Jc+tHUHlFd5gODjGn48tHexUK8q6iqOVriEgSnd3/1T7myBYc+0KBVze/7F7n65ec9rW6OD7xw==", + "dev": true, + "license": "MIT", "dependencies": { "assert": "^2.0.0", + "browser-resolve": "^2.0.0", "browserify-zlib": "^0.2.0", - "buffer": "^6.0.3", - "console-browserify": "^1.2.0", + "buffer": "^5.7.1", + "console-browserify": "^1.1.0", "constants-browserify": "^1.0.0", - "crypto-browserify": "^3.12.0", - "domain-browser": "^4.22.0", - "events": "^3.3.0", - "filter-obj": "^2.0.2", + "create-require": "^1.1.1", + "crypto-browserify": "^3.11.0", + "domain-browser": "4.22.0", + "events": "^3.0.0", "https-browserify": "^1.0.0", + "isomorphic-timers-promises": "^1.0.1", "os-browserify": "^0.3.0", "path-browserify": "^1.0.1", + "pkg-dir": "^5.0.0", "process": "^0.11.10", - "punycode": "^2.1.1", + "punycode": "^1.4.1", "querystring-es3": "^0.2.1", - "readable-stream": "^4.0.0", + "readable-stream": "^3.6.0", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", - "string_decoder": "^1.3.0", - "timers-browserify": "^2.0.12", - "tty-browserify": "^0.0.1", - "type-fest": "^2.14.0", - "url": "^0.11.0", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.1", + "url": "^0.11.4", "util": "^0.12.4", - "vm-browserify": "^1.1.2" + "vm-browserify": "^1.0.1" }, "engines": { - "node": ">=12" - }, - "peerDependencies": { - "webpack": ">=5" + "node": ">=10" } }, - "node_modules/node-polyfill-webpack-plugin/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "node_modules/node-stdlib-browser/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", "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" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/node-polyfill-webpack-plugin/node_modules/readable-stream": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", - "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", - "dev": true, - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.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==" - }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "bin": { - "semver": "bin/semver" - } + "license": "MIT" }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -18830,197 +10352,22 @@ "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": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/nprogress": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", - "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==" - }, - "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, - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/nypm": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.9.tgz", - "integrity": "sha512-BI2SdqqTHg2d4wJh8P9A1W+bslg33vOE9IZDY6eR2QC+Pu1iNBVZUqczrd43rJb+fMzHU7ltAYKsEFY/kHMFcw==", - "dev": true, - "dependencies": { - "citty": "^0.1.6", - "consola": "^3.2.3", - "execa": "^8.0.1", - "pathe": "^1.1.2", - "pkg-types": "^1.1.1", - "ufo": "^1.5.3" - }, - "bin": { - "nypm": "dist/cli.mjs" - }, - "engines": { - "node": "^14.16.0 || >=16.10.0" - } - }, - "node_modules/nypm/node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/nypm/node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nypm/node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "engines": { - "node": ">=16.17.0" - } - }, - "node_modules/nypm/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nypm/node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nypm/node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nypm/node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nypm/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nypm/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, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/nypm/node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", + "license": "MIT" }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -19030,27 +10377,33 @@ "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/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -19064,6 +10417,7 @@ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -19073,6 +10427,7 @@ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.5", "define-properties": "^1.2.1", @@ -19087,28 +10442,31 @@ } }, "node_modules/object.entries": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", - "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" } }, "node_modules/object.fromentries": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", - "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -19118,39 +10476,30 @@ } }, "node_modules/object.groupby": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", - "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1" - } - }, - "node_modules/object.hasown": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz", - "integrity": "sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==", - "dev": true, - "dependencies": { - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 0.4" } }, "node_modules/object.values": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", - "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -19159,136 +10508,47 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/objectorarray": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/objectorarray/-/objectorarray-1.0.5.tgz", - "integrity": "sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg==", - "dev": true - }, - "node_modules/ohash": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.3.tgz", - "integrity": "sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==", - "dev": true - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", "dependencies": { "wrappy": "1" } }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", - "dev": true, - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, + "license": "MIT", "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, "engines": { "node": ">= 0.8.0" } }, - "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, - "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/os-browserify": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", - "dev": true - }, - "node_modules/ospath": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", - "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -19304,6 +10564,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -19314,50 +10575,34 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true - }, - "node_modules/param-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", - "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", "dev": true, - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } + "license": "(MIT AND Zlib)" }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -19366,22 +10611,65 @@ } }, "node_modules/parse-asn1": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", - "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz", + "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==", "dev": true, + "license": "ISC", "dependencies": { - "asn1.js": "^5.2.0", - "browserify-aes": "^1.0.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "hash-base": "~3.0", + "pbkdf2": "^3.1.2", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" } }, + "node_modules/parse-asn1/node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -19395,51 +10683,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parse-node-version": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", - "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", - "dev": true, - "peer": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/parse-srcset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", - "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/pascal-case": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", - "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", - "dev": true, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -19449,6 +10715,7 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -19458,6 +10725,7 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -19465,58 +10733,41 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" }, "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", - "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", - "dev": true - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true - }, "node_modules/pbkdf2": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", "dev": true, + "license": "MIT", "dependencies": { "create-hash": "^1.1.2", "create-hmac": "^1.1.4", @@ -19528,92 +10779,26 @@ "node": ">=0.12" } }, - "node_modules/peek-stream": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz", - "integrity": "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==", - "dev": true, - "dependencies": { - "buffer-from": "^1.0.0", - "duplexify": "^3.5.0", - "through2": "^2.0.3" - } - }, - "node_modules/peek-stream/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 - }, - "node_modules/peek-stream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, - "node_modules/peek-stream/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==", - "dev": true, - "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/peek-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==", - "dev": true - }, - "node_modules/peek-stream/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==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/peek-stream/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true - }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", - "devOptional": true + "license": "MIT", + "optional": true }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -19624,6 +10809,7 @@ "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" } @@ -19633,6 +10819,7 @@ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } @@ -19642,6 +10829,7 @@ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", "dev": true, + "license": "MIT", "dependencies": { "find-up": "^5.0.0" }, @@ -19649,56 +10837,30 @@ "node": ">=10" } }, - "node_modules/pkg-types": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.3.tgz", - "integrity": "sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA==", - "dev": true, - "dependencies": { - "confbox": "^0.1.7", - "mlly": "^1.7.1", - "pathe": "^1.1.2" - } - }, - "node_modules/pnp-webpack-plugin": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.7.0.tgz", - "integrity": "sha512-2Rb3vm+EXble/sMXNSu6eoBx8e79gKqhNq9F5ZWW6ERNCTE/Q0wQNne5541tE5vKjfM8hpNCYL+LGc1YTfI0dg==", - "dev": true, - "dependencies": { - "ts-pnp": "^1.1.6" - }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", "engines": { - "node": ">=6" + "node": ">=10.13.0" } }, - "node_modules/polished": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/polished/-/polished-4.2.2.tgz", - "integrity": "sha512-Sz2Lkdxz6F2Pgnpi9U5Ng/WdWAUZxmHrNPoVlm3aAemxoy2Qy7LGjQg4uf8qKelDAUW94F4np3iH2YPf2qefcQ==", + "node_modules/possible-typed-array-names": { + "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, - "dependencies": { - "@babel/runtime": "^7.17.8" - }, + "license": "MIT", "engines": { - "node": ">=10" - } - }, - "node_modules/popmotion": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.3.tgz", - "integrity": "sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==", - "dependencies": { - "framesync": "6.0.1", - "hey-listen": "^1.0.8", - "style-value-types": "5.0.0", - "tslib": "^2.1.0" + "node": ">= 0.4" } }, "node_modules/postcss": { - "version": "8.4.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", - "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "dev": true, "funding": [ { "type": "opencollective", @@ -19713,27 +10875,29 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" } }, "node_modules/postcss-import": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", - "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "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": ">=10.0.0" + "node": ">=14.0.0" }, "peerDependencies": { "postcss": "^8.0.0" @@ -19744,6 +10908,7 @@ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", "dev": true, + "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" }, @@ -19759,20 +10924,27 @@ } }, "node_modules/postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", "dependencies": { - "lilconfig": "^2.0.5", - "yaml": "^1.10.2" + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" }, "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "node": ">= 14" }, "peerDependencies": { "postcss": ">=8.0.9", @@ -19787,197 +10959,38 @@ } } }, - "node_modules/postcss-load-config/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/postcss-loader": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz", - "integrity": "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==", - "dev": true, - "dependencies": { - "cosmiconfig": "^8.3.5", - "jiti": "^1.20.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "postcss": "^7.0.0 || ^8.0.1", - "webpack": "^5.0.0" - } - }, - "node_modules/postcss-loader/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/postcss-loader/node_modules/cosmiconfig": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", - "dev": true, - "dependencies": { - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/postcss-loader/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/postcss-loader/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/postcss-loader/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/postcss-loader/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", - "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", - "dev": true, - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.0.tgz", - "integrity": "sha512-SaIbK8XW+MZbd0xHPf7kdfA/3eOt7vxJ72IRecn3EzuZVLr1r0orzf0MX/pN8m+NMDoo6X/SQd8oeKqGZd8PXg==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dev": true, - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/postcss-nested": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", - "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==", + "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.0.10" + "postcss-selector-parser": "^6.1.1" }, "engines": { "node": ">=12.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, "peerDependencies": { "postcss": "^8.2.14" } }, "node_modules/postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "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" @@ -19989,119 +11002,59 @@ "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==" + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" }, "node_modules/posthog-js": { - "version": "1.105.6", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.105.6.tgz", - "integrity": "sha512-5ITXsh29XIuNohHLy21nawGnfFZDpyt+yfnWge9sJl5yv0nNuoUmLiDgw1tJafoqGrfd5CUasKyzSI21HxsSeQ==", + "version": "1.198.0", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.198.0.tgz", + "integrity": "sha512-QvXaLW9OTIWoXzFf9lAeVVN7q1exBjVi8Piygz771AvpnySloFrfHntFgnU0eMYSM199psANOtNlmqckG8YdfQ==", + "license": "MIT", "dependencies": { + "core-js": "^3.38.1", "fflate": "^0.4.8", - "preact": "^10.19.3" + "preact": "^10.19.3", + "web-vitals": "^4.2.0" } }, + "node_modules/posthog-js/node_modules/fflate": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", + "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==", + "license": "MIT" + }, "node_modules/preact": { - "version": "10.19.5", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.19.5.tgz", - "integrity": "sha512-OPELkDmSVbKjbFqF9tgvOowiiQ9TmsJljIzXRyNE8nGiis94pwv1siF78rQkAP1Q1738Ce6pellRg/Ns/CtHqQ==", + "version": "10.25.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.25.2.tgz", + "integrity": "sha512-GEts1EH3oMnqdOIeXhlbBSddZ9nrINd070WBOiPO2ous1orrKGUM4SMDbwyjSWD1iMS2dBvaDjAa5qUhz3TXqw==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" } }, - "node_modules/prebuild-install": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", - "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", - "dev": true, - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prebuild-install/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true - }, - "node_modules/prebuild-install/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, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/prebuild-install/node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "dev": true, - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/prebuild-install/node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } }, "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true, + "license": "MIT", "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" @@ -20112,6 +11065,7 @@ "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", "dev": true, + "license": "MIT", "dependencies": { "fast-diff": "^1.1.2" }, @@ -20120,29 +11074,32 @@ } }, "node_modules/prettier-plugin-tailwindcss": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.2.8.tgz", - "integrity": "sha512-KgPcEnJeIijlMjsA6WwYgRs5rh3/q76oInqtMXBA/EMcamrcYJpyhtRhyX1ayT9hnHlHTuO8sIifHF10WuSDKg==", + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.9.tgz", + "integrity": "sha512-r0i3uhaZAXYP0At5xGfJH876W3HHGHDp+LCRUJrs57PBeQ6mYHMwr25KH8NPX44F2yGTvdnH7OqCshlQx183Eg==", "dev": true, + "license": "MIT", "engines": { - "node": ">=12.17.0" + "node": ">=14.21.3" }, "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", - "@shufo/prettier-plugin-blade": "*", "@trivago/prettier-plugin-sort-imports": "*", - "prettier": ">=2.2.0", + "@zackad/prettier-plugin-twig-melody": "*", + "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", + "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", - "prettier-plugin-svelte": "*", - "prettier-plugin-twig-melody": "*" + "prettier-plugin-svelte": "*" }, "peerDependenciesMeta": { "@ianvs/prettier-plugin-sort-imports": { @@ -20154,10 +11111,10 @@ "@shopify/prettier-plugin-liquid": { "optional": true }, - "@shufo/prettier-plugin-blade": { + "@trivago/prettier-plugin-sort-imports": { "optional": true }, - "@trivago/prettier-plugin-sort-imports": { + "@zackad/prettier-plugin-twig-melody": { "optional": true }, "prettier-plugin-astro": { @@ -20172,84 +11129,36 @@ "prettier-plugin-jsdoc": { "optional": true }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-multiline-arrays": { + "optional": true + }, "prettier-plugin-organize-attributes": { "optional": true }, "prettier-plugin-organize-imports": { "optional": true }, + "prettier-plugin-sort-imports": { + "optional": true + }, "prettier-plugin-style-order": { "optional": true }, "prettier-plugin-svelte": { "optional": true - }, - "prettier-plugin-twig-melody": { - "optional": true } } }, - "node_modules/pretty-bytes": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", - "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", - "dev": true, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", "engines": { "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pretty-error": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", - "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", - "dev": true, - "dependencies": { - "lodash": "^4.17.20", - "renderkid": "^3.0.0" - } - }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/pretty-format/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - }, - "node_modules/pretty-hrtime": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", - "dev": true, - "engines": { - "node": ">= 0.8" } }, "node_modules/process": { @@ -20257,6 +11166,7 @@ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6.0" } @@ -20264,97 +11174,42 @@ "node_modules/process-nextick-args": { "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==" - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "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, - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } + "license": "MIT" }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/property-expr": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", - "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==" - }, "node_modules/property-information": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.4.0.tgz", - "integrity": "sha512-9t5qARVofg2xQqKtytzt+lZ4d1Qvj8t5B8fEwXK6qOfgRLgH/b13QlgEyDh033NOS31nXeFbYv7CLUDG1CeifQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.0.0.tgz", + "integrity": "sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/proxy-from-env": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", - "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", - "dev": true - }, - "node_modules/prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", - "dev": true, - "optional": true, - "peer": true - }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" }, "node_modules/public-encrypt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", "dev": true, + "license": "MIT", "dependencies": { "bn.js": "^4.1.0", "browserify-rsa": "^4.0.0", @@ -20365,192 +11220,65 @@ } }, "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "dev": true, - "dependencies": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - } - }, - "node_modules/pumpify/node_modules/pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } + "license": "MIT" }, "node_modules/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/puppeteer-core": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-2.1.1.tgz", - "integrity": "sha512-n13AWriBMPYxnpbb6bnaY5YoY6rGj8vPLrz6CZF3o0qJNEwlcfJVxBzYZ0NJsQ21UbdJoijPCDrM++SUVEz7+w==", - "dev": true, - "dependencies": { - "@types/mime-types": "^2.1.0", - "debug": "^4.1.0", - "extract-zip": "^1.6.6", - "https-proxy-agent": "^4.0.0", - "mime": "^2.0.3", - "mime-types": "^2.1.25", - "progress": "^2.0.1", - "proxy-from-env": "^1.0.0", - "rimraf": "^2.6.1", - "ws": "^6.1.0" - }, - "engines": { - "node": ">=8.16.0" - } - }, - "node_modules/puppeteer-core/node_modules/extract-zip": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", - "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", - "dev": true, - "dependencies": { - "concat-stream": "^1.6.2", - "debug": "^2.6.9", - "mkdirp": "^0.5.4", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - } - }, - "node_modules/puppeteer-core/node_modules/extract-zip/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/puppeteer-core/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/puppeteer-core/node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/puppeteer-core/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/puppeteer-core/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/puppeteer-core/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/puppeteer-core/node_modules/ws": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", - "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", - "dev": true, - "dependencies": { - "async-limiter": "~1.0.0" - } - }, "node_modules/pvtsutils": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.5.tgz", - "integrity": "sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", "dependencies": { - "tslib": "^2.6.1" + "tslib": "^2.8.1" } }, "node_modules/pvutils": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", + "license": "MIT", "engines": { "node": ">=6.0.0" } }, - "node_modules/qs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", - "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", - "dev": true, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", "dependencies": { - "side-channel": "^1.0.4" + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qs": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz", + "integrity": "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -20559,31 +11287,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/query-string": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", - "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", - "dependencies": { - "decode-uri-component": "^0.2.2", - "filter-obj": "^1.1.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/query-string/node_modules/filter-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", - "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/querystring-es3": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", @@ -20593,21 +11296,6 @@ "node": ">=0.4.x" } }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true - }, - "node_modules/queue": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", - "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "dev": true, - "dependencies": { - "inherits": "~2.0.3" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -20626,54 +11314,24 @@ "type": "consulting", "url": "https://feross.org/support" } - ] - }, - "node_modules/queue-tick": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", - "dev": true - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + ], + "license": "MIT" }, "node_modules/raf": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", "optional": true, "dependencies": { "performance-now": "^2.1.0" } }, - "node_modules/raf-schd": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", - "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" - }, - "node_modules/ramda": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", - "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" } @@ -20683,128 +11341,29 @@ "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", "dev": true, + "license": "MIT", "dependencies": { "randombytes": "^2.0.5", "safe-buffer": "^5.1.0" } }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dev": true, - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/react-beautiful-dnd": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", - "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", - "dependencies": { - "@babel/runtime": "^7.9.2", - "css-box-model": "^1.2.0", - "memoize-one": "^5.1.1", - "raf-schd": "^4.0.2", - "react-redux": "^7.2.0", - "redux": "^4.0.4", - "use-memo-one": "^1.1.1" - }, - "peerDependencies": { - "react": "^16.8.5 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/react-beautiful-dnd/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" - }, - "node_modules/react-beautiful-dnd/node_modules/react-redux": { - "version": "7.2.9", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", - "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", - "dependencies": { - "@babel/runtime": "^7.15.4", - "@types/react-redux": "^7.1.20", - "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^17.0.2" - }, - "peerDependencies": { - "react": "^16.8.3 || ^17 || ^18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, "node_modules/react-code-input": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/react-code-input/-/react-code-input-3.10.1.tgz", "integrity": "sha512-B1RqSc32BzFP9eoV5LWhRTmbJ8I3rKs+6E01yaJwDqcVBE4kKgRJHBmPcXRD58qkCPssnF/Aq5UDNzIiTw7eNg==", + "license": "MIT", "dependencies": { "classnames": "^2.2.5", "react": "^16.3.2", @@ -20819,6 +11378,7 @@ "version": "16.14.0", "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -20832,6 +11392,7 @@ "version": "16.14.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -20846,168 +11407,109 @@ "version": "0.19.1", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" } }, - "node_modules/react-colorful": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz", - "integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==", - "dev": true, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, "node_modules/react-day-picker": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.0.tgz", - "integrity": "sha512-mz+qeyrOM7++1NCb1ARXmkjMkzWVh2GL9YiPbRjKe0zHccvekk4HE+0MPOZOrosn8r8zTHIIeOUXTmXRqmkRmg==", + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.4.3.tgz", + "integrity": "sha512-kQmn7lBR6fHCjaDhW6ByzTMeZUgsBSvIKJLaUKq5xTuJcg2UqHiFAjrPLk/owQS/NzsnSgyGL/bhoi0Z7g+r3w==", + "license": "MIT", + "dependencies": { + "@date-fns/tz": "^1.2.0", + "date-fns": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, "funding": { "type": "individual", "url": "https://github.com/sponsors/gpbl" }, "peerDependencies": { - "date-fns": "^2.28.0 || ^3.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": ">=16.8.0" } }, - "node_modules/react-docgen": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-7.0.3.tgz", - "integrity": "sha512-i8aF1nyKInZnANZ4uZrH49qn1paRgBZ7wZiCNBMnenlPzEv0mRl+ShpTVEI6wZNl8sSc79xZkivtgLKQArcanQ==", - "dev": true, - "dependencies": { - "@babel/core": "^7.18.9", - "@babel/traverse": "^7.18.9", - "@babel/types": "^7.18.9", - "@types/babel__core": "^7.18.0", - "@types/babel__traverse": "^7.18.0", - "@types/doctrine": "^0.0.9", - "@types/resolve": "^1.20.2", - "doctrine": "^3.0.0", - "resolve": "^1.22.1", - "strip-indent": "^4.0.0" - }, - "engines": { - "node": ">=16.14.0" - } - }, - "node_modules/react-docgen-typescript": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.2.2.tgz", - "integrity": "sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==", - "dev": true, - "peerDependencies": { - "typescript": ">= 4.3.x" - } - }, - "node_modules/react-docgen/node_modules/@types/doctrine": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", - "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", - "dev": true - }, "node_modules/react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "17.0.2" + "react": "^18.3.1" } }, - "node_modules/react-draggable": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", - "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "node_modules/react-error-boundary": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", + "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", + "license": "MIT", "dependencies": { - "clsx": "^1.1.1", - "prop-types": "^15.8.1" + "@babel/runtime": "^7.12.5" }, - "peerDependencies": { - "react": ">= 16.3.0", - "react-dom": ">= 16.3.0" - } - }, - "node_modules/react-draggable/node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", "engines": { - "node": ">=6" - } - }, - "node_modules/react-element-to-jsx-string": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/react-element-to-jsx-string/-/react-element-to-jsx-string-15.0.0.tgz", - "integrity": "sha512-UDg4lXB6BzlobN60P8fHWVPX3Kyw8ORrTeBtClmIlGdkOOE+GYQSFvmEU5iLLpwp/6v42DINwNcwOhOLfQ//FQ==", - "dev": true, - "dependencies": { - "@base2/pretty-print-object": "1.0.1", - "is-plain-object": "5.0.0", - "react-is": "18.1.0" + "node": ">=10", + "npm": ">=6" }, "peerDependencies": { - "react": "^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0", - "react-dom": "^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0" + "react": ">=16.13.1" } }, - "node_modules/react-element-to-jsx-string/node_modules/react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" }, - "node_modules/react-grid-layout": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.4.4.tgz", - "integrity": "sha512-7+Lg8E8O8HfOH5FrY80GCIR1SHTn2QnAYKh27/5spoz+OHhMmEhU/14gIkRzJOtympDPaXcVRX/nT1FjmeOUmQ==", + "node_modules/react-helmet": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz", + "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==", + "license": "MIT", "dependencies": { - "clsx": "^2.0.0", - "fast-equals": "^4.0.3", - "prop-types": "^15.8.1", - "react-draggable": "^4.4.5", - "react-resizable": "^3.0.5", - "resize-observer-polyfill": "^1.5.1" + "object-assign": "^4.1.1", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.1.1", + "react-side-effect": "^2.1.0" }, "peerDependencies": { - "react": ">= 16.3.0", - "react-dom": ">= 16.3.0" + "react": ">=16.3.0" } }, "node_modules/react-hook-form": { - "version": "7.49.3", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.49.3.tgz", - "integrity": "sha512-foD6r3juidAT1cOZzpmD/gOKt7fRsDhXXZ0y28+Al1CHgX+AY1qIN9VSIIItXRq1dN68QrRwl1ORFlwjBaAqeQ==", + "version": "7.54.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.0.tgz", + "integrity": "sha512-PS05+UQy/IdSbJNojBypxAo9wllhHgGmyr8/dyGQcPoiMf3e7Dfb9PWYVRco55bLbxH9S+1yDDJeTdlYCSxO3A==", + "license": "MIT", "engines": { - "node": ">=18", - "pnpm": "8" + "node": ">=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/react-hook-form" }, "peerDependencies": { - "react": "^16.8.0 || ^17 || ^18" + "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "node_modules/react-i18next": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.3.1.tgz", - "integrity": "sha512-5v8E2XjZDFzK7K87eSwC7AJcAkcLt5xYZ4+yTPDAW1i7C93oOY1dnr4BaQM7un4Hm+GmghuiPvevWwlca5PwDA==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.2.0.tgz", + "integrity": "sha512-iJNc8111EaDtVTVMKigvBtPHyrJV+KblWG73cUxqp+WmJCcwkzhWNFXmkAD5pwP2Z4woeDj/oXDdbjDsb3Gutg==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.20.6", + "@babel/runtime": "^7.25.0", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { - "i18next": ">= 19.0.0", + "i18next": ">= 23.2.3", "react": ">= 16.8.0" }, "peerDependenciesMeta": { @@ -21020,114 +11522,53 @@ } }, "node_modules/react-icons": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz", - "integrity": "sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz", + "integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==", + "license": "MIT", "peerDependencies": { "react": "*" } }, "node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" - }, - "node_modules/react-mailchimp-subscribe": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/react-mailchimp-subscribe/-/react-mailchimp-subscribe-2.1.3.tgz", - "integrity": "sha512-ZRuPZMnX/9pHQLnAQavsgB5xIF+gNqjNCCq1vvTs23cn+93W2oOp17qjg3LpDBEt1HJi6IHXMwpKXn0taY8FHw==", - "dependencies": { - "jsonp": "^0.2.1", - "prop-types": "^15.5.10", - "to-querystring": "^1.0.4" - }, - "peerDependencies": { - "react": ">=15" - } + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" }, "node_modules/react-markdown": { - "version": "8.0.7", - "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.7.tgz", - "integrity": "sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.0.1.tgz", + "integrity": "sha512-Qt9TWsQJ75np2AVoKftns5eI7r50H6u3qwp+TSihlxOcw8ZaStmR0FEeeENU+mWSxyAgOmqMYjiIKn7ibMheKA==", "dependencies": { - "@types/hast": "^2.0.0", - "@types/prop-types": "^15.0.0", - "@types/unist": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-whitespace": "^2.0.0", - "prop-types": "^15.0.0", - "property-information": "^6.0.0", - "react-is": "^18.0.0", - "remark-parse": "^10.0.0", - "remark-rehype": "^10.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-object": "^0.4.0", - "unified": "^10.0.0", - "unist-util-visit": "^4.0.0", - "vfile": "^5.0.0" + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" }, "peerDependencies": { - "@types/react": ">=16", - "react": ">=16" - } - }, - "node_modules/react-redux": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", - "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", - "dependencies": { - "@babel/runtime": "^7.12.1", - "@types/hoist-non-react-statics": "^3.3.1", - "@types/use-sync-external-store": "^0.0.3", - "hoist-non-react-statics": "^3.3.2", - "react-is": "^18.0.0", - "use-sync-external-store": "^1.0.0" - }, - "peerDependencies": { - "@types/react": "^16.8 || ^17.0 || ^18.0", - "@types/react-dom": "^16.8 || ^17.0 || ^18.0", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0", - "react-native": ">=0.59", - "redux": "^4 || ^5.0.0-beta.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - }, - "redux": { - "optional": true - } - } - }, - "node_modules/react-refresh": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", - "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "@types/react": ">=18", + "react": ">=18" } }, "node_modules/react-remove-scroll": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", - "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz", + "integrity": "sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==", + "license": "MIT", "dependencies": { - "react-remove-scroll-bar": "^2.3.3", + "react-remove-scroll-bar": "^2.3.6", "react-style-singleton": "^2.2.1", "tslib": "^2.1.0", "use-callback-ref": "^1.3.0", @@ -21147,9 +11588,10 @@ } }, "node_modules/react-remove-scroll-bar": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz", - "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", + "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", + "license": "MIT", "dependencies": { "react-style-singleton": "^2.2.1", "tslib": "^2.0.0" @@ -21167,22 +11609,10 @@ } } }, - "node_modules/react-resizable": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.5.tgz", - "integrity": "sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==", - "dependencies": { - "prop-types": "15.x", - "react-draggable": "^4.0.3" - }, - "peerDependencies": { - "react": ">= 16.3" - } - }, "node_modules/react-select": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.1.tgz", - "integrity": "sha512-RT1CJmuc+ejqm5MPgzyZujqDskdvB9a9ZqrdnVLsvAHjJ3Tj0hELnLeVPQlmYdVKCdCpxanepl6z7R5KhXhWzg==", + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.9.0.tgz", + "integrity": "sha512-nwRKGanVHGjdccsnzhFte/PULziueZxGD8LL2WojON78Mvnq7LdAMEtu2frrwld1fr3geixg3iiMBIc/LLAZpw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.0", @@ -21193,23 +11623,27 @@ "memoize-one": "^6.0.0", "prop-types": "^15.6.0", "react-transition-group": "^4.3.0", - "use-isomorphic-layout-effect": "^1.1.2" + "use-isomorphic-layout-effect": "^1.2.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/react-select/node_modules/memoize-one": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", - "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", - "license": "MIT" + "node_modules/react-side-effect": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", + "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0 || ^18.0.0" + } }, "node_modules/react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "license": "MIT", "dependencies": { "get-nonce": "^1.0.0", "invariant": "^2.2.4", @@ -21228,36 +11662,17 @@ } } }, - "node_modules/react-table": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.8.0.tgz", - "integrity": "sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.3 || ^17.0.0-0 || ^18.0.0" - } - }, "node_modules/react-toastify": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.3.tgz", - "integrity": "sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==", + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.6.tgz", + "integrity": "sha512-yYjp+omCDf9lhZcrZHKbSq7YMuK0zcYkDFTzfRFgTXkTFHZ1ToxwAonzA4JI5CxA91JpjFLmwEsZEgfYfOqI1A==", + "license": "MIT", "dependencies": { - "clsx": "^1.1.1" + "clsx": "^2.1.0" }, "peerDependencies": { - "react": ">=16", - "react-dom": ">=16" - } - }, - "node_modules/react-toastify/node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "engines": { - "node": ">=6" + "react": ">=18", + "react-dom": ">=18" } }, "node_modules/react-transition-group": { @@ -21281,138 +11696,31 @@ "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-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "dependencies": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "dependencies": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg/node_modules/type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/readable-stream/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" - }, - "node_modules/readable-stream/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, + "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -21420,64 +11728,46 @@ "node": ">=8.10.0" } }, - "node_modules/recast": { - "version": "0.23.4", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.4.tgz", - "integrity": "sha512-qtEDqIZGVcSZCHniWwZWbRy79Dc6Wp3kT/UmDA2RJKBPg7+7k51aQBZirHmUGn5uvHf2rg8DkjizrN26k61ATw==", + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, - "dependencies": { - "assert": "^2.0.0", - "ast-types": "^0.16.1", - "esprima": "~4.0.0", - "source-map": "~0.6.1", - "tslib": "^2.0.1" + "license": "MIT", + "engines": { + "node": ">=8.6" }, - "engines": { - "node": ">= 4" + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/recast/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "dependencies": { - "@babel/runtime": "^7.9.2" - } - }, - "node_modules/redux-thunk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", - "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", - "peerDependencies": { - "redux": "^4" - } + "node_modules/redaxios": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/redaxios/-/redaxios-0.5.1.tgz", + "integrity": "sha512-FSD2AmfdbkYwl7KDExYQlVvIrFz6Yd83pGfaGjBzM9F6rpq8g652Q4Yq5QD4c+nf4g2AgeElv1y+8ajUPiOYMg==", + "license": "Apache-2.0" }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", - "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" }, "node_modules/reflect.getprototypeof": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", - "integrity": "sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.8.tgz", + "integrity": "sha512-B5dj6usc5dkk8uFliwjwDHM8To5/QwdKz9JcBZ8Ic4G1f0YmeeJTtE/ZTdgRFPAfxZFiUaPhZ1Jcs4qeagItGQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "globalthis": "^1.0.3", - "which-builtin-type": "^1.1.3" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "dunder-proto": "^1.0.0", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.2.0", + "which-builtin-type": "^1.2.0" }, "engines": { "node": ">= 0.4" @@ -21486,53 +11776,23 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", - "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", - "dev": true, - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, - "node_modules/regenerator-transform": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", - "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, - "node_modules/regex-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", - "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", - "dev": true + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" }, "node_modules/regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", + "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -21541,113 +11801,14 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", - "dev": true, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", "dependencies": { - "@babel/regjsgen": "^0.8.0", - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", - "dev": true, - "dependencies": { - "jsesc": "~0.5.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - } - }, - "node_modules/relateurl": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/remark-external-links": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/remark-external-links/-/remark-external-links-8.0.0.tgz", - "integrity": "sha512-5vPSX0kHoSsqtdftSHhIYofVINC8qmp0nctkeU9YoJwV3YfiBRiI6cbFRJ0oI/1F9xS+bopXG0m2KS8VFscuKA==", - "dev": true, - "dependencies": { - "extend": "^3.0.0", - "is-absolute-url": "^3.0.0", - "mdast-util-definitions": "^4.0.0", - "space-separated-tokens": "^1.0.0", - "unist-util-visit": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-external-links/node_modules/space-separated-tokens": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", - "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/remark-external-links/node_modules/unist-util-is": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", - "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-external-links/node_modules/unist-util-visit": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", - "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0", - "unist-util-visit-parents": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-external-links/node_modules/unist-util-visit-parents": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", - "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0" + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" }, "funding": { "type": "opencollective", @@ -21655,13 +11816,14 @@ } }, "node_modules/remark-parse": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", - "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-from-markdown": "^1.0.0", - "unified": "^10.0.0" + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" }, "funding": { "type": "opencollective", @@ -21669,214 +11831,41 @@ } }, "node_modules/remark-rehype": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-10.1.0.tgz", - "integrity": "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.1.tgz", + "integrity": "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==", "dependencies": { - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "mdast-util-to-hast": "^12.1.0", - "unified": "^10.0.0" + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/remark-slug": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/remark-slug/-/remark-slug-6.1.0.tgz", - "integrity": "sha512-oGCxDF9deA8phWvxFuyr3oSJsdyUAxMFbA0mZ7Y1Sas+emILtO+e5WutF9564gDsEN4IXaQXm5pFo6MLH+YmwQ==", - "dev": true, - "dependencies": { - "github-slugger": "^1.0.0", - "mdast-util-to-string": "^1.0.0", - "unist-util-visit": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-slug/node_modules/mdast-util-to-string": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz", - "integrity": "sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-slug/node_modules/unist-util-is": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", - "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-slug/node_modules/unist-util-visit": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", - "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0", - "unist-util-visit-parents": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-slug/node_modules/unist-util-visit-parents": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", - "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/renderkid": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", - "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", - "dev": true, - "dependencies": { - "css-select": "^4.1.3", - "dom-converter": "^0.2.0", - "htmlparser2": "^6.1.0", - "lodash": "^4.17.21", - "strip-ansi": "^6.0.1" - } - }, - "node_modules/renderkid/node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dev": true, - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/renderkid/node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "dev": true, - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/renderkid/node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/renderkid/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/renderkid/node_modules/htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" - } - }, - "node_modules/request-progress": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", - "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", - "dev": true, - "dependencies": { - "throttleit": "^1.0.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/requireindex": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", - "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", - "dev": true, - "engines": { - "node": ">=0.10.5" - } - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" - }, - "node_modules/reselect": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", - "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" - }, - "node_modules/resize-observer-polyfill": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -21890,12 +11879,12 @@ } }, "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/resolve-pkg-maps": { @@ -21903,82 +11892,27 @@ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/resolve-url-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", - "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", - "dev": true, - "dependencies": { - "adjust-sourcemap-loader": "^4.0.0", - "convert-source-map": "^1.7.0", - "loader-utils": "^2.0.0", - "postcss": "^8.2.14", - "source-map": "0.6.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/resolve-url-loader/node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dev": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/resolve-url-loader/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "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, - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true, + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" } }, - "node_modules/rfdc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", - "dev": true - }, "node_modules/rgbcolor": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", "optional": true, "engines": { "node": ">= 0.8.15" @@ -21988,7 +11922,9 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { "glob": "^7.1.3" }, @@ -21999,35 +11935,55 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/ripemd160": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "license": "MIT", "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1" } }, + "node_modules/rollup": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz", + "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.28.1", + "@rollup/rollup-android-arm64": "4.28.1", + "@rollup/rollup-darwin-arm64": "4.28.1", + "@rollup/rollup-darwin-x64": "4.28.1", + "@rollup/rollup-freebsd-arm64": "4.28.1", + "@rollup/rollup-freebsd-x64": "4.28.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.28.1", + "@rollup/rollup-linux-arm-musleabihf": "4.28.1", + "@rollup/rollup-linux-arm64-gnu": "4.28.1", + "@rollup/rollup-linux-arm64-musl": "4.28.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.28.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1", + "@rollup/rollup-linux-riscv64-gnu": "4.28.1", + "@rollup/rollup-linux-s390x-gnu": "4.28.1", + "@rollup/rollup-linux-x64-gnu": "4.28.1", + "@rollup/rollup-linux-x64-musl": "4.28.1", + "@rollup/rollup-win32-arm64-msvc": "4.28.1", + "@rollup/rollup-win32-ia32-msvc": "4.28.1", + "@rollup/rollup-win32-x64-msvc": "4.28.1", + "fsevents": "~2.3.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -22047,39 +12003,22 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } }, - "node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dependencies": { - "mri": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/safe-array-concat": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", - "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", - "get-intrinsic": "^1.2.2", - "has-symbols": "^1.0.3", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", "isarray": "^2.0.5" }, "engines": { @@ -22106,16 +12045,18 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/safe-regex-test": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", - "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", - "get-intrinsic": "^1.2.2", + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", "is-regex": "^1.1.4" }, "engines": { @@ -22125,210 +12066,63 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "node_modules/sanitize-html": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.12.1.tgz", - "integrity": "sha512-Plh+JAn0UVDpBRP/xEjsk+xDCoOvMBwQUf/K+/cBAVuTbtX8bj2VB7S1sL1dssVpykqp0/KPSesHrqXtokVBpA==", - "dependencies": { - "deepmerge": "^4.2.2", - "escape-string-regexp": "^4.0.0", - "htmlparser2": "^8.0.0", - "is-plain-object": "^5.0.0", - "parse-srcset": "^1.0.2", - "postcss": "^8.3.11" - } - }, - "node_modules/sass-loader": { - "version": "13.3.3", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.3.tgz", - "integrity": "sha512-mt5YN2F1MOZr3d/wBRcZxeFgwgkH44wVc2zohO2YF6JiOMkiXe4BYRZpSu2sO1g71mo/j16txzUhsKZlqjVGzA==", - "dev": true, - "dependencies": { - "neo-async": "^2.6.2" - }, - "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "fibers": ">= 3.1.0", - "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", - "sass": "^1.3.0", - "sass-embedded": "*", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "fibers": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - } - } - }, - "node_modules/sax": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", - "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/section-matter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", - "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", - "dependencies": { - "extend-shallow": "^2.0.1", - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=4" + "loose-envify": "^1.1.0" } }, "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" - } - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" }, "engines": { - "node": ">= 0.8.0" + "node": ">=10" } }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { + "node_modules/set-blocking": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "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, - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dev": true, - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-cookie-parser": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", - "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" }, "node_modules/set-function-length": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", - "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "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.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/set-function-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, + "license": "MIT", "dependencies": { - "define-data-property": "^1.0.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -22338,18 +12132,14 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true + "dev": true, + "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==", + "license": "(MIT AND BSD-3-Clause)", "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -22358,97 +12148,12 @@ "sha.js": "bin.js" } }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" - }, - "node_modules/sharp": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.2.tgz", - "integrity": "sha512-WlYOPyyPDiiM07j/UO+E720ju6gtNtHjEGg5vovUk1Lgxyjm2LFO+37Nt/UI3MMh2l6hxTWQWi7qk3cXJTutcQ==", - "hasInstallScript": true, - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.2", - "semver": "^7.5.4" - }, - "engines": { - "libvips": ">=8.15.1", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.2", - "@img/sharp-darwin-x64": "0.33.2", - "@img/sharp-libvips-darwin-arm64": "1.0.1", - "@img/sharp-libvips-darwin-x64": "1.0.1", - "@img/sharp-libvips-linux-arm": "1.0.1", - "@img/sharp-libvips-linux-arm64": "1.0.1", - "@img/sharp-libvips-linux-s390x": "1.0.1", - "@img/sharp-libvips-linux-x64": "1.0.1", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.1", - "@img/sharp-libvips-linuxmusl-x64": "1.0.1", - "@img/sharp-linux-arm": "0.33.2", - "@img/sharp-linux-arm64": "0.33.2", - "@img/sharp-linux-s390x": "0.33.2", - "@img/sharp-linux-x64": "0.33.2", - "@img/sharp-linuxmusl-arm64": "0.33.2", - "@img/sharp-linuxmusl-x64": "0.33.2", - "@img/sharp-wasm32": "0.33.2", - "@img/sharp-win32-ia32": "0.33.2", - "@img/sharp-win32-x64": "0.33.2" - } - }, - "node_modules/sharp/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sharp/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sharp/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -22461,154 +12166,115 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "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" - } - ] - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "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" - } - ], - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "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": ">=8" - } - }, - "node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" + "node": ">=14" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", - "engines": { - "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, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/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 - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -22622,204 +12288,40 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "node_modules/stable-hash": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", + "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", "dev": true, - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.18", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", - "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", - "dev": true - }, - "node_modules/split-on-first": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", - "engines": { - "node": ">=6" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" - }, - "node_modules/sshpk": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", - "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", - "dev": true, - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sshpk/node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", - "dev": true - }, - "node_modules/sshpk/node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "dev": true + "license": "MIT" }, "node_modules/stackblur-canvas": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.6.0.tgz", - "integrity": "sha512-8S1aIA+UoF6erJYnglGPug6MaHYGo1Ot7h5fuXx4fUPvcvQfcdw2o/ppCse63+eZf8PPidSu4v1JnmEVtEDnpg==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", "optional": true, "engines": { "node": ">=0.1.14" } }, - "node_modules/stackframe": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", - "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", - "dev": true - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/stop-iteration-iterator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", - "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", - "dev": true, - "dependencies": { - "internal-slot": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/store2": { - "version": "2.14.2", - "resolved": "https://registry.npmjs.org/store2/-/store2-2.14.2.tgz", - "integrity": "sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w==", - "dev": true - }, - "node_modules/storybook": { - "version": "7.6.20", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-7.6.20.tgz", - "integrity": "sha512-Wt04pPTO71pwmRmsgkyZhNo4Bvdb/1pBAMsIFb9nQLykEdzzpXjvingxFFvdOG4nIowzwgxD+CLlyRqVJqnATw==", - "dev": true, - "dependencies": { - "@storybook/cli": "7.6.20" - }, - "bin": { - "sb": "index.js", - "storybook": "index.js" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/storybook-dark-mode": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/storybook-dark-mode/-/storybook-dark-mode-3.0.3.tgz", - "integrity": "sha512-ZLBLVpkuKTdtUv3DTuOjeP/bE7DHhOxVpDROKc0NtEYq9JHLUu6z05LLZinE3v6QPXQZ9TMQPm3Xe/0BcLEZlw==", - "dev": true, - "dependencies": { - "@storybook/addons": "^7.0.0", - "@storybook/components": "^7.0.0", - "@storybook/core-events": "^7.0.0", - "@storybook/global": "^5.0.0", - "@storybook/manager-api": "^7.0.0", - "@storybook/theming": "^7.0.0", - "fast-deep-equal": "^3.1.3", - "memoizerific": "^1.11.3" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, "node_modules/stream-browserify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", "dev": true, + "license": "MIT", "dependencies": { "inherits": "~2.0.4", "readable-stream": "^3.5.0" } }, - "node_modules/stream-browserify/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, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/stream-http": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", "dev": true, + "license": "MIT", "dependencies": { "builtin-status-codes": "^3.0.0", "inherits": "^2.0.4", @@ -22827,67 +12329,31 @@ "xtend": "^4.0.2" } }, - "node_modules/stream-http/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, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/stream-shift": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", - "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", - "dev": true - }, - "node_modules/streamx": { - "version": "2.15.8", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.8.tgz", - "integrity": "sha512-6pwMeMY/SuISiRsuS8TeIrAzyFbG5gGPHFQsYjUr/pbBadaL1PCWmzKw+CHZSwainfvcF6Si6cVLq4XTEwswFQ==", - "dev": true, - "dependencies": { - "fast-fifo": "^1.1.0", - "queue-tick": "^1.0.1" - }, - "optionalDependencies": { - "bare-events": "^2.2.0" - } - }, - "node_modules/strict-uri-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", - "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", - "engines": { - "node": ">=4" - } - }, "node_modules/string_decoder": { "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==", + "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } }, "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/string-width-cjs": { @@ -22896,6 +12362,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -22909,43 +12376,108 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/string-width/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/string.prototype.matchall": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", - "integrity": "sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==", + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", + "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "regexp.prototype.flags": "^1.5.0", - "set-function-name": "^2.0.0", - "side-channel": "^1.0.4" + "internal-slot": "^1.0.7", + "regexp.prototype.flags": "^1.5.2", + "set-function-name": "^2.0.2", + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/string.prototype.trim": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", - "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -22955,38 +12487,60 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", - "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", - "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "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" }, @@ -23000,6 +12554,7 @@ "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" }, @@ -23012,47 +12567,17 @@ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, - "node_modules/strip-bom-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", - "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", - "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==", - "dev": true, - "dependencies": { - "min-indent": "^1.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -23060,133 +12585,96 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/style-loader": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", - "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", - "dev": true, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, "node_modules/style-to-object": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.4.tgz", - "integrity": "sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", + "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==", "dependencies": { - "inline-style-parser": "0.1.1" - } - }, - "node_modules/style-value-types": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.0.0.tgz", - "integrity": "sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==", - "dependencies": { - "hey-listen": "^1.0.8", - "tslib": "^2.1.0" - } - }, - "node_modules/styled-components": { - "version": "5.3.11", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz", - "integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==", - "dependencies": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/traverse": "^7.4.5", - "@emotion/is-prop-valid": "^1.1.0", - "@emotion/stylis": "^0.8.4", - "@emotion/unitless": "^0.7.4", - "babel-plugin-styled-components": ">= 1.12.0", - "css-to-react-native": "^3.0.0", - "hoist-non-react-statics": "^3.0.0", - "shallowequal": "^1.1.0", - "supports-color": "^5.5.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/styled-components" - }, - "peerDependencies": { - "react": ">= 16.8.0", - "react-dom": ">= 16.8.0", - "react-is": ">= 16.8.0" - } - }, - "node_modules/styled-components/node_modules/@emotion/is-prop-valid": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", - "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", - "dependencies": { - "@emotion/memoize": "^0.8.1" - } - }, - "node_modules/styled-components/node_modules/@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" - }, - "node_modules/styled-components/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/styled-components/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/styled-jsx": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", - "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", - "dev": true, - "dependencies": { - "client-only": "0.0.1" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } + "inline-style-parser": "0.2.4" } }, "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/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/sucrase/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/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" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "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" }, @@ -23198,6 +12686,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -23209,88 +12698,75 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", "optional": true, "engines": { "node": ">=12.0.0" } }, - "node_modules/swc-loader": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/swc-loader/-/swc-loader-0.2.3.tgz", - "integrity": "sha512-D1p6XXURfSPleZZA/Lipb3A8pZ17fP4NObZvFCDjK/OKljroqDpPmsBdTraWhVBqUNpcWBQY1imWdoPScRlQ7A==", + "node_modules/synckit": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", "dev": true, - "peerDependencies": { - "@swc/core": "^1.2.147", - "webpack": ">=2" + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" } }, - "node_modules/synchronous-promise": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.17.tgz", - "integrity": "sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==", - "dev": true - }, "node_modules/tailwind-merge": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz", - "integrity": "sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.5.tgz", + "integrity": "sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/dcastil" } }, "node_modules/tailwindcss": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.7.tgz", - "integrity": "sha512-B6DLqJzc21x7wntlH/GsZwEXTBttVSl1FtCzC8WP4oBc/NKef7kaax5jeihkkCEWc831/5NDJ9gRNDK6NEioQQ==", + "version": "3.4.16", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.16.tgz", + "integrity": "sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw==", "dev": true, + "license": "MIT", "dependencies": { + "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", - "chokidar": "^3.5.3", - "color-name": "^1.1.4", - "detective": "^5.2.1", + "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.2.12", + "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "lilconfig": "^2.0.6", - "micromatch": "^4.0.5", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.0.9", - "postcss-import": "^14.1.0", - "postcss-js": "^4.0.0", - "postcss-load-config": "^3.1.4", - "postcss-nested": "6.0.0", - "postcss-selector-parser": "^6.0.11", - "postcss-value-parser": "^4.2.0", - "quick-lru": "^5.1.1", - "resolve": "^1.22.1" + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" }, "engines": { - "node": ">=12.13.0" - }, - "peerDependencies": { - "postcss": "^8.0.9" - } - }, - "node_modules/tailwindcss/node_modules/postcss-selector-parser": { - "version": "6.0.15", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", - "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", - "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" + "node": ">=14.0.0" } }, "node_modules/tapable": { @@ -23298,299 +12774,16 @@ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dev": true, - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar-fs": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.5.tgz", - "integrity": "sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg==", - "dev": true, - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - }, - "optionalDependencies": { - "bare-fs": "^2.1.1", - "bare-path": "^2.1.0" - } - }, - "node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/telejson": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/telejson/-/telejson-7.2.0.tgz", - "integrity": "sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ==", - "dev": true, - "dependencies": { - "memoizerific": "^1.11.3" - } - }, - "node_modules/temp": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.4.tgz", - "integrity": "sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg==", - "dev": true, - "dependencies": { - "rimraf": "~2.6.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/temp-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", - "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/temp/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/temp/node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/tempy": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tempy/-/tempy-1.0.1.tgz", - "integrity": "sha512-biM9brNqxSc04Ee71hzFbryD11nX7VPhQQY32AdDmjFvodsRFz/3ufeoTZ6uYkRFfGo188tENcASNs3vTdsM0w==", - "dev": true, - "dependencies": { - "del": "^6.0.0", - "is-stream": "^2.0.0", - "temp-dir": "^2.0.0", - "type-fest": "^0.16.0", - "unique-string": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tempy/node_modules/type-fest": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", - "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/terser": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.26.0.tgz", - "integrity": "sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==", - "dev": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "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.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" - }, - "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/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, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/terser-webpack-plugin/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, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/terser/node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "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 - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/text-segmentation": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", "optional": true, "dependencies": { "utrie": "^1.0.2" @@ -23600,45 +12793,30 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "node_modules/throttleit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", - "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", "dev": true, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" } }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" - }, - "node_modules/through2": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz", - "integrity": "sha512-45Llu+EwHKtAZYTPPVn3XZHBgakWMN3rokhEv5hu596XP+cNgplMg+Gj+1nmAvj+L0K7+N49zBKx5rah5u0QIQ==", + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", "dependencies": { - "readable-stream": "~1.0.17", - "xtend": "~2.1.1" - } - }, - "node_modules/through2/node_modules/object-keys": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", - "integrity": "sha512-ncrLw+X55z7bkl5PnUvHwFK9FcGuFYo9gtjws2XtSzL+aZ8tm830P60WJ0dSmFVaSalWieW5MD7kEdnXda9yJw==" - }, - "node_modules/through2/node_modules/xtend": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", - "integrity": "sha512-vMNKzr2rHP9Dp/e1NQFnLQlwlhp9L/LfvnsVdHxN1f+uggyVI3i08uD14GPvCToPkdsRfyPqIyYGmIk58V98ZQ==", - "dependencies": { - "object-keys": "~0.4.0" + "thenify": ">= 3.1.0 < 4" }, "engines": { - "node": ">=0.4" + "node": ">=0.8" } }, "node_modules/timers-browserify": { @@ -23646,6 +12824,7 @@ "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", "dev": true, + "license": "MIT", "dependencies": { "setimmediate": "^1.0.4" }, @@ -23654,46 +12833,23 @@ } }, "node_modules/tiny-invariant": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", - "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==" + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" }, - "node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "dependencies": { - "rimraf": "^3.0.0" - }, - "engines": { - "node": ">=8.17.0" - } - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "engines": { - "node": ">=4" - } - }, - "node_modules/to-querystring": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/to-querystring/-/to-querystring-1.2.0.tgz", - "integrity": "sha512-V4qvlRNOltdNxvWGuDS71sNMH6FtFNx3GP967WT8gb6xzi4thJTUfogIln9hvz1ZmWcm9hY35LklMexo0YVHdg==" + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "license": "MIT" }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -23701,54 +12857,11 @@ "node": ">=8.0" } }, - "node_modules/tocbot": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/tocbot/-/tocbot-4.25.0.tgz", - "integrity": "sha512-kE5wyCQJ40hqUaRVkyQ4z5+4juzYsv/eK+aqD97N62YH0TxFhzJvo22RUQQZdO3YnXAk42ZOfOpjVdy+Z0YokA==", - "dev": true - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/toposort": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", - "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" - }, - "node_modules/tough-cookie": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", - "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", - "dev": true, - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" }, "node_modules/trim-lines": { "version": "3.0.1", @@ -23760,30 +12873,48 @@ } }, "node_modules/trough": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", - "integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/ts-dedent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", - "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=6.10" + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" } }, - "node_modules/ts-pnp": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz", - "integrity": "sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==", + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsconfck": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.4.tgz", + "integrity": "sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==", + "dev": true, + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, "engines": { - "node": ">=6" + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -23792,71 +12923,481 @@ } }, "node_modules/tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, + "license": "MIT", "dependencies": { - "json5": "^2.2.2", + "@types/json5": "^0.0.29", + "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tsconfig-paths-webpack-plugin": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.1.0.tgz", - "integrity": "sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA==", - "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.7.0", - "tsconfig-paths": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "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/tsscmp": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", - "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", - "engines": { - "node": ">=0.6.x" - } - }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "node_modules/tsx": { + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", + "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", "dev": true, + "license": "MIT", "dependencies": { - "tslib": "^1.8.1" + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" }, "engines": { - "node": ">= 6" + "node": ">=18.0.0" }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + "optionalDependencies": { + "fsevents": "~2.3.3" } }, - "node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", + "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz", + "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz", + "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz", + "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz", + "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz", + "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz", + "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz", + "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz", + "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz", + "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz", + "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz", + "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz", + "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz", + "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz", + "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz", + "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz", + "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz", + "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz", + "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz", + "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz", + "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz", + "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz", + "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", + "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.1", + "@esbuild/android-arm": "0.25.1", + "@esbuild/android-arm64": "0.25.1", + "@esbuild/android-x64": "0.25.1", + "@esbuild/darwin-arm64": "0.25.1", + "@esbuild/darwin-x64": "0.25.1", + "@esbuild/freebsd-arm64": "0.25.1", + "@esbuild/freebsd-x64": "0.25.1", + "@esbuild/linux-arm": "0.25.1", + "@esbuild/linux-arm64": "0.25.1", + "@esbuild/linux-ia32": "0.25.1", + "@esbuild/linux-loong64": "0.25.1", + "@esbuild/linux-mips64el": "0.25.1", + "@esbuild/linux-ppc64": "0.25.1", + "@esbuild/linux-riscv64": "0.25.1", + "@esbuild/linux-s390x": "0.25.1", + "@esbuild/linux-x64": "0.25.1", + "@esbuild/netbsd-arm64": "0.25.1", + "@esbuild/netbsd-x64": "0.25.1", + "@esbuild/openbsd-arm64": "0.25.1", + "@esbuild/openbsd-x64": "0.25.1", + "@esbuild/sunos-x64": "0.25.1", + "@esbuild/win32-arm64": "0.25.1", + "@esbuild/win32-ia32": "0.25.1", + "@esbuild/win32-x64": "0.25.1" + } }, "node_modules/tsyringe": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.8.0.tgz", "integrity": "sha512-YB1FG+axdxADa3ncEtRnQCFq/M0lALGLxSZeVNbTU8NqhOVc51nnv2CISTcvc1kyv6EGPtXVr0v6lWeDxiijOA==", + "license": "MIT", "dependencies": { "tslib": "^1.9.3" }, @@ -23867,41 +13408,34 @@ "node_modules/tsyringe/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" }, "node_modules/tty-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", - "dev": true - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } + "license": "MIT" }, "node_modules/tweetnacl": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense" }, "node_modules/tweetnacl-util": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz", - "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==" + "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==", + "license": "Unlicense" }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -23910,54 +13444,45 @@ } }, "node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=12.20" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, "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==", + "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, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" } }, "node_modules/typed-array-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", - "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -23967,16 +13492,19 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", - "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.3.tgz", + "integrity": "sha512-GsvTyUHTriq6o/bHcTd0vM7OQ9JEdlvluu9YISaA7+KzDzPaIzEeDFNkTfhdE3MYcNhNi0vq/LlegYgIs5yPAw==", "dev": true, + "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "reflect.getprototypeof": "^1.0.6" }, "engines": { "node": ">= 0.4" @@ -23986,60 +13514,61 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "dev": true - }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "devOptional": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, - "node_modules/uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" - }, - "node_modules/ufo": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", - "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==", - "dev": true - }, - "node_modules/uglify-js": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", - "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "node_modules/typescript-eslint": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.18.0.tgz", + "integrity": "sha512-Xq2rRjn6tzVpAyHr3+nmSg1/9k9aIHnJ2iZeOH7cfGOWqTkXTm3kwpQglEuLGdNrYvPF+2gtAs+/KF5rjVo+WQ==", "dev": true, - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.18.0", + "@typescript-eslint/parser": "8.18.0", + "@typescript-eslint/utils": "8.18.0" }, "engines": { - "node": ">=0.8.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/unbox-primitive": { @@ -24047,6 +13576,7 @@ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "has-bigints": "^1.0.2", @@ -24058,96 +13588,36 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true - }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "dev": true, - "engines": { - "node": ">=4" - } + "license": "MIT" }, "node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", "dependencies": { - "@types/unist": "^2.0.0", + "@types/unist": "^3.0.0", "bail": "^2.0.0", + "devlop": "^1.0.0", "extend": "^3.0.0", - "is-buffer": "^2.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", - "vfile": "^5.0.0" + "vfile": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/unique-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", - "dev": true, - "dependencies": { - "crypto-random-string": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/unist-util-generated": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", - "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", "dependencies": { - "@types/unist": "^2.0.0" + "@types/unist": "^3.0.0" }, "funding": { "type": "opencollective", @@ -24155,11 +13625,11 @@ } }, "node_modules/unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", "dependencies": { - "@types/unist": "^2.0.0" + "@types/unist": "^3.0.0" }, "funding": { "type": "opencollective", @@ -24167,11 +13637,11 @@ } }, "node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", "dependencies": { - "@types/unist": "^2.0.0" + "@types/unist": "^3.0.0" }, "funding": { "type": "opencollective", @@ -24179,13 +13649,13 @@ } }, "node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" }, "funding": { "type": "opencollective", @@ -24193,12 +13663,12 @@ } }, "node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" }, "funding": { "type": "opencollective", @@ -24206,71 +13676,30 @@ } }, "node_modules/universal-user-agent": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", - "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "engines": { - "node": ">= 0.8" - } + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", + "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==", + "license": "ISC" }, "node_modules/unplugin": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.6.0.tgz", - "integrity": "sha512-BfJEpWBu3aE/AyHx8VaNE/WgouoQxgH9baAiH82JjX8cqVyi3uJQstqwD5J+SZxIK326SZIhsSZlALXVBCknTQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.0.tgz", + "integrity": "sha512-5liCNPuJW8dqh3+DM6uNM2EI3MLLpCKp/KY+9pB5M2S2SR2qvvDHhKgBOaTWEbZTAws3CXfB0rKTIolWKL05VQ==", "dev": true, + "license": "MIT", "dependencies": { - "acorn": "^8.11.2", - "chokidar": "^3.5.3", - "webpack-sources": "^3.2.3", - "webpack-virtual-modules": "^0.6.1" - } - }, - "node_modules/unplugin/node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true, - "bin": { - "acorn": "bin/acorn" + "acorn": "^8.14.0", + "webpack-virtual-modules": "^0.6.2" }, "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/unplugin/node_modules/webpack-virtual-modules": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.1.tgz", - "integrity": "sha512-poXpCylU7ExuvZK8z+On3kX+S8o/2dQ/SVYueKA0D4WEMXROXgY8Ez50/bQEUmvoSMMrWcrJqCHuhAbsiwg7Dg==", - "dev": true - }, - "node_modules/untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "dev": true, - "engines": { - "node": ">=8" + "node": ">=14.0.0" } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, "funding": [ { "type": "opencollective", @@ -24285,9 +13714,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -24301,40 +13731,37 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, "node_modules/url": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.3.tgz", - "integrity": "sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==", + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", "dev": true, + "license": "MIT", "dependencies": { "punycode": "^1.4.1", - "qs": "^6.11.2" - } - }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/url/node_modules/punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/use-callback-ref": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.1.tgz", - "integrity": "sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", + "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==", + "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, @@ -24352,12 +13779,12 @@ } }, "node_modules/use-isomorphic-layout-effect": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", - "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.0.tgz", + "integrity": "sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w==", "license": "MIT", "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -24365,31 +13792,11 @@ } } }, - "node_modules/use-memo-one": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", - "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/use-resize-observer": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-9.1.0.tgz", - "integrity": "sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==", - "dev": true, - "dependencies": { - "@juggle/resize-observer": "^3.3.1" - }, - "peerDependencies": { - "react": "16.8.0 - 18", - "react-dom": "16.8.0 - 18" - } - }, "node_modules/use-sidecar": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", + "license": "MIT", "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" @@ -24408,11 +13815,12 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "license": "MIT", "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/util": { @@ -24420,6 +13828,7 @@ "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", @@ -24431,127 +13840,53 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/utila": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", - "dev": true - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" }, "node_modules/utrie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", "optional": true, "dependencies": { "base64-arraybuffer": "^1.0.2" } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } }, - "node_modules/uuidv4": { - "version": "6.2.13", - "resolved": "https://registry.npmjs.org/uuidv4/-/uuidv4-6.2.13.tgz", - "integrity": "sha512-AXyzMjazYB3ovL3q051VLH06Ixj//Knx7QnUSi1T//Ie3io6CpsPu9nVMOx5MoLWh6xV0B9J0hIaxungxXUbPQ==", - "dependencies": { - "@types/uuid": "8.3.4", - "uuid": "8.3.2" - } - }, - "node_modules/uuidv4/node_modules/@types/uuid": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", - "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==" - }, - "node_modules/uvu": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", - "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", - "dependencies": { - "dequal": "^2.0.0", - "diff": "^5.0.0", - "kleur": "^4.0.3", - "sade": "^1.7.3" - }, - "bin": { - "uvu": "bin.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/uvu/node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "node_modules/verror/node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true - }, "node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" }, "funding": { "type": "opencollective", @@ -24559,252 +13894,189 @@ } }, "node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, + "node_modules/vite": { + "version": "5.4.18", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz", + "integrity": "sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-node-polyfills": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/vite-plugin-node-polyfills/-/vite-plugin-node-polyfills-0.22.0.tgz", + "integrity": "sha512-F+G3LjiGbG8QpbH9bZ//GSBr9i1InSTkaulfUHFa9jkLqVGORFBoqc2A/Yu5Mmh1kNAbiAeKeK+6aaQUf3x0JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-inject": "^5.0.5", + "node-stdlib-browser": "^1.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/davidmyersdev" + }, + "peerDependencies": { + "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/vite-plugin-top-level-await": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.4.4.tgz", + "integrity": "sha512-QyxQbvcMkgt+kDb12m2P8Ed35Sp6nXP+l8ptGrnHV9zgYDUpraO0CPdlqLSeBqvY2DToR52nutDG7mIHuysdiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-virtual": "^3.0.2", + "@swc/core": "^1.7.0", + "uuid": "^10.0.0" + }, + "peerDependencies": { + "vite": ">=2.8" + } + }, + "node_modules/vite-plugin-wasm": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.3.0.tgz", + "integrity": "sha512-tVhz6w+W9MVsOCHzxo6SSMSswCeIw4HTrXEi6qL3IRzATl83jl09JVO1djBqPSwfjgnpVHNLYcaMbaDX5WB/pg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vite": "^2 || ^3 || ^4 || ^5" + } + }, + "node_modules/vite-tsconfig-paths": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, "node_modules/vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "dependencies": { - "makeerror": "1.0.12" + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", - "dev": true, - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, - "dependencies": { - "defaults": "^1.0.3" - } + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/webpack": { - "version": "5.94.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", - "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.7.1", - "acorn-import-attributes": "^1.9.5", - "browserslist": "^4.21.10", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "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": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.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-dev-middleware": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.2.tgz", - "integrity": "sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ==", - "dev": true, - "dependencies": { - "colorette": "^2.0.10", - "memfs": "^3.4.12", - "mime-types": "^2.1.31", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - } - } - }, - "node_modules/webpack-dev-middleware/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack-dev-middleware/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, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/webpack-dev-middleware/node_modules/schema-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", - "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/webpack-hot-middleware": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.26.0.tgz", - "integrity": "sha512-okzjec5sAEy4t+7rzdT8eRyxsk0FDSmBPN2KwX4Qd+6+oQCfe5Ve07+u7cJvofgB+B4w5/4dO4Pz0jhhHyyPLQ==", - "dev": true, - "dependencies": { - "ansi-html-community": "0.0.8", - "html-entities": "^2.1.0", - "strip-ansi": "^6.0.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "dev": true, - "engines": { - "node": ">=10.13.0" - } + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" }, "node_modules/webpack-virtual-modules": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz", - "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==", - "dev": true - }, - "node_modules/webpack/node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true - }, - "node_modules/webpack/node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/webpack/node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", - "dev": true, - "peerDependencies": { - "acorn": "^8" - } + "license": "MIT" }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -24815,6 +14087,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -24826,39 +14099,45 @@ } }, "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.0.tgz", + "integrity": "sha512-Ei7Miu/AXe2JJ4iNF5j/UphAgRoma4trE6PtisM09bPygb3egMH3YLW/befsWb1A1AxvNSFidOFTB18XtnIIng==", "dev": true, + "license": "MIT", "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.0", + "is-number-object": "^1.1.0", + "is-string": "^1.1.0", + "is-symbol": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/which-builtin-type": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", - "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.0.tgz", + "integrity": "sha512-I+qLGQ/vucCby4tf5HsLmGueEla4ZhwTBSqaooS+Y0BuxN4Cp+okmGuV+8mXZ84KDI9BA+oklo+RzKg0ONdSUA==", "dev": true, + "license": "MIT", "dependencies": { - "function.prototype.name": "^1.1.5", - "has-tostringtag": "^1.0.0", + "call-bind": "^1.0.7", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.0.5", - "is-finalizationregistry": "^1.0.2", + "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.1.4", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.15" }, "engines": { "node": ">= 0.4" @@ -24868,31 +14147,16 @@ } }, "node_modules/which-collection": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", - "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, + "license": "MIT", "dependencies": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "dev": true, - "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" + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" }, "engines": { "node": ">= 0.4" @@ -24901,17 +14165,48 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/which-typed-array": { + "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", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -24930,6 +14225,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -24942,11 +14238,34 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -24959,6 +14278,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -24966,28 +14286,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -25001,72 +14305,168 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, - "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 - } - } + "license": "ISC" }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.4" } }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" }, "node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", + "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, "engines": { "node": ">= 14" } }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yjs": { + "version": "13.6.24", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.24.tgz", + "integrity": "sha512-xn/pYLTZa3uD1uDG8lpxfLRo5SR/rp0frdASOl2a71aYNvUXdWcLtVL91s2y7j+Q8ppmjZ9H3jsGVgoFMbT2VA==", + "license": "MIT", + "peer": true, + "dependencies": { + "lib0": "^0.2.99" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" } }, "node_modules/yocto-queue": { @@ -25074,6 +14474,7 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -25081,45 +14482,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yup": { - "version": "0.32.11", - "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz", - "integrity": "sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==", - "dependencies": { - "@babel/runtime": "^7.15.4", - "@types/lodash": "^4.14.175", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "nanoclone": "^0.2.1", - "property-expr": "^2.0.4", - "toposort": "^2.0.2" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/zustand": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.0.tgz", - "integrity": "sha512-zlVFqS5TQ21nwijjhJlx4f9iGrXSL0o/+Dpy4txAP22miJ8Ti6c1Ol1RLNN98BMib83lmDH/2KmLwaNXpjrO1A==", - "dependencies": { - "use-sync-external-store": "1.2.0" - }, + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.2.tgz", + "integrity": "sha512-8qNdnJVJlHlrKXi50LDqqUNmUbuBjoKLrYQBnoChIbVph7vni+sY+YpvdjXG9YLd/Bxr6scMcR+rm5H3aSqPaw==", + "license": "MIT", "engines": { - "node": ">=12.7.0" + "node": ">=12.20.0" }, "peerDependencies": { - "@types/react": ">=16.8", + "@types/react": ">=18.0.0", "immer": ">=9.0.6", - "react": ">=16.8" + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" }, "peerDependenciesMeta": { "@types/react": { @@ -25130,8 +14514,20 @@ }, "react": { "optional": true + }, + "use-sync-external-store": { + "optional": true } } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 9558e99e9..6225b78f0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,163 +1,142 @@ { + "name": "frontend-v2", "private": true, + "version": "0.0.0", + "type": "module", "scripts": { - "prepare": "cd .. && npm install", - "dev": "next dev", - "build": "next build", - "start": "next start", - "start:docker": "next build && next start", - "lint": "eslint --ext js,ts,tsx ./src", - "lint:fix": "eslint --fix --ext js,ts,tsx ./src", - "type:check": "tsc --project tsconfig.json --noEmit", - "storybook": "storybook dev -p 6006 -s ./public", - "build-storybook": "storybook build" - }, - "overrides": { - "@storybook/nextjs": { - "sharp": "npm:dry-uninstall" - } + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "lint": "eslint ./src", + "lint:fix": "eslint --fix ./src", + "type:check": "tsc --noEmit --project ./tsconfig.app.json" }, "dependencies": { - "@casl/ability": "^6.5.0", - "@casl/react": "^3.1.0", - "@dnd-kit/core": "^6.0.8", - "@dnd-kit/modifiers": "^6.0.1", - "@dnd-kit/sortable": "^7.0.2", - "@emotion/css": "^11.10.0", - "@emotion/server": "^11.10.0", - "@fontsource/inter": "^4.5.15", - "@fortawesome/fontawesome-svg-core": "^6.1.2", - "@fortawesome/free-brands-svg-icons": "^6.1.2", - "@fortawesome/free-regular-svg-icons": "^6.1.1", - "@fortawesome/free-solid-svg-icons": "^6.1.2", - "@fortawesome/react-fontawesome": "^0.2.0", - "@hcaptcha/react-hcaptcha": "^1.10.1", - "@headlessui/react": "^1.7.7", - "@hookform/resolvers": "^2.9.10", - "@octokit/rest": "^19.0.7", - "@peculiar/x509": "^1.11.0", - "@radix-ui/react-accordion": "^1.1.2", - "@radix-ui/react-alert-dialog": "^1.0.5", - "@radix-ui/react-checkbox": "^1.0.4", - "@radix-ui/react-collapsible": "^1.0.3", - "@radix-ui/react-dialog": "^1.0.5", - "@radix-ui/react-dropdown-menu": "^2.0.6", - "@radix-ui/react-hover-card": "^1.0.7", - "@radix-ui/react-label": "^2.0.2", - "@radix-ui/react-popover": "^1.0.7", - "@radix-ui/react-popper": "^1.1.3", - "@radix-ui/react-progress": "^1.0.3", - "@radix-ui/react-radio-group": "^1.1.3", - "@radix-ui/react-select": "^2.0.0", - "@radix-ui/react-switch": "^1.0.3", - "@radix-ui/react-tabs": "^1.0.4", - "@radix-ui/react-toast": "^1.1.5", - "@radix-ui/react-tooltip": "^1.0.7", - "@reduxjs/toolkit": "^1.8.3", - "@sindresorhus/slugify": "1.1.0", - "@stripe/react-stripe-js": "^1.16.3", - "@stripe/stripe-js": "^1.46.0", - "@tanstack/react-query": "^4.23.0", - "@types/argon2-browser": "^1.18.1", + "@casl/ability": "^6.7.2", + "@casl/react": "^4.0.0", + "@dagrejs/dagre": "^1.1.4", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", + "@fontsource/inter": "^5.1.0", + "@fortawesome/fontawesome-svg-core": "^6.7.1", + "@fortawesome/free-brands-svg-icons": "^6.7.1", + "@fortawesome/free-regular-svg-icons": "^6.7.1", + "@fortawesome/free-solid-svg-icons": "^6.7.1", + "@fortawesome/react-fontawesome": "^0.2.2", + "@hcaptcha/react-hcaptcha": "^1.11.0", + "@headlessui/react": "^1.7.19", + "@hookform/resolvers": "^3.9.1", + "@lexical/react": "^0.29.0", + "@lottiefiles/dotlottie-react": "^0.12.0", + "@octokit/rest": "^21.0.2", + "@peculiar/x509": "^1.12.3", + "@radix-ui/react-accordion": "^1.2.2", + "@radix-ui/react-alert-dialog": "^1.1.3", + "@radix-ui/react-checkbox": "^1.1.3", + "@radix-ui/react-collapsible": "^1.1.2", + "@radix-ui/react-dialog": "^1.1.3", + "@radix-ui/react-dropdown-menu": "^2.1.3", + "@radix-ui/react-hover-card": "^1.1.3", + "@radix-ui/react-label": "^2.1.1", + "@radix-ui/react-popover": "^1.1.3", + "@radix-ui/react-popper": "^1.2.1", + "@radix-ui/react-progress": "^1.1.1", + "@radix-ui/react-radio-group": "^1.2.2", + "@radix-ui/react-select": "^2.1.3", + "@radix-ui/react-switch": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.2", + "@radix-ui/react-toast": "^1.2.3", + "@radix-ui/react-tooltip": "^1.1.5", + "@sindresorhus/slugify": "^2.2.1", + "@tanstack/react-query": "^5.62.7", + "@tanstack/react-router": "^1.95.1", + "@tanstack/virtual-file-routes": "^1.87.6", + "@tanstack/zod-adapter": "^1.91.0", + "@types/dagre": "^0.7.52", + "@types/nprogress": "^0.2.3", "@ucast/mongo2js": "^1.3.4", - "add": "^2.0.6", + "@xyflow/react": "^12.4.4", "argon2-browser": "^1.18.0", - "axios": "^0.28.0", - "axios-auth-refresh": "^3.3.6", - "base64-loader": "^1.0.0", - "classnames": "^2.3.1", - "cookies": "^0.9.1", - "cva": "npm:class-variance-authority@^0.4.0", - "date-fns": "^2.30.0", + "axios": "^1.7.9", + "classnames": "^2.5.1", + "cva": "npm:class-variance-authority@^0.7.1", + "date-fns": "^4.1.0", + "dompurify": "^3.2.4", "file-saver": "^2.0.5", - "framer-motion": "^6.2.3", - "fs": "^0.0.2", - "gray-matter": "^4.0.3", - "http-proxy": "^1.18.1", - "i18next": "^22.4.15", - "i18next-browser-languagedetector": "^7.0.1", - "i18next-http-backend": "^2.2.0", - "infisical-node": "^1.0.37", + "framer-motion": "^11.14.1", + "i18next": "^24.1.0", + "i18next-browser-languagedetector": "^8.0.2", + "i18next-http-backend": "^3.0.1", "jspdf": "^2.5.2", "jsrp": "^0.2.4", - "jwt-decode": "^3.1.2", - "lottie-react": "^2.4.0", - "markdown-it": "^13.0.1", + "jwt-decode": "^4.0.0", + "lexical": "^0.29.0", "ms": "^2.1.3", - "next": "^12.3.4", "nprogress": "^0.2.0", - "picomatch": "^2.3.1", - "posthog-js": "^1.105.6", - "query-string": "^7.1.3", - "react": "^17.0.2", - "react-beautiful-dnd": "^13.1.1", + "picomatch": "^4.0.2", + "posthog-js": "^1.198.0", + "qrcode": "^1.5.4", + "react": "^18.3.1", "react-code-input": "^3.10.1", - "react-day-picker": "^8.8.0", - "react-dom": "^17.0.2", - "react-grid-layout": "^1.3.4", - "react-hook-form": "^7.43.0", - "react-i18next": "^12.2.2", - "react-icons": "^5.3.0", - "react-mailchimp-subscribe": "^2.1.3", - "react-markdown": "^8.0.3", - "react-redux": "^8.0.2", - "react-select": "^5.8.1", - "react-table": "^7.8.0", - "react-toastify": "^9.1.3", - "sanitize-html": "^2.12.1", - "set-cookie-parser": "^2.5.1", - "sharp": "^0.33.2", - "styled-components": "^5.3.7", - "tailwind-merge": "^1.8.1", + "react-day-picker": "^9.4.3", + "react-dom": "^18.3.1", + "react-helmet": "^6.1.0", + "react-hook-form": "^7.54.0", + "react-i18next": "^15.2.0", + "react-icons": "^5.4.0", + "react-markdown": "^10.0.1", + "react-select": "^5.9.0", + "react-toastify": "^10.0.6", + "redaxios": "^0.5.1", + "rehype-raw": "^7.0.0", + "tailwind-merge": "^2.5.5", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", - "uuid": "^8.3.2", - "uuidv4": "^6.2.13", - "yaml": "^2.2.2", - "yup": "^0.32.11", - "zod": "^3.22.3", - "zustand": "^4.5.0" + "yaml": "^2.6.1", + "zod": "^3.24.1", + "zustand": "^5.0.2" }, "devDependencies": { - "@storybook/addon-essentials": "^7.5.2", - "@storybook/addon-interactions": "^7.0.23", - "@storybook/addon-links": "^7.0.23", - "@storybook/addon-styling": "^1.3.0", - "@storybook/blocks": "^7.0.23", - "@storybook/client-api": "^7.2.1", - "@storybook/nextjs": "^7.0.23", - "@storybook/react": "^7.0.23", - "@storybook/testing-library": "^0.2.0", - "@tailwindcss/typography": "^0.5.4", - "@types/file-saver": "^2.0.5", - "@types/jsrp": "^0.2.4", - "@types/node": "^18.11.9", - "@types/picomatch": "^2.3.0", - "@types/react": "^18.0.26", - "@types/sanitize-html": "^2.9.0", - "@typescript-eslint/eslint-plugin": "^5.48.1", - "@typescript-eslint/parser": "^5.45.0", - "autoprefixer": "^10.4.7", - "cypress": "^13.3.2", - "eslint": "^8.32.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.15.0", + "@kesills/eslint-config-airbnb-typescript": "^20.0.0", + "@stylistic/eslint-plugin": "^2.12.1", + "@tailwindcss/typography": "^0.5.15", + "@tanstack/eslint-plugin-router": "^1.87.6", + "@tanstack/router-devtools": "^1.87.9", + "@tanstack/router-plugin": "^1.95.1", + "@types/argon2-browser": "^1.18.4", + "@types/file-saver": "^2.0.7", + "@types/jsrp": "^0.2.6", + "@types/ms": "^0.7.34", + "@types/picomatch": "^3.0.1", + "@types/qrcode": "^1.5.5", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@types/react-helmet": "^6.1.11", + "@vitejs/plugin-react-swc": "^3.5.0", + "autoprefixer": "^10.4.20", + "eslint": "^8.57.1", "eslint-config-airbnb": "^19.0.4", - "eslint-config-airbnb-typescript": "^17.0.0", - "eslint-config-next": "^13.0.5", - "eslint-config-prettier": "^8.6.0", - "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.27.4", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-react": "^7.32.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-simple-import-sort": "^8.0.0", - "eslint-plugin-storybook": "^0.6.12", - "postcss": "^8.4.39", - "prettier": "^2.8.3", - "prettier-plugin-tailwindcss": "^0.2.2", - "storybook": "^7.6.20", - "storybook-dark-mode": "^3.0.0", - "tailwindcss": "3.2", - "typescript": "^4.9.3" + "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.7.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.14", + "eslint-plugin-simple-import-sort": "^12.1.1", + "globals": "^15.12.0", + "postcss": "^8.4.49", + "prettier": "3.4.2", + "prettier-plugin-tailwindcss": "^0.6.9", + "tailwindcss": "^3.4.16", + "typescript": "~5.6.2", + "typescript-eslint": "^8.15.0", + "vite": "^5.4.18", + "vite-plugin-node-polyfills": "^0.22.0", + "vite-plugin-top-level-await": "^1.4.4", + "vite-plugin-wasm": "^3.3.0", + "vite-tsconfig-paths": "^5.1.4" } } diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index 12a703d90..2e7af2b7f 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -1,6 +1,6 @@ -module.exports = { +export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, -}; +} diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts deleted file mode 100644 index f430cd8f4..000000000 --- a/frontend/public/data/frequentConstants.ts +++ /dev/null @@ -1,93 +0,0 @@ -interface Mapping { - [key: string]: string; -} - -const integrationSlugNameMapping: Mapping = { - "azure-key-vault": "Azure Key Vault", - "aws-parameter-store": "AWS Parameter Store", - "aws-secret-manager": "AWS Secrets Manager", - heroku: "Heroku", - vercel: "Vercel", - netlify: "Netlify", - github: "GitHub", - gitlab: "GitLab", - render: "Render", - "laravel-forge": "Laravel Forge", - railway: "Railway", - flyio: "Fly.io", - circleci: "CircleCI", - databricks: "Databricks", - travisci: "TravisCI", - supabase: "Supabase", - checkly: "Checkly", - qovery: "Qovery", - "terraform-cloud": "Terraform Cloud", - teamcity: "TeamCity", - "hashicorp-vault": "Vault", - "cloudflare-pages": "Cloudflare Pages", - "cloudflare-workers": "Cloudflare Workers", - codefresh: "Codefresh", - "digital-ocean-app-platform": "Digital Ocean App Platform", - bitbucket: "BitBucket", - "cloud-66": "Cloud 66", - northflank: "Northflank", - windmill: "Windmill", - "gcp-secret-manager": "GCP Secret Manager", - "hasura-cloud": "Hasura Cloud", - rundeck: "Rundeck", - "azure-devops": "Azure DevOps", - "azure-app-configuration": "Azure App Configuration" -}; - -const envMapping: Mapping = { - Development: "dev", - Staging: "staging", - Production: "prod", - Testing: "test" -}; - -const reverseEnvMapping: Mapping = { - dev: "Development", - staging: "Staging", - prod: "Production", - test: "Testing" -}; - -const contextNetlifyMapping: Mapping = { - dev: "Local development", - "branch-deploy": "Branch deploys", - "deploy-preview": "Deploy Previews", - production: "Production" -}; - -const reverseContextNetlifyMapping: Mapping = { - "Local development": "dev", - "Branch deploys": "branch-deploy", - "Deploy Previews": "deploy-preview", - Production: "production" -}; - -const plansDev: Mapping = { - starter: "prod_Mb4ATFT5QAHoPM", - team: "prod_NEpD2WMXUS2eDn", - professional: "prod_Mb4CetZ2jE7jdl", - enterprise: "licence_key_required" -}; - -const plansProd: Mapping = { - starter: "prod_Mb8oR5XNwyFTul", - team: "prod_NEp7fAB3UJWK6A", - professional: "prod_Mb8pUIpA0OUi5N", - enterprise: "licence_key_required" -}; - -const plans = plansProd || plansDev; - -export { - contextNetlifyMapping, - envMapping, - integrationSlugNameMapping, - plans, - reverseContextNetlifyMapping, - reverseEnvMapping -}; diff --git a/frontend/public/data/frequentInterfaces.ts b/frontend/public/data/frequentInterfaces.ts deleted file mode 100644 index eb40782da..000000000 --- a/frontend/public/data/frequentInterfaces.ts +++ /dev/null @@ -1,19 +0,0 @@ -export interface Tag { - id: string; - name: string; - slug: string; - user: string; - workspace: string; - createdAt: string; -} - -export interface SecretDataProps { - pos: number; - key: string; - value: string | undefined; - valueOverride: string | undefined; - id: string; - idOverride?: string; - comment: string; - tags: Tag[]; -} diff --git a/frontend/public/images/integrations/Amazon Web Services.png b/frontend/public/images/integrations/Amazon Web Services.png index 65b4a6ee8..d4025224e 100644 Binary files a/frontend/public/images/integrations/Amazon Web Services.png and b/frontend/public/images/integrations/Amazon Web Services.png differ diff --git a/frontend/public/images/integrations/Auth0.png b/frontend/public/images/integrations/Auth0.png new file mode 100644 index 000000000..e86d76c06 Binary files /dev/null and b/frontend/public/images/integrations/Auth0.png differ diff --git a/frontend/public/images/integrations/Camunda.png b/frontend/public/images/integrations/Camunda.png new file mode 100644 index 000000000..a3bb215b3 Binary files /dev/null and b/frontend/public/images/integrations/Camunda.png differ diff --git a/frontend/public/images/integrations/Circle CI.png b/frontend/public/images/integrations/CircleCI.png similarity index 100% rename from frontend/public/images/integrations/Circle CI.png rename to frontend/public/images/integrations/CircleCI.png diff --git a/frontend/public/images/integrations/GitHub.png b/frontend/public/images/integrations/GitHub.png index 9490ffc6d..7492fcb54 100644 Binary files a/frontend/public/images/integrations/GitHub.png and b/frontend/public/images/integrations/GitHub.png differ diff --git a/frontend/public/images/integrations/Humanitec.png b/frontend/public/images/integrations/Humanitec.png new file mode 100644 index 000000000..7f763d359 Binary files /dev/null and b/frontend/public/images/integrations/Humanitec.png differ diff --git a/frontend/public/images/integrations/MsSql.png b/frontend/public/images/integrations/MsSql.png new file mode 100644 index 000000000..108ed60f9 Binary files /dev/null and b/frontend/public/images/integrations/MsSql.png differ diff --git a/frontend/public/images/integrations/MySql.png b/frontend/public/images/integrations/MySql.png new file mode 100644 index 000000000..d92befdbc Binary files /dev/null and b/frontend/public/images/integrations/MySql.png differ diff --git a/frontend/public/images/integrations/Octopus Deploy.png b/frontend/public/images/integrations/Octopus Deploy.png new file mode 100644 index 000000000..a7a0d2ffa Binary files /dev/null and b/frontend/public/images/integrations/Octopus Deploy.png differ diff --git a/frontend/public/images/integrations/Postgres.png b/frontend/public/images/integrations/Postgres.png new file mode 100644 index 000000000..b7152860d Binary files /dev/null and b/frontend/public/images/integrations/Postgres.png differ diff --git a/frontend/public/images/integrations/SendGrid.png b/frontend/public/images/integrations/SendGrid.png new file mode 100644 index 000000000..3d2c9a92d Binary files /dev/null and b/frontend/public/images/integrations/SendGrid.png differ diff --git a/frontend/public/images/secretRotation/secret-rotations-v2-location.png b/frontend/public/images/secretRotation/secret-rotations-v2-location.png new file mode 100644 index 000000000..6c0e7d8f1 Binary files /dev/null and b/frontend/public/images/secretRotation/secret-rotations-v2-location.png differ diff --git a/frontend/public/images/sso/Auth0.png b/frontend/public/images/sso/Auth0.png new file mode 100644 index 000000000..e86d76c06 Binary files /dev/null and b/frontend/public/images/sso/Auth0.png differ diff --git a/frontend/public/images/sso/Google.png b/frontend/public/images/sso/Google.png new file mode 100644 index 000000000..b3ed76596 Binary files /dev/null and b/frontend/public/images/sso/Google.png differ diff --git a/frontend/public/images/sso/JumpCloud.png b/frontend/public/images/sso/JumpCloud.png new file mode 100644 index 000000000..94d15f71e Binary files /dev/null and b/frontend/public/images/sso/JumpCloud.png differ diff --git a/frontend/public/images/sso/Keycloak.png b/frontend/public/images/sso/Keycloak.png new file mode 100644 index 000000000..86405f3af Binary files /dev/null and b/frontend/public/images/sso/Keycloak.png differ diff --git a/frontend/public/images/sso/Microsoft Azure.png b/frontend/public/images/sso/Microsoft Azure.png new file mode 100644 index 000000000..c9388d612 Binary files /dev/null and b/frontend/public/images/sso/Microsoft Azure.png differ diff --git a/frontend/public/images/sso/Okta.png b/frontend/public/images/sso/Okta.png new file mode 100644 index 000000000..d742d4347 Binary files /dev/null and b/frontend/public/images/sso/Okta.png differ diff --git a/frontend/public/locales/en/translations.json b/frontend/public/locales/en/translations.json index b1465f04d..8b6af1169 100644 --- a/frontend/public/locales/en/translations.json +++ b/frontend/public/locales/en/translations.json @@ -222,10 +222,10 @@ "org-members-description": "Manage members of your organization. These users could afterwards be formed into projects.", "search-members": "Search members...", "add-dialog": { - "add-member-to-project": "Add a member to your project", + "add-member-to-project": "Add users to your project", "already-all-invited": "All the users in your organization are already invited.", "add-user-org-first": "Add more users to the organization first.", - "user-will-email": "The user will receive an email with the instructions.", + "user-will-email": "Users will receive an email with instructions to gain access.", "looking-add": "<0>If you are looking to add users to your org,<1>click here", "add-user-to-org": "Add Users to Organization" } @@ -301,8 +301,8 @@ "project-id-description2": "For more guidance, including code snipets for various languages and frameworks, see ", "auto-generated": "This is your project's auto-generated unique identifier. It can't be changed.", "docs": "Infisical Docs", - "auto-capitalization": "Auto Capitalization", - "auto-capitalization-description": "According to standards, Infisical will automatically capitalize your keys. If you want to disable this feature, you can do so here." + "enforce-capitalization": "Enforce Capitalization", + "enforce-capitalization-description": "According to standards, Infisical enforces uppercase secret keys. If you want to disable this feature, you can do so here." } }, "signup": { diff --git a/frontend/public/locales/es/translations.json b/frontend/public/locales/es/translations.json index 44da9a8ce..d7ee331f4 100644 --- a/frontend/public/locales/es/translations.json +++ b/frontend/public/locales/es/translations.json @@ -289,8 +289,8 @@ "project-id-description2": "Para más guías, incluyendo ejemplos de código en diferentes lenguajes y frameworks, visita ", "auto-generated": "Este es el ID único y autogenerado de proyecto. No se puede modificar.", "docs": "Documentación de Infisical", - "auto-capitalization": "Mayúsculas automáticas", - "auto-capitalization-description": "De acuerdo con los estándares, Infisical pondrá en mayúsculas tus claves. Si quieres desactivar esta funcionalidad, lo puedes hacer aquí." + "enforce-capitalization": "Hacer cumplir la capitalización", + "enforce-capitalization-description": "Según los estándares, Infisical aplica claves secretas en mayúsculas. Si desea desactivar esta función, puede hacerlo aquí." } }, "signup": { diff --git a/frontend/public/locales/pt-BR/translations.json b/frontend/public/locales/pt-BR/translations.json index ba324849b..a491d3d74 100644 --- a/frontend/public/locales/pt-BR/translations.json +++ b/frontend/public/locales/pt-BR/translations.json @@ -266,8 +266,8 @@ "project-id-description2": "Para obter mais orientações, incluindo trechos de código para várias linguagens e frameworks, consulte ", "auto-generated": "Este é o identificador exclusivo - gerado automaticamente - do seu projeto. Não pode ser alterado.", "docs": "Documentação do Infisical", - "auto-capitalization": "Converter em caixa alta automaticamente", - "auto-capitalization-description": "Por padrão, Infisical converte automaticamente as chaves em caixa alta. Se você quiser desativar essa funcionalidade, pode fazê-lo aqui." + "enforce-capitalization": "Aplicar capitalização", + "enforce-capitalization-description": "De acordo com os padrões, o Infisical impõe chaves secretas em letras maiúsculas. Se quiser desabilitar esse recurso, você pode fazer isso aqui." } }, "signup": { diff --git a/frontend/public/lotties/certificate-authority.json b/frontend/public/lotties/certificate-authority.json new file mode 100644 index 000000000..44e1488a0 --- /dev/null +++ b/frontend/public/lotties/certificate-authority.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":0,"op":180,"w":430,"h":430,"nm":"wired-outline-1945-court","ddd":0,"assets":[{"id":"comp_1","nm":"hover-pinch","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"outline 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[215,215.377,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250.377,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[43.109,0],[-43.109,0]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[43.109,0],[2.391,0]],"c":false}]},{"t":180,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[43.109,0],[-43.109,0]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[42.99,0],[-43.164,0]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-117.51,0],[-94.411,0]],"c":false}]},{"t":180,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[42.99,0],[-43.164,0]],"c":false}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[43.109,240.179],[42.99,240.179],[26.99,204.803],[27.097,204.803]],"c":true}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[43.109,240.179],[-117.51,240.179],[-117.46,205.303],[27.097,205.303]],"c":true}]},{"t":180,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[43.109,240.179],[42.99,240.179],[26.99,204.803],[27.097,204.803]],"c":true}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[15.1,26.687],[15.1,59.803],[14.994,59.803],[14.994,26.687]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[15.1,26.187],[15.1,59.303],[-117.423,59.303],[-117.423,26.187]],"c":false}]},{"t":180,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[15.1,26.687],[15.1,59.803],[14.994,59.803],[14.994,26.687]],"c":false}]}],"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ind":4,"ty":"sh","ix":5,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-117.01,-79.306],[43.109,0],[37.648,26.687],[37.533,26.687],[42.99,0]],"c":true}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-117.01,-79.306],[43.109,0],[37.648,26.187],[-117.493,26.187],[-117.51,0]],"c":true}]},{"t":180,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-117.01,-79.306],[43.109,0],[37.648,26.687],[37.533,26.687],[42.99,0]],"c":true}]}],"ix":2},"nm":"Path 5","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-1945-court').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18.06,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[367.01,169.94],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":6,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":180,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"outline 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[215,215.377,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250.377,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[43.109,0],[-43.109,0]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-116.885,0],[-140.433,0]],"c":false}]},{"t":180,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[43.109,0],[-43.109,0]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-277.129,0],[-190.911,0]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-277.129,0],[-238.911,0]],"c":false}]},{"t":180,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-277.129,0],[-190.911,0]],"c":false}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[43.109,240.179],[-277.129,240.179],[-261.117,205.303],[27.097,205.303]],"c":true}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-115.891,239.963],[-277.129,240.179],[-261.117,205.303],[-116.403,205.105]],"c":true}]},{"t":180,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[43.109,240.179],[-277.129,240.179],[-261.117,205.303],[27.097,205.303]],"c":true}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[15.1,26.187],[15.1,59.303],[-249.113,59.303],[-249.113,26.187]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-116.907,26.187],[-116.907,59.303],[-249.113,59.303],[-249.113,26.187]],"c":false}]},{"t":180,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[15.1,26.187],[15.1,59.303],[-249.113,59.303],[-249.113,26.187]],"c":false}]}],"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ind":4,"ty":"sh","ix":5,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-117.01,-79.306],[43.109,0],[37.648,26.187],[-271.669,26.187],[-277.129,0]],"c":true}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-117.01,-79.306],[-116.885,0],[-116.889,26.187],[-271.669,26.187],[-277.129,0]],"c":true}]},{"t":180,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-117.01,-79.306],[43.109,0],[37.648,26.187],[-271.669,26.187],[-277.129,0]],"c":true}]}],"ix":2},"nm":"Path 5","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-1945-court').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18.06,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[367.01,169.94],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":6,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,-14.739],[-14.739,0],[0,14.739],[14.739,0]],"o":[[0,14.739],[14.739,0],[0,-14.739],[-14.739,0]],"v":[[-26.687,0],[0,26.687],[26.687,0],[0,-26.687]],"c":true}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[{"i":[[0,-10.29],[-10.29,0],[0,10.29],[10.29,0]],"o":[[0,10.29],[10.29,0],[0,-10.29],[-10.29,0]],"v":[[-18.631,0],[0,18.631],[18.631,0],[0,-18.631]],"c":true}]},{"t":180,"s":[{"i":[[0,-14.739],[-14.739,0],[0,14.739],[14.739,0]],"o":[[0,14.739],[14.739,0],[0,-14.739],[-14.739,0]],"v":[[-26.687,0],[0,26.687],[26.687,0],[0,-26.687]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-1945-court').layer('control').effect('secondary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18.06,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".secondary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"secondary"},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[250,147.631],"to":[-8.667,0],"ti":[0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[198,147.631],"to":[0,0],"ti":[-8.667,0]},{"t":180,"s":[250,147.631]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":1,"k":[{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":90,"s":[30]},{"t":180,"s":[0]}],"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"outline 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[314.768,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[173.268,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[314.768,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[349.768,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-32.342,12.714],[-21.492,-12.714],[21.492,-12.714],[32.342,12.714]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[32.342,-132.286],[21.492,-106.857],[-21.492,-106.857],[-32.342,-132.286]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[21.492,-12.714],[-21.492,-12.714],[-21.492,-106.857],[21.492,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-1945-court').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18.06,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"outline 12","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[314.768,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[173.268,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[314.768,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[349.768,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-32.342,12.714],[-21.492,-12.714],[21.492,-12.714],[32.342,12.714]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[32.342,-132.286],[21.492,-106.857],[-21.492,-106.857],[-32.342,-132.286]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[21.492,-12.714],[-21.492,-12.714],[-21.492,-106.857],[21.492,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"outline 4","tt":2,"tp":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[215,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[144.253,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[215,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[250,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-67.425,-132.286],[-78.275,-106.857],[-121.26,-106.857],[-132.11,-132.286]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-132.11,12.714],[-121.26,-12.714],[-78.275,-12.714],[-67.425,12.714]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-78.275,-12.714],[-121.26,-12.714],[-121.26,-106.857],[-78.275,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-1945-court').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18.06,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"mask-1","td":1,"refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[215,215,0],"ix":2,"l":2},"a":{"a":0,"k":[215,215,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":430,"h":430,"ip":0,"op":844,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"outline","tt":2,"tp":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":0.4},"o":{"x":0.333,"y":0.333},"t":0,"s":[115.239,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":0.4},"o":{"x":0.6,"y":0.6},"t":90,"s":[115.239,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[115.239,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[150.239,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-167.186,-132.286],[-178.036,-106.857],[-221.02,-106.857],[-231.87,-132.286]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-231.87,12.714],[-221.02,-12.714],[-178.036,-12.714],[-167.186,12.714]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-178.036,-12.714],[-221.02,-12.714],[-221.02,-106.857],[-178.036,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-1945-court').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18.06,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":8,"ty":0,"nm":"mask-line-1","td":1,"refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[215,215,0],"ix":2,"l":2},"a":{"a":0,"k":[215,215,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":430,"h":430,"ip":0,"op":844,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":0,"nm":"Columns-2","tt":2,"tp":8,"refId":"comp_4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[215,215,0],"ix":2,"l":2},"a":{"a":0,"k":[215,215,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":430,"h":430,"ip":0,"op":844,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":0,"nm":"mask-line-2","td":1,"refId":"comp_6","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[215,215,0],"ix":2,"l":2},"a":{"a":0,"k":[215,215,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":430,"h":430,"ip":0,"op":844,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":0,"nm":"columns-3","tt":2,"tp":10,"refId":"comp_7","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[215,215,0],"ix":2,"l":2},"a":{"a":0,"k":[215,215,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":430,"h":430,"ip":0,"op":844,"st":0,"bm":0}]},{"id":"comp_2","nm":"mask-1","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"outline 14","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[314.768,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[173.268,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[314.768,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[349.768,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-32.342,12.714],[-21.492,-12.714],[21.492,-12.714],[32.342,12.714]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[32.342,-132.286],[21.492,-106.857],[-21.492,-106.857],[-32.342,-132.286]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[21.492,-12.714],[-21.492,-12.714],[-21.492,-106.857],[21.492,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-1945-court').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"outline 13","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[215,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[144.253,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[215,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[250,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-67.425,-132.286],[-78.275,-106.857],[-121.26,-106.857],[-132.11,-132.286]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-132.11,12.714],[-121.26,-12.714],[-78.275,-12.714],[-67.425,12.714]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-78.275,-12.714],[-121.26,-12.714],[-121.26,-106.857],[-78.275,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-1945-court').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0}]},{"id":"comp_3","nm":"mask-line-1","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"outline 16","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[314.768,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[173.268,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[314.768,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[349.768,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-32.342,12.714],[-21.492,-12.714],[21.492,-12.714],[32.342,12.714]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[32.342,-132.286],[21.492,-106.857],[-21.492,-106.857],[-32.342,-132.286]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[21.492,-12.714],[-21.492,-12.714],[-21.492,-106.857],[21.492,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-1945-court').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"outline 15","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[215,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[144.253,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[215,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[250,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-67.425,-132.286],[-78.275,-106.857],[-121.26,-106.857],[-132.11,-132.286]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-132.11,12.714],[-121.26,-12.714],[-78.275,-12.714],[-67.425,12.714]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-78.275,-12.714],[-121.26,-12.714],[-121.26,-106.857],[-78.275,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-1945-court').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"outline 14","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":0.4},"o":{"x":0.333,"y":0.333},"t":0,"s":[115.239,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":0.4},"o":{"x":0.6,"y":0.6},"t":90,"s":[115.239,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[115.239,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[150.239,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-167.186,-132.286],[-178.036,-106.857],[-221.02,-106.857],[-231.87,-132.286]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-231.87,12.714],[-221.02,-12.714],[-178.036,-12.714],[-167.186,12.714]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-178.036,-12.714],[-221.02,-12.714],[-221.02,-106.857],[-178.036,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-1945-court').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0}]},{"id":"comp_4","nm":"Columns-2","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"outline 8","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[314.768,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[244.268,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[314.768,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[349.768,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-32.342,12.714],[-21.492,-12.714],[21.492,-12.714],[32.342,12.714]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[32.342,-132.286],[21.492,-106.857],[-21.492,-106.857],[-32.342,-132.286]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[21.492,-12.714],[-21.492,-12.714],[-21.492,-106.857],[21.492,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-1945-court').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18.06,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"outline 13","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[314.768,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[244.268,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[314.768,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[349.768,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-32.342,12.714],[-21.492,-12.714],[21.492,-12.714],[32.342,12.714]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[32.342,-132.286],[21.492,-106.857],[-21.492,-106.857],[-32.342,-132.286]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[21.492,-12.714],[-21.492,-12.714],[-21.492,-106.857],[21.492,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"st","c":{"a":0,"k":[0,0.032258063555,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"outline 7","tt":2,"tp":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[215,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[215.253,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[215,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[250,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-67.425,-132.286],[-78.275,-106.857],[-121.26,-106.857],[-132.11,-132.286]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-132.11,12.714],[-121.26,-12.714],[-78.275,-12.714],[-67.425,12.714]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-78.275,-12.714],[-121.26,-12.714],[-121.26,-106.857],[-78.275,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-1945-court').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18.06,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"mask-3","td":1,"refId":"comp_5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[215,215,0],"ix":2,"l":2},"a":{"a":0,"k":[215,215,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":430,"h":430,"ip":0,"op":844,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"outline 6","tt":2,"tp":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[115.239,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[186.239,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[115.239,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[150.239,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-167.186,-132.286],[-178.036,-106.857],[-221.02,-106.857],[-231.87,-132.286]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-231.87,12.714],[-221.02,-12.714],[-178.036,-12.714],[-167.186,12.714]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-178.036,-12.714],[-221.02,-12.714],[-221.02,-106.857],[-178.036,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-1945-court').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18.06,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0}]},{"id":"comp_5","nm":"mask-3","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"outline 15","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[314.768,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[244.268,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[314.768,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[349.768,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-32.342,12.714],[-21.492,-12.714],[21.492,-12.714],[32.342,12.714]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[32.342,-132.286],[21.492,-106.857],[-21.492,-106.857],[-32.342,-132.286]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[21.492,-12.714],[-21.492,-12.714],[-21.492,-106.857],[21.492,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"st","c":{"a":0,"k":[0,0.032258063555,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"outline 14","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[215,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[215.253,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[215,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[250,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-67.425,-132.286],[-78.275,-106.857],[-121.26,-106.857],[-132.11,-132.286]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-132.11,12.714],[-121.26,-12.714],[-78.275,-12.714],[-67.425,12.714]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-78.275,-12.714],[-121.26,-12.714],[-121.26,-106.857],[-78.275,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"st","c":{"a":0,"k":[0,0.032258063555,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0}]},{"id":"comp_6","nm":"mask-line-2","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"outline 19","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[314.768,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[173.268,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[314.768,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[349.768,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-32.342,12.714],[-21.492,-12.714],[21.492,-12.714],[32.342,12.714]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[32.342,-132.286],[21.492,-106.857],[-21.492,-106.857],[-32.342,-132.286]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[21.492,-12.714],[-21.492,-12.714],[-21.492,-106.857],[21.492,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-1945-court').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"outline 18","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[215,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[144.253,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[215,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[250,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-67.425,-132.286],[-78.275,-106.857],[-121.26,-106.857],[-132.11,-132.286]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-132.11,12.714],[-121.26,-12.714],[-78.275,-12.714],[-67.425,12.714]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-78.275,-12.714],[-121.26,-12.714],[-121.26,-106.857],[-78.275,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-1945-court').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"outline 17","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":0.4},"o":{"x":0.333,"y":0.333},"t":0,"s":[115.239,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":0.4},"o":{"x":0.6,"y":0.6},"t":90,"s":[115.239,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[115.239,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[150.239,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-167.186,-132.286],[-178.036,-106.857],[-221.02,-106.857],[-231.87,-132.286]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-231.87,12.714],[-221.02,-12.714],[-178.036,-12.714],[-167.186,12.714]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-178.036,-12.714],[-221.02,-12.714],[-221.02,-106.857],[-178.036,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-1945-court').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"outline 16","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[314.768,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[244.268,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[314.768,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[349.768,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-32.342,12.714],[-21.492,-12.714],[21.492,-12.714],[32.342,12.714]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[32.342,-132.286],[21.492,-106.857],[-21.492,-106.857],[-32.342,-132.286]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[21.492,-12.714],[-21.492,-12.714],[-21.492,-106.857],[21.492,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"st","c":{"a":0,"k":[0,0.032258063555,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"outline 15","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[215,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[215.253,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[215,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[250,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-67.425,-132.286],[-78.275,-106.857],[-121.26,-106.857],[-132.11,-132.286]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-132.11,12.714],[-121.26,-12.714],[-78.275,-12.714],[-67.425,12.714]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-78.275,-12.714],[-121.26,-12.714],[-121.26,-106.857],[-78.275,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"st","c":{"a":0,"k":[0,0.032258063555,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"outline 14","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[115.239,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[186.239,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[115.239,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[150.239,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-167.186,-132.286],[-178.036,-106.857],[-221.02,-106.857],[-231.87,-132.286]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-231.87,12.714],[-221.02,-12.714],[-178.036,-12.714],[-167.186,12.714]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-178.036,-12.714],[-221.02,-12.714],[-221.02,-106.857],[-178.036,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"st","c":{"a":0,"k":[0,0.032258063555,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0}]},{"id":"comp_7","nm":"columns-3","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"outline 11","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[314.768,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[313.768,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[314.768,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[349.768,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-32.342,12.714],[-21.492,-12.714],[21.492,-12.714],[32.342,12.714]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[32.342,-132.286],[21.492,-106.857],[-21.492,-106.857],[-32.342,-132.286]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[21.492,-12.714],[-21.492,-12.714],[-21.492,-106.857],[21.492,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-1945-court').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18.06,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"mask","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[314.768,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[313.768,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[314.768,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[349.768,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-32.342,12.714],[-21.492,-12.714],[21.492,-12.714],[32.342,12.714]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[32.342,-132.286],[21.492,-106.857],[-21.492,-106.857],[-32.342,-132.286]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[21.492,-12.714],[-21.492,-12.714],[-21.492,-106.857],[21.492,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"outline 10","tt":2,"tp":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[215,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[284.753,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[215,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[250,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-67.425,-132.286],[-78.275,-106.857],[-121.26,-106.857],[-132.11,-132.286]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-132.11,12.714],[-121.26,-12.714],[-78.275,-12.714],[-67.425,12.714]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-78.275,-12.714],[-121.26,-12.714],[-121.26,-106.857],[-78.275,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-1945-court').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18.06,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"mask","td":1,"refId":"comp_8","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[215,215,0],"ix":2,"l":2},"a":{"a":0,"k":[215,215,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":430,"h":430,"ip":0,"op":844,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"outline 9","tt":2,"tp":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[115.239,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[255.739,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[115.239,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[150.239,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-167.186,-132.286],[-178.036,-106.857],[-221.02,-106.857],[-231.87,-132.286]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-231.87,12.714],[-221.02,-12.714],[-178.036,-12.714],[-167.186,12.714]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-178.036,-12.714],[-221.02,-12.714],[-221.02,-106.857],[-178.036,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-1945-court').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18.06,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0}]},{"id":"comp_8","nm":"mask","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"mask 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[314.768,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[313.768,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[314.768,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[349.768,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-32.342,12.714],[-21.492,-12.714],[21.492,-12.714],[32.342,12.714]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[32.342,-132.286],[21.492,-106.857],[-21.492,-106.857],[-32.342,-132.286]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[21.492,-12.714],[-21.492,-12.714],[-21.492,-106.857],[21.492,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"outline 14","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[215,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":90,"s":[284.753,267.243,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[215,267.243,0]}],"ix":2,"l":2},"a":{"a":0,"k":[250,302.243,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-67.425,-132.286],[-78.275,-106.857],[-121.26,-106.857],[-132.11,-132.286]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-132.11,12.714],[-121.26,-12.714],[-78.275,-12.714],[-67.425,12.714]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-78.275,-12.714],[-121.26,-12.714],[-121.26,-106.857],[-78.275,-106.857]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-1945-court').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-1945-court').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[349.768,362.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"control","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"stroke","np":3,"mn":"Pseudo/@@eNFtiauHQXSOqu227cRFCQ","ix":1,"en":1,"ef":[{"ty":7,"nm":"Menu","mn":"Pseudo/@@eNFtiauHQXSOqu227cRFCQ-0001","ix":1,"v":{"a":0,"k":3,"ix":1}}]},{"ty":5,"nm":"primary","np":3,"mn":"ADBE Color Control","ix":2,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]},{"ty":5,"nm":"secondary","np":3,"mn":"ADBE Color Control","ix":3,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]}],"ip":0,"op":281,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"hover-pinch","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[215,215,0],"ix":2,"l":2},"a":{"a":0,"k":[215,215,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":430,"h":430,"ip":0,"op":190,"st":0,"bm":0}],"markers":[{"tm":0,"cm":"default:hover-pinch","dr":180}],"props":{}} \ No newline at end of file diff --git a/frontend/public/lotties/certificate.json b/frontend/public/lotties/certificate.json new file mode 100644 index 000000000..d634f9446 --- /dev/null +++ b/frontend/public/lotties/certificate.json @@ -0,0 +1 @@ +{"v":"5.8.1","fr":60,"ip":0,"op":89,"w":430,"h":430,"nm":"966-privacy-policy-outline","ddd":0,"assets":[{"id":"comp_1","nm":"Content-28","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"outline 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[17]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[-15]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":38,"s":[4]},{"t":50,"s":[0]}],"ix":10},"p":{"a":0,"k":[215,214.76,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.161,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0],[0.398,-0.317],[0,0],[0,0],[0.118,0.495],[0,0],[-0.308,0.344]],"o":[[0,0],[-0.118,0.495],[0,0],[0,0],[-0.398,-0.317],[0,0],[0,0],[0.308,0.344]],"v":[[2,-1.535],[1.71,0.268],[0.912,1.521],[0,2.236],[-0.912,1.521],[-1.71,0.268],[-2,-1.535],[0,-2.236]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":19,"s":[{"i":[[0,0],[0,0],[7.971,-6.344],[0,0],[0,0],[2.369,9.908],[0,0],[-6.168,6.878]],"o":[[0,0],[-2.369,9.908],[0,0],[0,0],[-7.971,-6.344],[0,0],[0,0],[6.169,6.878]],"v":[[40.037,-30.733],[34.222,5.361],[18.265,30.444],[0,44.76],[-18.265,30.444],[-34.222,5.361],[-40.037,-30.733],[0,-44.76]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":32,"s":[{"i":[[0,0],[0,0],[6.19,-4.927],[0,0],[0,0],[1.84,7.694],[0,0],[-4.79,5.341]],"o":[[0,0],[-1.84,7.694],[0,0],[0,0],[-6.19,-4.927],[0,0],[0,0],[4.79,5.341]],"v":[[31.092,-23.867],[26.577,4.163],[14.185,23.642],[0,34.76],[-14.185,23.642],[-26.577,4.163],[-31.092,-23.867],[0,-34.76]],"c":true}]},{"t":44,"s":[{"i":[[0,0],[0,0],[7.128,-5.674],[0,0],[0,0],[2.119,8.861],[0,0],[-5.516,6.151]],"o":[[0,0],[-2.119,8.861],[0,0],[0,0],[-7.128,-5.674],[0,0],[0,0],[5.517,6.151]],"v":[[35.806,-27.485],[30.606,4.795],[16.335,27.226],[0,40.03],[-16.335,27.226],[-30.606,4.795],[-35.806,-27.485],[0,-40.03]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('966-privacy-policy-outline').layer('control').effect('secondary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('966-privacy-policy-outline').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".secondary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"secondary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"outline","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[215,321.746,0],"ix":2,"l":2},"a":{"a":0,"k":[135,291.746,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-40.03,0],[40.03,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":13,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('966-privacy-policy-outline').layer('control').effect('secondary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('966-privacy-policy-outline').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".secondary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"secondary"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('966-privacy-policy-outline').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[94.97,318.433],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-26.687,0],[26.687,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.21],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":17,"s":[0]},{"t":46,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('966-privacy-policy-outline').layer('control').effect('secondary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('966-privacy-policy-outline').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".secondary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"secondary"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('966-privacy-policy-outline').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[188.373,318.433],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":4,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-80.06,0],[80.06,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.21],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":36,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('966-privacy-policy-outline').layer('control').effect('secondary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('966-privacy-policy-outline').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".secondary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"secondary"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('966-privacy-policy-outline').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[135,265.06],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":4,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"bm":0}]},{"id":"comp_3","nm":"hover-swipe","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Page-corner","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.001,249.76,0],"ix":2,"l":2},"a":{"a":0,"k":[250.001,249.76,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.22,"y":1},"o":{"x":0.333,"y":0},"t":42,"s":[{"i":[[0,0],[-49.694,-50.431],[0,0]],"o":[[0,0],[50.313,51.06],[0,0]],"v":[[-53.373,-53.373],[-0.373,-0.627],[53.373,53.373]],"c":false}]},{"t":89,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-53.373,-53.373],[-53.373,53.373],[53.373,53.373]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('966-privacy-policy-outline').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('966-privacy-policy-outline').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[330.06,116.567],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Page","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.243],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.326],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[9]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":47,"s":[-7]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":70,"s":[5]},{"t":89,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.243,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[317.001,368.76,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.326,"y":1},"o":{"x":0.333,"y":0},"t":20,"s":[351.001,381.76,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":42,"s":[291.751,356.51,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":65,"s":[321.001,369.26,0],"to":[0,0,0],"ti":[0,0,0]},{"t":80,"s":[317.001,368.76,0]}],"ix":2,"l":2},"a":{"a":0,"k":[352.001,403.76,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-53.373,-53.373],[-53.373,53.373],[53.373,53.373]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":20,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-53.373,-53.373],[-53.373,53.373],[53.373,53.373]],"c":false}]},{"t":38,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-213.237,-53.373],[-213.237,319.57],[53.373,319.57]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('966-privacy-policy-outline').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('966-privacy-policy-outline').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[330.06,116.567],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"t":20,"s":[100],"h":1},{"t":38,"s":[0],"h":1}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[26.69,-186.57],[-133.43,-186.57],[-133.43,186.57],[133.43,186.57],[133.43,-79.82]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('966-privacy-policy-outline').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('966-privacy-policy-outline').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[250,249.76],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"mask","parent":2,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.001,249.76,0],"ix":2,"l":2},"a":{"a":0,"k":[250.001,249.76,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":20,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[26.69,-186.57],[26.75,-186.57],[26.75,-79.76],[133.43,-79.76],[133.43,-79.82]],"c":true}]},{"t":38,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[26.69,-186.57],[-133.43,-186.57],[-133.43,186.57],[133.43,186.57],[133.43,-79.82]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250,249.76],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":51,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"Content-28","parent":2,"tt":2,"refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[215,215,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":430,"h":430,"ip":0,"op":51,"st":-50,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"Content-28","parent":2,"refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[215,215,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":430,"h":430,"ip":39,"op":883,"st":39,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"control","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"stroke","np":3,"mn":"Pseudo/@@C7/bkxIlQrGojTEoYN8oxw","ix":1,"en":1,"ef":[{"ty":7,"nm":"Menu","mn":"Pseudo/@@C7/bkxIlQrGojTEoYN8oxw-0001","ix":1,"v":{"a":0,"k":3,"ix":1}}]},{"ty":5,"nm":"primary","np":3,"mn":"ADBE Color Control","ix":2,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]},{"ty":5,"nm":"secondary","np":3,"mn":"ADBE Color Control","ix":3,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]}],"ip":0,"op":375,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"hover-swipe","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[215,215,0],"ix":2,"l":2},"a":{"a":0,"k":[215,215,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":430,"h":430,"ip":0,"op":99,"st":0,"bm":0}],"markers":[{"tm":0,"cm":"default:hover-swipe","dr":89}]} \ No newline at end of file diff --git a/frontend/public/lotties/circular-check.json b/frontend/public/lotties/circular-check.json new file mode 100644 index 000000000..caaed3f5d --- /dev/null +++ b/frontend/public/lotties/circular-check.json @@ -0,0 +1 @@ +{"v":"5.7.5","fr":100,"ip":0,"op":152,"w":500,"h":500,"nm":"Comp 1","ddd":0,"metadata":{},"assets":[{"id":"0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0},{"ddd":0,"refId":"1","w":376,"h":376,"ind":2,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[62,62],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":150,"s":[100],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0,"parent":1},{"ddd":0,"ind":3,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0},{"ddd":0,"refId":"2","w":376,"h":376,"ind":4,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[62,62],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"h":1},{"t":2,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0,"parent":3},{"ddd":0,"ind":5,"ty":3,"nm":".primary.design (Group)","sr":1,"ks":{"p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":1,"k":[{"t":2,"s":[0],"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]}},{"t":100,"s":[360],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0,"parent":5},{"ddd":0,"refId":"3","w":489,"h":440,"ind":7,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[-263,-288],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":150,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0,"parent":6},{"ddd":0,"ind":8,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0},{"ddd":0,"refId":"4","w":387,"h":387,"ind":9,"ty":0,"nm":".primary.design (Masked)","sr":1,"ks":{"p":{"a":0,"k":[57,57],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0,"parent":8},{"ddd":0,"ind":10,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0},{"ddd":0,"refId":"5","w":504,"h":503,"ind":11,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[-2,-1],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":150,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0,"parent":10}]},{"id":"1","layers":[{"ddd":0,"ind":12,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-62,-62],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,15.651],[-1.54,15.571],[-3.061,15.341],[-4.54,14.971],[-5.99,14.451],[-7.37,13.801],[-8.69,13.011],[-9.92,12.101],[-11.07,11.071],[-12.1,9.921],[-13.01,8.691],[-13.8,7.371],[-14.45,5.981],[-14.97,4.541],[-15.35,3.061],[-15.57,1.541],[-15.65,0.001],[-15.57,-1.539],[-15.35,-3.059],[-14.97,-4.549],[-14.45,-5.989],[-13.8,-7.368],[-13.01,-8.689],[-12.1,-9.919],[-11.07,-11.069],[-9.92,-12.099],[-8.69,-13.009],[-7.37,-13.799],[-5.99,-14.449],[-4.55,-14.969],[-3.061,-15.349],[-1.54,-15.569],[1.54,-15.569],[3.06,-15.349],[4.54,-14.969],[5.979,-14.449],[7.37,-13.799],[8.689,-13.009],[9.92,-12.099],[11.06,-11.069],[12.1,-9.919],[13.01,-8.689],[13.8,-7.368],[14.45,-5.989],[14.97,-4.549],[15.34,-3.059],[15.57,-1.539],[15.65,0.001],[15.57,1.541],[15.34,3.061],[14.97,4.541],[14.45,5.981],[13.8,7.371],[13.01,8.691],[12.1,9.921],[11.06,11.071],[9.92,12.101],[8.689,13.011],[7.37,13.801],[5.979,14.451],[4.54,14.971],[3.06,15.341],[1.54,15.571],[0,15.651]],"i":[[0,0],[0.51,0.05],[0.5,0.1],[0.479,0.15],[0.47,0.2],[0.45,0.24],[0.42,0.28],[0.39,0.32],[0.36,0.36],[0.319,0.4],[0.279,0.42],[0.239,0.45],[0.19,0.47],[0.149,0.49],[0.1,0.5],[0.05,0.5],[0,0.51],[-0.06,0.51],[-0.101,0.5],[-0.15,0.49],[-0.2,0.47],[-0.24,0.45],[-0.28,0.42],[-0.32,0.39],[-0.359,0.36],[-0.4,0.32],[-0.42,0.28],[-0.45,0.24],[-0.47,0.19],[-0.48,0.15],[-0.5,0.1],[-0.51,0.05],[-1.021,-0.11],[-0.5,-0.1],[-0.48,-0.15],[-0.46,-0.2],[-0.45,-0.24],[-0.43,-0.29],[-0.39,-0.32],[-0.359,-0.36],[-0.33,-0.4],[-0.28,-0.43],[-0.24,-0.45],[-0.2,-0.47],[-0.149,-0.48],[-0.1,-0.5],[-0.051,-0.51],[0,-0.51],[0.05,-0.51],[0.1,-0.5],[0.141,-0.48],[0.189,-0.47],[0.23,-0.45],[0.28,-0.43],[0.32,-0.39],[0.37,-0.37],[0.4,-0.33],[0.421,-0.28],[0.45,-0.24],[0.48,-0.19],[0.49,-0.15],[0.5,-0.1],[0.51,-0.05],[0.51,0]],"o":[[-0.51,0],[-0.51,-0.05],[-0.5,-0.1],[-0.49,-0.15],[-0.47,-0.19],[-0.45,-0.24],[-0.42,-0.28],[-0.4,-0.33],[-0.359,-0.37],[-0.32,-0.39],[-0.28,-0.43],[-0.24,-0.45],[-0.2,-0.47],[-0.15,-0.48],[-0.101,-0.5],[-0.06,-0.51],[0,-0.51],[0.05,-0.51],[0.1,-0.5],[0.149,-0.48],[0.19,-0.47],[0.239,-0.45],[0.279,-0.43],[0.319,-0.4],[0.36,-0.36],[0.39,-0.32],[0.42,-0.29],[0.45,-0.24],[0.47,-0.2],[0.489,-0.15],[0.5,-0.1],[1.02,-0.11],[0.51,0.05],[0.5,0.1],[0.49,0.15],[0.48,0.19],[0.45,0.24],[0.421,0.28],[0.4,0.32],[0.37,0.36],[0.32,0.39],[0.28,0.42],[0.23,0.45],[0.189,0.47],[0.141,0.49],[0.1,0.5],[0.05,0.51],[0,0.51],[-0.051,0.5],[-0.1,0.5],[-0.149,0.49],[-0.2,0.48],[-0.24,0.45],[-0.28,0.42],[-0.33,0.4],[-0.359,0.36],[-0.39,0.32],[-0.43,0.28],[-0.45,0.24],[-0.46,0.2],[-0.48,0.15],[-0.5,0.1],[-0.51,0.05],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[296.88,125.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Group 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,15.651],[-1.54,15.571],[-3.061,15.341],[-4.54,14.971],[-5.99,14.451],[-7.37,13.801],[-8.69,13.011],[-9.92,12.101],[-11.07,11.071],[-12.1,9.921],[-13.01,8.691],[-13.8,7.371],[-14.45,5.981],[-14.97,4.541],[-15.35,3.061],[-15.57,1.541],[-15.65,0.001],[-15.57,-1.539],[-15.35,-3.059],[-14.97,-4.539],[-14.45,-5.989],[-13.8,-7.368],[-13.01,-8.689],[-12.1,-9.919],[-11.07,-11.069],[-9.92,-12.099],[-8.69,-13.009],[-7.37,-13.799],[-5.99,-14.449],[-4.55,-14.969],[-3.061,-15.349],[-1.54,-15.569],[1.54,-15.569],[3.06,-15.349],[4.54,-14.969],[5.979,-14.449],[7.37,-13.799],[8.689,-13.009],[9.92,-12.099],[11.06,-11.069],[12.1,-9.919],[13.01,-8.689],[13.8,-7.368],[14.45,-5.989],[14.97,-4.539],[15.34,-3.059],[15.57,-1.539],[15.65,0.001],[15.57,1.541],[15.34,3.061],[14.97,4.541],[14.45,5.981],[13.8,7.371],[13.01,8.691],[12.1,9.921],[11.06,11.071],[9.92,12.101],[8.689,13.011],[7.37,13.801],[5.979,14.451],[4.54,14.971],[3.06,15.341],[1.54,15.571],[0,15.651]],"i":[[0,0],[0.51,0.05],[0.5,0.1],[0.479,0.15],[0.47,0.19],[0.45,0.24],[0.431,0.28],[0.39,0.32],[0.36,0.36],[0.319,0.4],[0.279,0.42],[0.239,0.45],[0.19,0.47],[0.149,0.49],[0.109,0.49],[0.05,0.5],[0,0.51],[-0.06,0.51],[-0.101,0.5],[-0.15,0.48],[-0.2,0.47],[-0.24,0.45],[-0.28,0.43],[-0.32,0.39],[-0.359,0.37],[-0.4,0.32],[-0.42,0.28],[-0.45,0.24],[-0.47,0.19],[-0.48,0.15],[-0.5,0.11],[-0.51,0.05],[-1.021,-0.11],[-0.5,-0.1],[-0.48,-0.15],[-0.47,-0.2],[-0.45,-0.24],[-0.43,-0.28],[-0.39,-0.32],[-0.359,-0.36],[-0.33,-0.4],[-0.28,-0.42],[-0.24,-0.45],[-0.19,-0.47],[-0.149,-0.49],[-0.1,-0.5],[-0.051,-0.51],[0,-0.51],[0.05,-0.51],[0.1,-0.5],[0.15,-0.49],[0.189,-0.47],[0.24,-0.45],[0.28,-0.43],[0.32,-0.39],[0.37,-0.37],[0.4,-0.33],[0.421,-0.28],[0.45,-0.24],[0.471,-0.19],[0.49,-0.15],[0.5,-0.1],[0.5,-0.05],[0.51,0]],"o":[[-0.51,0],[-0.51,-0.05],[-0.5,-0.1],[-0.49,-0.15],[-0.47,-0.19],[-0.45,-0.24],[-0.42,-0.28],[-0.4,-0.33],[-0.359,-0.37],[-0.32,-0.39],[-0.28,-0.43],[-0.24,-0.45],[-0.2,-0.47],[-0.15,-0.49],[-0.101,-0.5],[-0.06,-0.51],[0,-0.51],[0.05,-0.51],[0.109,-0.5],[0.149,-0.49],[0.19,-0.47],[0.239,-0.45],[0.279,-0.42],[0.319,-0.4],[0.36,-0.36],[0.39,-0.32],[0.42,-0.28],[0.45,-0.24],[0.47,-0.2],[0.489,-0.15],[0.5,-0.1],[1.02,-0.11],[0.5,0.05],[0.5,0.11],[0.49,0.15],[0.471,0.19],[0.45,0.24],[0.421,0.28],[0.4,0.32],[0.37,0.37],[0.32,0.39],[0.28,0.43],[0.24,0.45],[0.189,0.47],[0.15,0.48],[0.1,0.5],[0.05,0.51],[0,0.51],[-0.051,0.5],[-0.1,0.49],[-0.149,0.49],[-0.19,0.47],[-0.24,0.45],[-0.28,0.42],[-0.33,0.4],[-0.359,0.36],[-0.39,0.32],[-0.43,0.28],[-0.45,0.24],[-0.47,0.19],[-0.48,0.15],[-0.5,0.1],[-0.51,0.05],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[359.38,125.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Group 3","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[171.876,15.65],[-171.876,15.65],[-187.526,0],[-171.876,-15.65],[171.876,-15.65],[187.526,0],[171.876,15.65]],"i":[[0,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-8.643],[8.644,0]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[8.644,0],[0,8.643],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[250.004,171.878],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.004,171.878],"ix":2},"a":{"a":0,"k":[250.004,171.878],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-23.984,61.442],[-35.043,56.866],[-78.768,13.195],[-78.782,-8.938],[-56.649,-8.951],[-23.984,23.673],[56.649,-56.865],[78.781,-56.852],[78.768,-34.72],[-12.924,56.865],[-23.984,61.442]],"i":[[0,0],[3.056,3.051],[0,0],[-6.107,6.116],[-6.116,-6.108],[0,0],[0,0],[-6.107,-6.115],[6.115,-6.108],[0,0],[4.002,0]],"o":[[-4.002,0],[0,0],[-6.115,-6.108],[6.108,-6.114],[0,0],[0,0],[6.115,-6.107],[6.108,6.115],[0,0],[-3.055,3.051],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[250.003,292.753],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","nm":"Group 2","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-151.042,-156.226],[-156.226,-151.043],[-156.226,151.042],[-151.042,156.226],[151.042,156.226],[156.226,151.042],[156.226,-151.043],[151.042,-156.226],[-151.042,-156.226]],"i":[[0,0],[0,-2.858],[0,0],[-2.858,0],[0,0],[0,2.858],[0,0],[2.858,0],[0,0]],"o":[[-2.858,0],[0,0],[0,2.858],[0,0],[2.858,0],[0,0],[0,-2.858],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[151.042,187.526],[-151.042,187.526],[-187.526,151.042],[-187.526,-151.043],[-151.042,-187.526],[151.042,-187.526],[187.526,-151.043],[187.526,151.042],[151.042,187.526]],"i":[[0,0],[0,0],[0,20.117],[0,0],[-20.117,0],[0,0],[0,-20.117],[0,0],[20.117,0]],"o":[[0,0],[-20.117,0],[0,0],[0,-20.117],[0,0],[20.117,0],[0,0],[0,20.117],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[250.004,250.003],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[250.004,250.003],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":153,"st":0,"bm":0}]},{"id":"2","layers":[{"ddd":0,"ind":13,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-62,-62],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,15.651],[-1.54,15.571],[-3.061,15.341],[-4.54,14.971],[-5.99,14.451],[-7.37,13.801],[-8.69,13.011],[-9.92,12.101],[-11.07,11.071],[-12.1,9.921],[-13.01,8.691],[-13.8,7.371],[-14.45,5.981],[-14.97,4.541],[-15.35,3.061],[-15.57,1.541],[-15.65,0.001],[-15.57,-1.539],[-15.35,-3.059],[-14.97,-4.549],[-14.45,-5.989],[-13.8,-7.368],[-13.01,-8.689],[-12.1,-9.919],[-11.07,-11.069],[-9.92,-12.099],[-8.69,-13.009],[-7.37,-13.799],[-5.99,-14.449],[-4.55,-14.969],[-3.061,-15.349],[-1.54,-15.569],[1.54,-15.569],[3.06,-15.349],[4.54,-14.969],[5.979,-14.449],[7.37,-13.799],[8.689,-13.009],[9.92,-12.099],[11.06,-11.069],[12.1,-9.919],[13.01,-8.689],[13.8,-7.368],[14.45,-5.989],[14.97,-4.549],[15.34,-3.059],[15.57,-1.539],[15.65,0.001],[15.57,1.541],[15.34,3.061],[14.97,4.541],[14.45,5.981],[13.8,7.371],[13.01,8.691],[12.1,9.921],[11.06,11.071],[9.92,12.101],[8.689,13.011],[7.37,13.801],[5.979,14.451],[4.54,14.971],[3.06,15.341],[1.54,15.571],[0,15.651]],"i":[[0,0],[0.51,0.05],[0.5,0.1],[0.479,0.15],[0.47,0.2],[0.45,0.24],[0.42,0.28],[0.39,0.32],[0.36,0.36],[0.319,0.4],[0.279,0.42],[0.239,0.45],[0.19,0.47],[0.149,0.49],[0.1,0.5],[0.05,0.5],[0,0.51],[-0.06,0.51],[-0.101,0.5],[-0.15,0.49],[-0.2,0.47],[-0.24,0.45],[-0.28,0.42],[-0.32,0.39],[-0.359,0.36],[-0.4,0.32],[-0.42,0.28],[-0.45,0.24],[-0.47,0.19],[-0.48,0.15],[-0.5,0.1],[-0.51,0.05],[-1.021,-0.11],[-0.5,-0.1],[-0.48,-0.15],[-0.46,-0.2],[-0.45,-0.24],[-0.43,-0.29],[-0.39,-0.32],[-0.359,-0.36],[-0.33,-0.4],[-0.28,-0.43],[-0.24,-0.45],[-0.2,-0.47],[-0.149,-0.48],[-0.1,-0.5],[-0.051,-0.51],[0,-0.51],[0.05,-0.51],[0.1,-0.5],[0.141,-0.48],[0.189,-0.47],[0.23,-0.45],[0.28,-0.43],[0.32,-0.39],[0.37,-0.37],[0.4,-0.33],[0.421,-0.28],[0.45,-0.24],[0.48,-0.19],[0.49,-0.15],[0.5,-0.1],[0.51,-0.05],[0.51,0]],"o":[[-0.51,0],[-0.51,-0.05],[-0.5,-0.1],[-0.49,-0.15],[-0.47,-0.19],[-0.45,-0.24],[-0.42,-0.28],[-0.4,-0.33],[-0.359,-0.37],[-0.32,-0.39],[-0.28,-0.43],[-0.24,-0.45],[-0.2,-0.47],[-0.15,-0.48],[-0.101,-0.5],[-0.06,-0.51],[0,-0.51],[0.05,-0.51],[0.1,-0.5],[0.149,-0.48],[0.19,-0.47],[0.239,-0.45],[0.279,-0.43],[0.319,-0.4],[0.36,-0.36],[0.39,-0.32],[0.42,-0.29],[0.45,-0.24],[0.47,-0.2],[0.489,-0.15],[0.5,-0.1],[1.02,-0.11],[0.51,0.05],[0.5,0.1],[0.49,0.15],[0.48,0.19],[0.45,0.24],[0.421,0.28],[0.4,0.32],[0.37,0.36],[0.32,0.39],[0.28,0.42],[0.23,0.45],[0.189,0.47],[0.141,0.49],[0.1,0.5],[0.05,0.51],[0,0.51],[-0.051,0.5],[-0.1,0.5],[-0.149,0.49],[-0.2,0.48],[-0.24,0.45],[-0.28,0.42],[-0.33,0.4],[-0.359,0.36],[-0.39,0.32],[-0.43,0.28],[-0.45,0.24],[-0.46,0.2],[-0.48,0.15],[-0.5,0.1],[-0.51,0.05],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[296.88,125.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Group 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,15.651],[-1.54,15.571],[-3.061,15.341],[-4.54,14.971],[-5.99,14.451],[-7.37,13.801],[-8.69,13.011],[-9.92,12.101],[-11.07,11.071],[-12.1,9.921],[-13.01,8.691],[-13.8,7.371],[-14.45,5.981],[-14.97,4.541],[-15.35,3.061],[-15.57,1.541],[-15.65,0.001],[-15.57,-1.539],[-15.35,-3.059],[-14.97,-4.539],[-14.45,-5.989],[-13.8,-7.368],[-13.01,-8.689],[-12.1,-9.919],[-11.07,-11.069],[-9.92,-12.099],[-8.69,-13.009],[-7.37,-13.799],[-5.99,-14.449],[-4.55,-14.969],[-3.061,-15.349],[-1.54,-15.569],[1.54,-15.569],[3.06,-15.349],[4.54,-14.969],[5.979,-14.449],[7.37,-13.799],[8.689,-13.009],[9.92,-12.099],[11.06,-11.069],[12.1,-9.919],[13.01,-8.689],[13.8,-7.368],[14.45,-5.989],[14.97,-4.539],[15.34,-3.059],[15.57,-1.539],[15.65,0.001],[15.57,1.541],[15.34,3.061],[14.97,4.541],[14.45,5.981],[13.8,7.371],[13.01,8.691],[12.1,9.921],[11.06,11.071],[9.92,12.101],[8.689,13.011],[7.37,13.801],[5.979,14.451],[4.54,14.971],[3.06,15.341],[1.54,15.571],[0,15.651]],"i":[[0,0],[0.51,0.05],[0.5,0.1],[0.479,0.15],[0.47,0.19],[0.45,0.24],[0.431,0.28],[0.39,0.32],[0.36,0.36],[0.319,0.4],[0.279,0.42],[0.239,0.45],[0.19,0.47],[0.149,0.49],[0.109,0.49],[0.05,0.5],[0,0.51],[-0.06,0.51],[-0.101,0.5],[-0.15,0.48],[-0.2,0.47],[-0.24,0.45],[-0.28,0.43],[-0.32,0.39],[-0.359,0.37],[-0.4,0.32],[-0.42,0.28],[-0.45,0.24],[-0.47,0.19],[-0.48,0.15],[-0.5,0.11],[-0.51,0.05],[-1.021,-0.11],[-0.5,-0.1],[-0.48,-0.15],[-0.47,-0.2],[-0.45,-0.24],[-0.43,-0.28],[-0.39,-0.32],[-0.359,-0.36],[-0.33,-0.4],[-0.28,-0.42],[-0.24,-0.45],[-0.19,-0.47],[-0.149,-0.49],[-0.1,-0.5],[-0.051,-0.51],[0,-0.51],[0.05,-0.51],[0.1,-0.5],[0.15,-0.49],[0.189,-0.47],[0.24,-0.45],[0.28,-0.43],[0.32,-0.39],[0.37,-0.37],[0.4,-0.33],[0.421,-0.28],[0.45,-0.24],[0.471,-0.19],[0.49,-0.15],[0.5,-0.1],[0.5,-0.05],[0.51,0]],"o":[[-0.51,0],[-0.51,-0.05],[-0.5,-0.1],[-0.49,-0.15],[-0.47,-0.19],[-0.45,-0.24],[-0.42,-0.28],[-0.4,-0.33],[-0.359,-0.37],[-0.32,-0.39],[-0.28,-0.43],[-0.24,-0.45],[-0.2,-0.47],[-0.15,-0.49],[-0.101,-0.5],[-0.06,-0.51],[0,-0.51],[0.05,-0.51],[0.109,-0.5],[0.149,-0.49],[0.19,-0.47],[0.239,-0.45],[0.279,-0.42],[0.319,-0.4],[0.36,-0.36],[0.39,-0.32],[0.42,-0.28],[0.45,-0.24],[0.47,-0.2],[0.489,-0.15],[0.5,-0.1],[1.02,-0.11],[0.5,0.05],[0.5,0.11],[0.49,0.15],[0.471,0.19],[0.45,0.24],[0.421,0.28],[0.4,0.32],[0.37,0.37],[0.32,0.39],[0.28,0.43],[0.24,0.45],[0.189,0.47],[0.15,0.48],[0.1,0.5],[0.05,0.51],[0,0.51],[-0.051,0.5],[-0.1,0.49],[-0.149,0.49],[-0.19,0.47],[-0.24,0.45],[-0.28,0.42],[-0.33,0.4],[-0.359,0.36],[-0.39,0.32],[-0.43,0.28],[-0.45,0.24],[-0.47,0.19],[-0.48,0.15],[-0.5,0.1],[-0.51,0.05],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[359.38,125.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Group 3","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[171.876,15.65],[-171.876,15.65],[-187.526,0],[-171.876,-15.65],[171.876,-15.65],[187.526,0],[171.876,15.65]],"i":[[0,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-8.643],[8.644,0]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[8.644,0],[0,8.643],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[250.004,171.878],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.004,171.878],"ix":2},"a":{"a":0,"k":[250.004,171.878],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-23.984,61.442],[-35.043,56.866],[-78.768,13.195],[-78.782,-8.938],[-56.649,-8.951],[-23.984,23.673],[56.649,-56.865],[78.781,-56.852],[78.768,-34.72],[-12.924,56.865],[-23.984,61.442]],"i":[[0,0],[3.056,3.051],[0,0],[-6.107,6.116],[-6.116,-6.108],[0,0],[0,0],[-6.107,-6.115],[6.115,-6.108],[0,0],[4.002,0]],"o":[[-4.002,0],[0,0],[-6.115,-6.108],[6.108,-6.114],[0,0],[0,0],[6.115,-6.107],[6.108,6.115],[0,0],[-3.055,3.051],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[250.003,292.753],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","nm":"Group 2","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-151.042,-156.226],[-156.226,-151.043],[-156.226,151.042],[-151.042,156.226],[151.042,156.226],[156.226,151.042],[156.226,-151.043],[151.042,-156.226],[-151.042,-156.226]],"i":[[0,0],[0,-2.858],[0,0],[-2.858,0],[0,0],[0,2.858],[0,0],[2.858,0],[0,0]],"o":[[-2.858,0],[0,0],[0,2.858],[0,0],[2.858,0],[0,0],[0,-2.858],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[151.042,187.526],[-151.042,187.526],[-187.526,151.042],[-187.526,-151.043],[-151.042,-187.526],[151.042,-187.526],[187.526,-151.043],[187.526,151.042],[151.042,187.526]],"i":[[0,0],[0,0],[0,20.117],[0,0],[-20.117,0],[0,0],[0,-20.117],[0,0],[20.117,0]],"o":[[0,0],[-20.117,0],[0,0],[0,-20.117],[0,0],[20.117,0],[0,0],[0,20.117],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[250.004,250.003],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[250.004,250.003],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":153,"st":0,"bm":0}]},{"id":"3","layers":[{"ddd":0,"ind":14,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[263,288],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[67.709,-45.792],[-23.984,45.792],[-67.709,2.122],[-118.504,-193.753],[116.997,-195.253],[67.997,-45.753]],"i":[[0,0],[0,0],[0,0],[-81.604,66.643],[-34.5,-33],[34.5,-35.5]],"o":[[0,0],[0,0],[0,0],[90,-73.5],[46.014,44.014],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":1,"k":[{"t":0,"s":[22.5],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":25,"s":[28.5],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":100,"s":[28.5],"i":{"x":[0.2],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":150,"s":[22.5],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":25,"s":[181],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":100,"s":[181],"i":{"x":[0.2],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":150,"s":[360],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[-5.7300639152526855,-52.15876770019531],"ix":2},"a":{"a":0,"k":[-5.730064037791635,-94.90776975705802],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":153,"st":0,"bm":0}]},{"id":"6","layers":[{"ddd":0,"ind":15,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[-57,-57],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0},{"ddd":0,"refId":"7","w":376,"h":133,"ind":16,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[62,55],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":150,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0,"parent":15},{"ddd":0,"ind":17,"ty":4,"nm":".primary.design (In/Out) - box","sr":1,"ks":{"p":{"a":0,"k":[-57,-57],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"rc","d":1,"s":{"a":0,"k":[387,387],"ix":2},"p":{"a":0,"k":[250.5,250.5],"ix":2},"r":{"a":0,"k":0,"ix":2}},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":0,"ix":2},"r":1,"bm":0}],"ip":0,"op":153,"st":0,"bm":0}]},{"id":"4","layers":[{"ddd":0,"refId":"8","w":387,"h":387,"ind":9,"ty":0,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[250.5,250.5],"ix":2},"a":{"a":0,"k":[250.5,250.5],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0,"td":1},{"ddd":0,"refId":"6","w":387,"h":387,"ind":9,"ty":0,"nm":".primary.design (Masked)","sr":1,"ks":{"p":{"a":0,"k":[250.5,250.5],"ix":2},"a":{"a":0,"k":[250.5,250.5],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0,"tt":1}]},{"id":"8","layers":[{"ddd":0,"ind":18,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[-57,-57],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0},{"ddd":0,"refId":"9","w":387,"h":387,"ind":19,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[57,57],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":150,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0,"parent":18}]},{"id":"7","layers":[{"ddd":0,"ind":20,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-62,-55],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,15.651],[-1.54,15.571],[-3.061,15.341],[-4.54,14.971],[-5.99,14.451],[-7.37,13.801],[-8.69,13.011],[-9.92,12.101],[-11.07,11.071],[-12.1,9.921],[-13.01,8.691],[-13.8,7.371],[-14.45,5.981],[-14.97,4.541],[-15.35,3.061],[-15.57,1.541],[-15.65,0.001],[-15.57,-1.539],[-15.35,-3.059],[-14.97,-4.549],[-14.45,-5.989],[-13.8,-7.368],[-13.01,-8.689],[-12.1,-9.919],[-11.07,-11.069],[-9.92,-12.099],[-8.69,-13.009],[-7.37,-13.799],[-5.99,-14.449],[-4.55,-14.969],[-3.061,-15.349],[-1.54,-15.569],[1.54,-15.569],[3.06,-15.349],[4.54,-14.969],[5.979,-14.449],[7.37,-13.799],[8.689,-13.009],[9.92,-12.099],[11.06,-11.069],[12.1,-9.919],[13.01,-8.689],[13.8,-7.368],[14.45,-5.989],[14.97,-4.549],[15.34,-3.059],[15.57,-1.539],[15.65,0.001],[15.57,1.541],[15.34,3.061],[14.97,4.541],[14.45,5.981],[13.8,7.371],[13.01,8.691],[12.1,9.921],[11.06,11.071],[9.92,12.101],[8.689,13.011],[7.37,13.801],[5.979,14.451],[4.54,14.971],[3.06,15.341],[1.54,15.571],[0,15.651]],"i":[[0,0],[0.51,0.05],[0.5,0.1],[0.479,0.15],[0.47,0.2],[0.45,0.24],[0.42,0.28],[0.39,0.32],[0.36,0.36],[0.319,0.4],[0.279,0.42],[0.239,0.45],[0.19,0.47],[0.149,0.49],[0.1,0.5],[0.05,0.5],[0,0.51],[-0.06,0.51],[-0.101,0.5],[-0.15,0.49],[-0.2,0.47],[-0.24,0.45],[-0.28,0.42],[-0.32,0.39],[-0.359,0.36],[-0.4,0.32],[-0.42,0.28],[-0.45,0.24],[-0.47,0.19],[-0.48,0.15],[-0.5,0.1],[-0.51,0.05],[-1.021,-0.11],[-0.5,-0.1],[-0.48,-0.15],[-0.46,-0.2],[-0.45,-0.24],[-0.43,-0.29],[-0.39,-0.32],[-0.359,-0.36],[-0.33,-0.4],[-0.28,-0.43],[-0.24,-0.45],[-0.2,-0.47],[-0.149,-0.48],[-0.1,-0.5],[-0.051,-0.51],[0,-0.51],[0.05,-0.51],[0.1,-0.5],[0.141,-0.48],[0.189,-0.47],[0.23,-0.45],[0.28,-0.43],[0.32,-0.39],[0.37,-0.37],[0.4,-0.33],[0.421,-0.28],[0.45,-0.24],[0.48,-0.19],[0.49,-0.15],[0.5,-0.1],[0.51,-0.05],[0.51,0]],"o":[[-0.51,0],[-0.51,-0.05],[-0.5,-0.1],[-0.49,-0.15],[-0.47,-0.19],[-0.45,-0.24],[-0.42,-0.28],[-0.4,-0.33],[-0.359,-0.37],[-0.32,-0.39],[-0.28,-0.43],[-0.24,-0.45],[-0.2,-0.47],[-0.15,-0.48],[-0.101,-0.5],[-0.06,-0.51],[0,-0.51],[0.05,-0.51],[0.1,-0.5],[0.149,-0.48],[0.19,-0.47],[0.239,-0.45],[0.279,-0.43],[0.319,-0.4],[0.36,-0.36],[0.39,-0.32],[0.42,-0.29],[0.45,-0.24],[0.47,-0.2],[0.489,-0.15],[0.5,-0.1],[1.02,-0.11],[0.51,0.05],[0.5,0.1],[0.49,0.15],[0.48,0.19],[0.45,0.24],[0.421,0.28],[0.4,0.32],[0.37,0.36],[0.32,0.39],[0.28,0.42],[0.23,0.45],[0.189,0.47],[0.141,0.49],[0.1,0.5],[0.05,0.51],[0,0.51],[-0.051,0.5],[-0.1,0.5],[-0.149,0.49],[-0.2,0.48],[-0.24,0.45],[-0.28,0.42],[-0.33,0.4],[-0.359,0.36],[-0.39,0.32],[-0.43,0.28],[-0.45,0.24],[-0.46,0.2],[-0.48,0.15],[-0.5,0.1],[-0.51,0.05],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[296.88,125.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Group 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,15.651],[-1.54,15.571],[-3.061,15.341],[-4.54,14.971],[-5.99,14.451],[-7.37,13.801],[-8.69,13.011],[-9.92,12.101],[-11.07,11.071],[-12.1,9.921],[-13.01,8.691],[-13.8,7.371],[-14.45,5.981],[-14.97,4.541],[-15.35,3.061],[-15.57,1.541],[-15.65,0.001],[-15.57,-1.539],[-15.35,-3.059],[-14.97,-4.539],[-14.45,-5.989],[-13.8,-7.368],[-13.01,-8.689],[-12.1,-9.919],[-11.07,-11.069],[-9.92,-12.099],[-8.69,-13.009],[-7.37,-13.799],[-5.99,-14.449],[-4.55,-14.969],[-3.061,-15.349],[-1.54,-15.569],[1.54,-15.569],[3.06,-15.349],[4.54,-14.969],[5.979,-14.449],[7.37,-13.799],[8.689,-13.009],[9.92,-12.099],[11.06,-11.069],[12.1,-9.919],[13.01,-8.689],[13.8,-7.368],[14.45,-5.989],[14.97,-4.539],[15.34,-3.059],[15.57,-1.539],[15.65,0.001],[15.57,1.541],[15.34,3.061],[14.97,4.541],[14.45,5.981],[13.8,7.371],[13.01,8.691],[12.1,9.921],[11.06,11.071],[9.92,12.101],[8.689,13.011],[7.37,13.801],[5.979,14.451],[4.54,14.971],[3.06,15.341],[1.54,15.571],[0,15.651]],"i":[[0,0],[0.51,0.05],[0.5,0.1],[0.479,0.15],[0.47,0.19],[0.45,0.24],[0.431,0.28],[0.39,0.32],[0.36,0.36],[0.319,0.4],[0.279,0.42],[0.239,0.45],[0.19,0.47],[0.149,0.49],[0.109,0.49],[0.05,0.5],[0,0.51],[-0.06,0.51],[-0.101,0.5],[-0.15,0.48],[-0.2,0.47],[-0.24,0.45],[-0.28,0.43],[-0.32,0.39],[-0.359,0.37],[-0.4,0.32],[-0.42,0.28],[-0.45,0.24],[-0.47,0.19],[-0.48,0.15],[-0.5,0.11],[-0.51,0.05],[-1.021,-0.11],[-0.5,-0.1],[-0.48,-0.15],[-0.47,-0.2],[-0.45,-0.24],[-0.43,-0.28],[-0.39,-0.32],[-0.359,-0.36],[-0.33,-0.4],[-0.28,-0.42],[-0.24,-0.45],[-0.19,-0.47],[-0.149,-0.49],[-0.1,-0.5],[-0.051,-0.51],[0,-0.51],[0.05,-0.51],[0.1,-0.5],[0.15,-0.49],[0.189,-0.47],[0.24,-0.45],[0.28,-0.43],[0.32,-0.39],[0.37,-0.37],[0.4,-0.33],[0.421,-0.28],[0.45,-0.24],[0.471,-0.19],[0.49,-0.15],[0.5,-0.1],[0.5,-0.05],[0.51,0]],"o":[[-0.51,0],[-0.51,-0.05],[-0.5,-0.1],[-0.49,-0.15],[-0.47,-0.19],[-0.45,-0.24],[-0.42,-0.28],[-0.4,-0.33],[-0.359,-0.37],[-0.32,-0.39],[-0.28,-0.43],[-0.24,-0.45],[-0.2,-0.47],[-0.15,-0.49],[-0.101,-0.5],[-0.06,-0.51],[0,-0.51],[0.05,-0.51],[0.109,-0.5],[0.149,-0.49],[0.19,-0.47],[0.239,-0.45],[0.279,-0.42],[0.319,-0.4],[0.36,-0.36],[0.39,-0.32],[0.42,-0.28],[0.45,-0.24],[0.47,-0.2],[0.489,-0.15],[0.5,-0.1],[1.02,-0.11],[0.5,0.05],[0.5,0.11],[0.49,0.15],[0.471,0.19],[0.45,0.24],[0.421,0.28],[0.4,0.32],[0.37,0.37],[0.32,0.39],[0.28,0.43],[0.24,0.45],[0.189,0.47],[0.15,0.48],[0.1,0.5],[0.05,0.51],[0,0.51],[-0.051,0.5],[-0.1,0.49],[-0.149,0.49],[-0.19,0.47],[-0.24,0.45],[-0.28,0.42],[-0.33,0.4],[-0.359,0.36],[-0.39,0.32],[-0.43,0.28],[-0.45,0.24],[-0.47,0.19],[-0.48,0.15],[-0.5,0.1],[-0.51,0.05],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[359.38,125.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Group 3","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[171.876,15.65],[-171.876,15.65],[-187.526,0],[-171.876,-15.65],[171.876,-15.65],[187.526,0],[171.876,15.65]],"i":[[0,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-8.643],[8.644,0]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[8.644,0],[0,8.643],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[250.004,171.878],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.004,171.878],"ix":2},"a":{"a":0,"k":[250.004,171.878],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":1,"k":[{"t":2,"s":[250.004,148.439],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"ti":[0,32.333],"to":[0,-32.333]},{"t":25,"s":[250.004,-45.561],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":100,"s":[250.004,-45.561],"i":{"x":[0.2],"y":[1]},"o":{"x":[0.333],"y":[0]},"ti":[0,-32.333],"to":[0,32.333]},{"t":150,"s":[250.004,148.439],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[250.004,148.439],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":153,"st":0,"bm":0}]},{"id":"9","layers":[]},{"id":"5","layers":[{"ddd":0,"ind":21,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[2,1],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":2,"s":[{"c":true,"v":[[151.042,-171.876],[-151.042,-171.876],[-171.876,-151.042],[-171.876,151.042],[-151.042,171.876],[151.042,171.876],[171.876,151.042],[171.876,-151.042],[151.042,-171.876]],"i":[[0,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.505,0]],"o":[[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.505,0],[0,0],[0,-11.506],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":25,"s":[{"c":true,"v":[[-1.004,-192.997],[-0.004,-192.997],[-192.997,-0.004],[-192.997,0.997],[-0.997,192.997],[0.996,192.997],[192.997,0.996],[192.997,1.004],[-1.004,-192.997]],"i":[[0,0],[0,0],[0,-106.591],[0,0],[-106.043,0],[0,0],[0,106.032],[0,0],[107.136,0]],"o":[[0,0],[-106.591,0],[0,0],[0,106.031],[0,0],[106.032,0],[0,0],[0,-107.148],[0,0]]}],"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]}},{"t":100,"s":[{"c":true,"v":[[-1.004,-192.997],[-0.004,-192.997],[-192.997,-0.004],[-192.997,0.997],[-0.997,192.997],[0.996,192.997],[192.997,0.996],[192.997,1.004],[-1.004,-192.997]],"i":[[0,0],[0,0],[0,-106.591],[0,0],[-106.043,0],[0,0],[0,106.032],[0,0],[107.136,0]],"o":[[0,0],[-106.591,0],[0,0],[0,106.031],[0,0],[106.032,0],[0,0],[0,-107.148],[0,0]]}],"i":{"x":[0.2],"y":[1]},"o":{"x":[0.167],"y":[0.167]}},{"t":150,"s":[{"c":true,"v":[[151.042,-171.876],[-151.042,-171.876],[-171.876,-151.042],[-171.876,151.042],[-151.042,171.876],[151.042,171.876],[171.876,151.042],[171.876,-151.042],[151.042,-171.876]],"i":[[0,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.505,0]],"o":[[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.505,0],[0,0],[0,-11.506],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":12,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":15,"s":[7],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":23,"s":[0],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]}},{"t":100,"s":[0],"i":{"x":[0.833],"y":[1]},"o":{"x":[0.299],"y":[0]}},{"t":117,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":120,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"e":{"a":1,"k":[{"t":12,"s":[100],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]}},{"t":23,"s":[66.5],"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]}},{"t":100,"s":[66.5],"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0]}},{"t":117,"s":[89.5],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":120,"s":[100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":1,"k":[{"t":100,"s":[59.99999999999999],"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]}},{"t":117,"s":[16],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":120,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":1,"k":[{"t":2,"s":[0],"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]}},{"t":100,"s":[360],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":153,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":12345679,"ty":4,"nm":"Group Layer 8","sr":1,"ks":{"p":{"a":0,"k":[365,464.38524590163934,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[51.229508196721305,51.229508196721305,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[220.741,37.184],[225.501,35.896],[228.749,32.36800000000001],[229.981,27.216000000000008],[228.749,22.12],[225.501,18.592],[220.741,17.304],[215.981,18.592],[212.677,22.12],[211.501,27.216000000000008],[212.677,32.36800000000001],[215.981,35.896],[220.741,37.184],[220.741,37.184],[220.741,37.184]],"i":[[0,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.382000000000062,0.8586999999999989],[1.79200000000003,0],[1.418999999999983,-0.8586999999999989],[0.8220000000000027,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.382000000000062,-0.8586999999999989],[0.8220000000000027,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.418999999999983,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[221.357,43.06400000000001],[214.917,41.608],[210.49300000000005,37.408],[211.221,36.232],[211.221,42.392],[205.173,42.392],[205.173,0],[211.501,0],[211.501,18.36800000000001],[210.49300000000005,16.912000000000006],[214.973,12.88],[221.357,11.424000000000007],[229.085,13.49600000000001],[234.51700000000005,19.152],[236.533,27.216000000000008],[234.51700000000005,35.28],[229.141,40.992],[221.357,43.06400000000001],[221.357,43.06400000000001],[221.357,43.06400000000001]],"i":[[0,0],[1.942000000000007,0.9706999999999937],[1.045999999999935,1.829300000000003],[-0.2426666666666506,0.3919999999999959],[0,-2.053333333333327],[2.015999999999963,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.122666666666674],[0.3360000000000127,0.4853333333333296],[-1.865999999999985,0.9707000000000079],[-2.38900000000001,0],[-2.277000000000044,-1.3813000000000102],[-1.30600000000004,-2.389300000000006],[0,-2.986699999999999],[1.343999999999937,-2.389300000000006],[2.27800000000002,-1.4187000000000012],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.351999999999975,0],[-1.903999999999996,-0.9707000000000079],[0.2426666666666506,-0.3919999999999959],[0,2.053333333333327],[-2.015999999999963,0],[0,-14.13066666666667],[2.109333333333325,0],[0,6.122666666666667],[-0.3360000000000127,-0.4853333333333296],[1.120000000000005,-1.717300000000009],[1.867000000000075,-0.9706999999999937],[2.875,0],[2.314999999999941,1.381299999999996],[1.343999999999937,2.389300000000006],[0,2.986699999999999],[-1.30600000000004,2.389300000000006],[-2.27699999999993,1.381299999999996],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[181.87,43.06400000000001],[176.438,42],[172.854,38.976],[171.566,34.384],[172.63,29.960000000000008],[176.046,26.656000000000006],[181.814,24.752],[192.342,23.016000000000005],[192.342,28],[183.046,29.624],[179.35,31.248],[178.174,34.16],[179.462,37.016000000000005],[182.878,38.08],[187.35799999999995,36.96000000000001],[190.38199999999995,33.992],[191.446,29.792],[191.446,22.008],[189.766,18.36800000000001],[185.398,16.912000000000006],[180.974,18.256],[178.23,21.616],[172.966,18.98400000000001],[175.71,15.064000000000007],[180.134,12.376],[185.566,11.424000000000007],[191.894,12.768],[196.206,16.52],[197.774,22.008],[197.774,42.392],[191.726,42.392],[191.726,36.904],[193.014,37.072],[190.27,40.264],[186.518,42.336],[181.87,43.06400000000001],[181.87,43.06400000000001],[181.87,43.06400000000001]],"i":[[0,0],[1.567999999999984,0.7092999999999989],[0.8589999999999236,1.2693000000000012],[0,1.7547],[-0.7089999999999463,1.306699999999992],[-1.530000000000086,0.8960000000000008],[-2.313999999999965,0.3733000000000004],[-3.509333333333302,0.5786666666666633],[0,-1.661333333333332],[3.098666666666645,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8579999999999472,-0.7467000000000041],[-1.381000000000085,0],[-1.268999999999892,0.7466999999999899],[-0.7089999999999463,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829999999999927,0],[1.269999999999982,-0.8960000000000008],[0.5979999999999563,-1.381299999999996],[1.754666666666708,0.8773333333333255],[-1.269000000000005,1.11999999999999],[-1.680000000000064,0.6346999999999952],[-1.903999999999996,0],[-1.828999999999951,-0.8960000000000008],[-1.008000000000038,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999963,0],[0,1.829333333333338],[-0.4293333333333749,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.717999999999961,0],[0,0],[0,0]],"o":[[-2.052999999999997,0],[-1.529999999999973,-0.7467000000000041],[-0.8580000000000609,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999573,-1.306700000000006],[1.530999999999949,-0.8960000000000008],[3.509333333333302,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666645,0.541333333333327],[-1.680000000000064,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.8959999999999582,0.7092999999999989],[1.717999999999961,0],[1.307000000000016,-0.7467000000000041],[0.7100000000000364,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.081999999999994,-0.9707000000000079],[-1.680000000000064,0],[-1.232000000000085,0.8586999999999989],[-1.754666666666708,-0.8773333333333255],[0.5599999999999454,-1.493300000000005],[1.269999999999982,-1.157300000000006],[1.717999999999961,-0.6347000000000094],[2.389999999999986,0],[1.866999999999962,0.8960000000000008],[1.045999999999935,1.568000000000012],[0,6.794666666666672],[-2.015999999999963,0],[0,-1.829333333333338],[0.4293333333333749,0.05599999999999739],[-0.70900000000006,1.2319999999999993],[-1.081999999999994,0.8960000000000008],[-1.380999999999972,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[159.072,42.392],[159.072,0],[165.4,0],[165.4,42.392],[159.072,42.392],[159.072,42.392],[159.072,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-14.13066666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,14.13066666666667],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[138.952,43.06400000000001],[130.888,40.992],[125.456,35.28],[123.496,27.16],[125.456,19.040000000000006],[130.832,13.49600000000001],[138.448,11.424000000000007],[144.552,12.600000000000009],[149.088,15.848],[151.888,20.49600000000001],[152.896,26.096],[152.84,27.608],[152.616,29.064000000000007],[128.48,29.064000000000007],[128.48,24.024],[149.032,24.024],[146.008,26.320000000000007],[145.616,21.448000000000008],[142.816,18.032],[138.448,16.744],[133.968,18.032],[130.944,21.616],[130.104,27.216000000000008],[130.944,32.592],[134.192,36.176],[139.008,37.464],[143.65599999999995,36.232],[146.736,33.040000000000006],[151.888,35.56],[149.088,39.42400000000001],[144.60799999999995,42.11200000000001],[138.952,43.06400000000001],[138.952,43.06400000000001],[138.952,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.307000000000016,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.836999999999989,0],[-1.79200000000003,-0.784000000000006],[-1.231999999999971,-1.381299999999996],[-0.6349999999999909,-1.754700000000014],[0,-1.978700000000003],[0.03699999999992087,-0.5227000000000004],[0.1119999999999663,-0.4480000000000075],[8.04533333333336,0],[0,1.680000000000007],[-6.850666666666712,0],[1.008000000000038,-0.7653333333333308],[0.6349999999999909,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.680000000000064,0],[1.307000000000016,-0.8586999999999989],[0.70900000000006,-1.567999999999998],[-0.1490000000000009,-2.202700000000007],[-0.7469999999999573,-1.530699999999996],[-1.380999999999972,-0.8586999999999989],[-1.79200000000003,0],[-1.268999999999892,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333386,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.755000000000109,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.315000000000055,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999937,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.277000000000044,0],[1.79200000000003,0.7839999999999918],[1.232000000000085,1.344000000000008],[0.6720000000000255,1.7547],[0,0.4852999999999952],[-0.03700000000003456,0.5227000000000004],[-8.04533333333336,0],[0,-1.680000000000007],[6.850666666666712,0],[-1.008000000000038,0.7653333333333308],[0.3729999999999336,-1.829300000000003],[-0.59699999999998,-1.456000000000003],[-1.232000000000085,-0.8586999999999989],[-1.67999999999995,0],[-1.306999999999903,0.8213000000000079],[-0.7089999999999463,1.530699999999996],[-0.1870000000000118,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418999999999983,0.8586999999999989],[1.828999999999951,0],[1.307000000000016,-0.8212999999999937],[1.717333333333386,0.8400000000000034],[-0.59699999999998,1.4187000000000012],[-1.231999999999971,1.11999999999999],[-1.716999999999985,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,7.951999999999998],[111.001,0.6720000000000041],[117.329,0.6720000000000041],[117.329,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,42.392],[111.001,12.096],[117.329,12.096],[117.329,42.392],[111.001,42.392],[111.001,42.392],[111.001,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[101.41,42.72800000000001],[94.01800000000003,40.040000000000006],[91.38599999999997,32.48],[91.38599999999997,17.808000000000007],[86.06600000000003,17.808000000000007],[86.06600000000003,12.096],[86.90599999999995,12.096],[90.21000000000004,10.864],[91.38599999999997,7.504000000000005],[91.38599999999997,5.152000000000001],[97.71400000000006,5.152000000000001],[97.71400000000006,12.096],[104.602,12.096],[104.602,17.808000000000007],[97.71400000000006,17.808000000000007],[97.71400000000006,32.2],[98.21799999999996,34.888000000000005],[99.84199999999998,36.568],[102.754,37.128],[103.76200000000006,37.072],[104.826,36.96000000000001],[104.826,42.392],[103.09,42.616],[101.41,42.72800000000001],[101.41,42.72800000000001],[101.41,42.72800000000001]],"i":[[0,0],[1.754999999999995,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333312,0],[0,1.903999999999996],[-0.2799999999999727,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999935,0],[0,-1.903999999999996],[2.295999999999935,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7469999999999573,-0.4106999999999914],[-1.19500000000005,0],[-0.3730000000000473,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6349999999999909,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333312,0],[0,-1.903999999999996],[0.2799999999999727,0],[1.419000000000096,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999935,0],[0,1.903999999999996],[-2.295999999999935,0],[0,4.797333333333327],[0,1.045299999999997],[0.3360000000000127,0.7092999999999989],[0.7470000000000709,0.3733000000000004],[0.2989999999999782,0],[0.3729999999999336,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[78.71499999999997,42.72800000000001],[71.32299999999998,40.040000000000006],[68.69100000000003,32.48],[68.69100000000003,17.808000000000007],[63.37099999999998,17.808000000000007],[63.37099999999998,12.096],[64.21100000000001,12.096],[67.51499999999999,10.864],[68.69100000000003,7.504000000000005],[68.69100000000003,5.152000000000001],[75.019,5.152000000000001],[75.019,12.096],[81.90700000000004,12.096],[81.90700000000004,17.808000000000007],[75.019,17.808000000000007],[75.019,32.2],[75.52300000000002,34.888000000000005],[77.14699999999999,36.568],[80.05900000000003,37.128],[81.06700000000001,37.072],[82.13099999999997,36.96000000000001],[82.13099999999997,42.392],[80.39499999999998,42.616],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001]],"i":[[0,0],[1.754000000000019,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7470000000000141,-0.4106999999999914],[-1.19500000000005,0],[-0.3740000000000236,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6340000000000146,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173999999999978,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418000000000006,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7460000000000377,0.3733000000000004],[0.297999999999945,0],[0.3730000000000473,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,37.184],[48.94799999999998,35.896],[52.19600000000003,32.36800000000001],[53.428,27.216000000000008],[52.19600000000003,22.12],[48.94799999999998,18.592],[44.18799999999999,17.304],[39.428,18.592],[36.12400000000002,22.12],[34.94799999999998,27.216000000000008],[36.12400000000002,32.36800000000001],[39.428,35.896],[44.18799999999999,37.184],[44.18799999999999,37.184],[44.18799999999999,37.184]],"i":[[0,0],[-1.381999999999948,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.381000000000029,0.8586999999999989],[1.79200000000003,0],[1.418000000000006,-0.8586999999999989],[0.8209999999999695,-1.493300000000005],[0,-1.904000000000011],[-0.7840000000000487,-1.5307000000000102],[-1.382000000000005,-0.8586999999999989],[-1.754999999999995,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.381000000000029,-0.8586999999999989],[0.8209999999999695,-1.5307000000000102],[0,-1.904000000000011],[-0.7840000000000487,-1.493300000000005],[-1.381999999999948,-0.8586999999999989],[-1.754999999999995,0],[-1.382000000000005,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.418000000000006,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,43.06400000000001],[36.18000000000001,40.992],[30.46800000000002,35.336],[28.33999999999997,27.216000000000008],[30.46800000000002,19.096],[36.18000000000001,13.49600000000001],[44.18799999999999,11.424000000000007],[52.19600000000003,13.49600000000001],[57.85199999999998,19.096],[59.98000000000002,27.216000000000008],[57.85199999999998,35.392],[52.139999999999986,41.048],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001]],"i":[[0,0],[2.425999999999988,1.381299999999996],[1.418000000000006,2.389300000000006],[0,3.024000000000001],[-1.41900000000004,2.352000000000004],[-2.389999999999986,1.343999999999994],[-2.949999999999989,0],[-2.352000000000032,-1.3813000000000102],[-1.381999999999948,-2.389300000000006],[0,-3.061300000000003],[1.418000000000006,-2.389299999999992],[2.38900000000001,-1.381299999999996],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.911999999999978,0],[-2.389999999999986,-1.381299999999996],[-1.41900000000004,-2.389299999999992],[0,-3.061300000000003],[1.418000000000006,-2.389300000000006],[2.38900000000001,-1.3813000000000102],[2.98599999999999,0],[2.388999999999953,1.343999999999994],[1.418000000000006,2.352000000000004],[0,3.061299999999989],[-1.418999999999983,2.389300000000006],[-2.389999999999986,1.343999999999994],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.608000000000004,0.6720000000000041],[6.608000000000004,36.512],[24.639999999999986,36.512],[24.639999999999986,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.202666666666687,0],[0,-11.94666666666666],[-6.01066666666668,0],[0,-1.959999999999994],[8.21333333333331,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.202666666666687,0],[0,11.94666666666667],[6.01066666666668,0],[0,1.959999999999994],[-8.21333333333331,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[98.08047485351562,-21.67217254638672],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[246.681,42.392],[246.681,0],[253.009,0],[253.009,18.032],[252.001,17.248],[255.585,12.936000000000007],[261.297,11.424000000000007],[267.23299999999995,12.88],[271.265,16.912000000000006],[272.721,22.792],[272.721,42.392],[266.449,42.392],[266.449,24.528000000000006],[265.553,20.664],[263.201,18.2],[259.729,17.304],[256.25699999999995,18.2],[253.849,20.664],[253.009,24.528000000000006],[253.009,42.392],[246.681,42.392],[246.681,42.392],[246.681,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.010666666666665],[0.3360000000000127,0.2613333333333259],[-1.643000000000029,0.9706999999999937],[-2.166000000000054,0],[-1.717999999999961,-0.9706999999999937],[-0.9710000000000036,-1.717300000000009],[0,-2.202699999999993],[0,-6.533333333333331],[2.090666666666721,0],[0,5.954666666666668],[0.59699999999998,1.045299999999997],[1.007999999999925,0.5600000000000023],[1.305999999999926,0],[1.045000000000073,-0.5973000000000042],[0.59699999999998,-1.082700000000003],[0,-1.493300000000005],[0,-5.954666666666668],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,6.010666666666665],[-0.3360000000000127,-0.2613333333333259],[0.7459999999999809,-1.903999999999996],[1.641999999999967,-1.0080000000000098],[2.240000000000009,0],[1.717000000000098,0.9707000000000079],[0.9700000000000273,1.717299999999994],[0,6.533333333333331],[-2.090666666666721,0],[0,-5.954666666666668],[0,-1.5307000000000102],[-0.5599999999999454,-1.082700000000003],[-1.008000000000038,-0.5973000000000042],[-1.270000000000095,0],[-1.007999999999953,0.5600000000000023],[-0.5600000000000023,1.082700000000003],[0,5.954666666666668],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[237.089,42.72800000000001],[229.697,40.040000000000006],[227.065,32.48],[227.065,17.808000000000007],[221.745,17.808000000000007],[221.745,12.096],[222.585,12.096],[225.889,10.864],[227.065,7.504000000000005],[227.065,5.152000000000001],[233.393,5.152000000000001],[233.393,12.096],[240.281,12.096],[240.281,17.808000000000007],[233.393,17.808000000000007],[233.393,32.2],[233.897,34.888000000000005],[235.521,36.568],[238.433,37.128],[239.441,37.072],[240.505,36.96000000000001],[240.505,42.392],[238.769,42.616],[237.089,42.72800000000001],[237.089,42.72800000000001],[237.089,42.72800000000001]],"i":[[0,0],[1.755000000000052,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7459999999999809,-0.4106999999999914],[-1.194000000000017,0],[-0.3729999999999905,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6350000000000477,-0.07469999999999288],[0.48599999999999,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.753999999999962,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418999999999983,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7470000000000141,0.3733000000000004],[0.2989999999999782,0],[0.3740000000000236,-0.03730000000000189],[0,1.810666666666663],[-0.5220000000000482,0.0747000000000071],[-0.6339999999999577,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,7.951999999999998],[210.259,0.6720000000000041],[216.587,0.6720000000000041],[216.587,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,42.392],[210.259,12.096],[216.587,12.096],[216.587,42.392],[210.259,42.392],[210.259,42.392],[210.259,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[169.688,42.392],[159.272,12.096],[165.992,12.096],[173.944,36.232],[171.592,36.232],[179.712,12.096],[185.48,12.096],[193.544,36.232],[191.192,36.232],[199.2,12.096],[205.92,12.096],[195.448,42.392],[189.736,42.392],[181.56,17.696],[183.632,17.696],[175.456,42.392],[169.688,42.392],[169.688,42.392],[169.688,42.392]],"i":[[0,0],[3.47199999999998,10.09866666666667],[-2.240000000000009,0],[-2.650666666666666,-8.045333333333332],[0.7839999999999918,0],[-2.706666666666649,8.045333333333332],[-1.922666666666657,0],[-2.687999999999988,-8.045333333333332],[0.7839999999999918,0],[-2.669333333333327,8.045333333333332],[-2.240000000000009,0],[3.490666666666641,-10.09866666666667],[1.903999999999996,0],[2.725333333333367,8.232],[-0.6906666666666865,0],[2.72533333333331,-8.232],[1.922666666666657,0],[0,0],[0,0]],"o":[[-3.47199999999998,-10.09866666666666],[2.240000000000009,0],[2.650666666666666,8.045333333333332],[-0.7839999999999918,0],[2.706666666666649,-8.045333333333332],[1.922666666666657,0],[2.687999999999988,8.045333333333332],[-0.7839999999999918,0],[2.669333333333327,-8.045333333333332],[2.240000000000009,0],[-3.490666666666641,10.09866666666666],[-1.903999999999996,0],[-2.725333333333367,-8.232],[0.6906666666666865,0],[-2.72533333333331,8.232],[-1.922666666666657,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[155.86146545410156,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[132.444,43.06400000000001],[124.38,40.992],[118.948,35.28],[116.988,27.16],[118.948,19.040000000000006],[124.324,13.49600000000001],[131.94,11.424000000000007],[138.044,12.600000000000009],[142.58,15.848],[145.38,20.49600000000001],[146.388,26.096],[146.332,27.608],[146.108,29.064000000000007],[121.972,29.064000000000007],[121.972,24.024],[142.524,24.024],[139.5,26.320000000000007],[139.108,21.448000000000008],[136.308,18.032],[131.94,16.744],[127.46,18.032],[124.436,21.616],[123.596,27.216000000000008],[124.436,32.592],[127.684,36.176],[132.5,37.464],[137.148,36.232],[140.228,33.040000000000006],[145.38,35.56],[142.58,39.42400000000001],[138.1,42.11200000000001],[132.444,43.06400000000001],[132.444,43.06400000000001],[132.444,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.305999999999983,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.838000000000022,0],[-1.79200000000003,-0.784000000000006],[-1.232000000000028,-1.381299999999996],[-0.6350000000000477,-1.754700000000014],[0,-1.978700000000003],[0.03699999999997772,-0.5227000000000004],[0.1120000000000232,-0.4480000000000075],[8.045333333333303,0],[0,1.680000000000007],[-6.850666666666655,0],[1.007999999999981,-0.7653333333333308],[0.6340000000000146,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.67999999999995,0],[1.305999999999983,-0.8586999999999989],[0.7090000000000032,-1.567999999999998],[-0.1499999999999773,-2.202700000000007],[-0.7470000000000141,-1.530699999999996],[-1.382000000000005,-0.8586999999999989],[-1.791999999999973,0],[-1.269999999999982,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333329,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.754000000000019,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.314999999999998,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999994,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.276999999999987,0],[1.791999999999973,0.7839999999999918],[1.231999999999971,1.344000000000008],[0.6719999999999686,1.7547],[0,0.4852999999999952],[-0.03800000000001091,0.5227000000000004],[-8.045333333333303,0],[0,-1.680000000000007],[6.850666666666655,0],[-1.007999999999981,0.7653333333333308],[0.3730000000000473,-1.829300000000003],[-0.5979999999999563,-1.456000000000003],[-1.232000000000028,-0.8586999999999989],[-1.680000000000007,0],[-1.307000000000016,0.8213000000000079],[-0.7100000000000364,1.530699999999996],[-0.186999999999955,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418000000000006,0.8586999999999989],[1.829000000000008,0],[1.305999999999983,-0.8212999999999937],[1.717333333333329,0.8400000000000034],[-0.5980000000000132,1.4187000000000012],[-1.232000000000028,1.11999999999999],[-1.718000000000018,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[95.32,37.184],[100.024,35.896],[103.328,32.36800000000001],[104.56,27.216000000000008],[103.328,22.12],[100.024,18.592],[95.32,17.304],[90.56,18.592],[87.256,22.12],[86.08000000000001,27.216000000000008],[87.256,32.36800000000001],[90.50399999999999,35.896],[95.32,37.184],[95.32,37.184],[95.32,37.184]],"i":[[0,0],[-1.381,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.820999999999998,1.493299999999991],[1.419000000000011,0.8586999999999989],[1.754999999999995,0],[1.418999999999983,-0.8586999999999989],[0.7839999999999918,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.381,-0.8586999999999989],[-1.792000000000002,0],[0,0],[0,0]],"o":[[1.754999999999995,0],[1.419000000000011,-0.8586999999999989],[0.820999999999998,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.381,-0.8586999999999989],[-1.754999999999995,0],[-1.419000000000011,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.7839999999999918,1.493299999999991],[1.419000000000011,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[94.70400000000001,43.06400000000001],[86.864,40.992],[81.43199999999999,35.28],[79.47200000000001,27.216000000000008],[81.488,19.152],[86.91999999999999,13.49600000000001],[94.648,11.424000000000007],[101.088,12.88],[105.512,16.912000000000006],[104.56,18.36800000000001],[104.56,0],[110.832,0],[110.832,42.392],[104.84,42.392],[104.84,36.232],[105.568,37.408],[101.088,41.608],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001]],"i":[[0,0],[2.314999999999998,1.381299999999996],[1.344000000000023,2.389300000000006],[0,2.986699999999999],[-1.343999999999994,2.389300000000006],[-2.276999999999987,1.381299999999996],[-2.875,0],[-1.8669999999999902,-0.9706999999999937],[-1.082999999999998,-1.717300000000009],[0.3173333333333233,-0.4853333333333296],[0,6.122666666666674],[-2.090666666666664,0],[0,-14.13066666666667],[1.99733333333333,0],[0,2.053333333333327],[-0.242666666666679,-0.3919999999999959],[1.941000000000003,-0.9707000000000079],[2.314999999999998,0],[0,0],[0,0]],"o":[[-2.912000000000006,0],[-2.277000000000015,-1.4187000000000012],[-1.306999999999988,-2.389300000000006],[0,-2.986699999999999],[1.343999999999994,-2.389300000000006],[2.277000000000015,-1.3813000000000102],[2.426999999999992,0],[1.867000000000019,0.9707000000000079],[-0.3173333333333233,0.4853333333333296],[0,-6.122666666666674],[2.090666666666664,0],[0,14.13066666666667],[-1.99733333333333,0],[0,-2.053333333333327],[0.242666666666679,0.3919999999999959],[-1.045000000000016,1.829300000000003],[-1.941000000000003,0.9706999999999937],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[57.40100000000001,43.06400000000001],[51.968999999999994,42],[48.38499999999999,38.976],[47.09700000000001,34.384],[48.161,29.960000000000008],[51.577,26.656000000000006],[57.345,24.752],[67.87299999999999,23.016000000000005],[67.87299999999999,28],[58.577,29.624],[54.881,31.248],[53.70500000000001,34.16],[54.992999999999995,37.016000000000005],[58.40899999999999,38.08],[62.88900000000001,36.96000000000001],[65.91300000000001,33.992],[66.977,29.792],[66.977,22.008],[65.297,18.36800000000001],[60.929,16.912000000000006],[56.505,18.256],[53.761,21.616],[48.496999999999986,18.98400000000001],[51.240999999999985,15.064000000000007],[55.66499999999999,12.376],[61.09700000000001,11.424000000000007],[67.42500000000001,12.768],[71.737,16.52],[73.305,22.008],[73.305,42.392],[67.257,42.392],[67.257,36.904],[68.54499999999999,37.072],[65.80099999999999,40.264],[62.04900000000001,42.336],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001]],"i":[[0,0],[1.568000000000012,0.7092999999999989],[0.8590000000000089,1.2693000000000012],[0,1.7547],[-0.7090000000000032,1.306699999999992],[-1.531000000000006,0.8960000000000008],[-2.314999999999998,0.3733000000000004],[-3.509333333333331,0.5786666666666633],[0,-1.661333333333332],[3.098666666666674,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8590000000000089,-0.7467000000000041],[-1.381,0],[-1.269000000000005,0.7466999999999899],[-0.7090000000000032,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829000000000008,0],[1.269000000000005,-0.8960000000000008],[0.5970000000000084,-1.381299999999996],[1.754666666666679,0.8773333333333255],[-1.268999999999977,1.11999999999999],[-1.680000000000007,0.6346999999999952],[-1.903999999999996,0],[-1.829000000000008,-0.8960000000000008],[-1.0080000000000098,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999991,0],[0,1.829333333333338],[-0.429333333333318,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.716999999999985,0],[0,0],[0,0]],"o":[[-2.053000000000026,0],[-1.531000000000006,-0.7467000000000041],[-0.8589999999999804,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999857,-1.306700000000006],[1.531000000000006,-0.8960000000000008],[3.509333333333331,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666674,0.541333333333327],[-1.680000000000007,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.896000000000015,0.7092999999999989],[1.717000000000013,0],[1.306999999999988,-0.7467000000000041],[0.7089999999999748,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.082999999999998,-0.9707000000000079],[-1.680000000000007,0],[-1.2319999999999993,0.8586999999999989],[-1.754666666666679,-0.8773333333333255],[0.5600000000000023,-1.493300000000005],[1.269000000000005,-1.157300000000006],[1.717000000000013,-0.6347000000000094],[2.388999999999982,0],[1.86699999999999,0.8960000000000008],[1.045000000000016,1.568000000000012],[0,6.794666666666672],[-2.015999999999991,0],[0,-1.829333333333338],[0.429333333333318,0.05599999999999739],[-0.7089999999999748,1.2319999999999993],[-1.082999999999998,0.8960000000000008],[-1.381,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.159999999999997,0.6720000000000041],[21.84,22.400000000000006],[18.75999999999999,22.400000000000006],[34.16,0.6720000000000041],[40.31999999999999,0.6720000000000041],[40.31999999999999,42.392],[33.768,42.392],[33.768,8.456000000000003],[36.232,9.128],[20.49600000000001,30.632000000000005],[19.824000000000012,30.632000000000005],[4.424000000000007,9.128],[6.608000000000004,8.456000000000003],[6.608000000000004,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.053333333333342,0],[-5.226666666666659,-7.242666666666665],[1.026666666666671,0],[-5.133333333333326,7.242666666666672],[-2.053333333333342,0],[0,-13.90666666666667],[2.183999999999997,0],[0,11.312],[-0.8213333333333424,-0.2240000000000038],[5.245333333333321,-7.168000000000006],[0.2239999999999895,0],[5.133333333333326,7.168000000000006],[-0.7280000000000086,0.2240000000000038],[0,-11.312],[2.202666666666659,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.053333333333342,0],[5.226666666666659,7.242666666666672],[-1.026666666666671,0],[5.133333333333326,-7.242666666666665],[2.053333333333342,0],[0,13.90666666666666],[-2.183999999999997,0],[0,-11.312],[0.8213333333333424,0.2240000000000038],[-5.245333333333321,7.168000000000006],[-0.2239999999999895,0],[-5.133333333333326,-7.168000000000006],[0.7280000000000086,-0.2240000000000038],[0,11.312],[-2.202666666666659,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[2.91259765625,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[702.6863719370097,144],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":72,"ix":2}},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[56.54167175292969,-0.000022762338630855083],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":80,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[122.0000003294881,25.00000012138912],"ix":2},"a":{"a":0,"k":[56.54167175292969,-0.00002288818359375],"ix":2},"s":{"a":0,"k":[34.403572049765366,34.403572049765366],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":153,"st":0,"bm":0},{"ddd":0,"refId":"0","w":500,"h":500,"ind":22,"ty":0,"nm":"hover-domain","sr":1,"ks":{"p":{"a":0,"k":[250,250],"ix":2},"a":{"a":0,"k":[250,250],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/frontend/public/lotties/gateway.json b/frontend/public/lotties/gateway.json new file mode 100644 index 000000000..d444658a4 --- /dev/null +++ b/frontend/public/lotties/gateway.json @@ -0,0 +1 @@ +{"v":"5.7.5","fr":100,"ip":0,"op":667,"w":200,"h":200,"nm":"Comp 1","ddd":0,"metadata":{},"assets":[],"layers":[{"ddd":0,"ind":12345679,"ty":4,"nm":"Group Layer 8","sr":1,"ks":{"p":{"a":0,"k":[146,185.75409836065575,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[20.491803278688526,20.491803278688526,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[220.741,37.184],[225.501,35.896],[228.749,32.36800000000001],[229.981,27.216000000000008],[228.749,22.12],[225.501,18.592],[220.741,17.304],[215.981,18.592],[212.677,22.12],[211.501,27.216000000000008],[212.677,32.36800000000001],[215.981,35.896],[220.741,37.184],[220.741,37.184],[220.741,37.184]],"i":[[0,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.382000000000062,0.8586999999999989],[1.79200000000003,0],[1.418999999999983,-0.8586999999999989],[0.8220000000000027,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.382000000000062,-0.8586999999999989],[0.8220000000000027,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.418999999999983,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[221.357,43.06400000000001],[214.917,41.608],[210.49300000000005,37.408],[211.221,36.232],[211.221,42.392],[205.173,42.392],[205.173,0],[211.501,0],[211.501,18.36800000000001],[210.49300000000005,16.912000000000006],[214.973,12.88],[221.357,11.424000000000007],[229.085,13.49600000000001],[234.51700000000005,19.152],[236.533,27.216000000000008],[234.51700000000005,35.28],[229.141,40.992],[221.357,43.06400000000001],[221.357,43.06400000000001],[221.357,43.06400000000001]],"i":[[0,0],[1.942000000000007,0.9706999999999937],[1.045999999999935,1.829300000000003],[-0.2426666666666506,0.3919999999999959],[0,-2.053333333333327],[2.015999999999963,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.122666666666674],[0.3360000000000127,0.4853333333333296],[-1.865999999999985,0.9707000000000079],[-2.38900000000001,0],[-2.277000000000044,-1.3813000000000102],[-1.30600000000004,-2.389300000000006],[0,-2.986699999999999],[1.343999999999937,-2.389300000000006],[2.27800000000002,-1.4187000000000012],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.351999999999975,0],[-1.903999999999996,-0.9707000000000079],[0.2426666666666506,-0.3919999999999959],[0,2.053333333333327],[-2.015999999999963,0],[0,-14.13066666666667],[2.109333333333325,0],[0,6.122666666666667],[-0.3360000000000127,-0.4853333333333296],[1.120000000000005,-1.717300000000009],[1.867000000000075,-0.9706999999999937],[2.875,0],[2.314999999999941,1.381299999999996],[1.343999999999937,2.389300000000006],[0,2.986699999999999],[-1.30600000000004,2.389300000000006],[-2.27699999999993,1.381299999999996],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[181.87,43.06400000000001],[176.438,42],[172.854,38.976],[171.566,34.384],[172.63,29.960000000000008],[176.046,26.656000000000006],[181.814,24.752],[192.342,23.016000000000005],[192.342,28],[183.046,29.624],[179.35,31.248],[178.174,34.16],[179.462,37.016000000000005],[182.878,38.08],[187.35799999999995,36.96000000000001],[190.38199999999995,33.992],[191.446,29.792],[191.446,22.008],[189.766,18.36800000000001],[185.398,16.912000000000006],[180.974,18.256],[178.23,21.616],[172.966,18.98400000000001],[175.71,15.064000000000007],[180.134,12.376],[185.566,11.424000000000007],[191.894,12.768],[196.206,16.52],[197.774,22.008],[197.774,42.392],[191.726,42.392],[191.726,36.904],[193.014,37.072],[190.27,40.264],[186.518,42.336],[181.87,43.06400000000001],[181.87,43.06400000000001],[181.87,43.06400000000001]],"i":[[0,0],[1.567999999999984,0.7092999999999989],[0.8589999999999236,1.2693000000000012],[0,1.7547],[-0.7089999999999463,1.306699999999992],[-1.530000000000086,0.8960000000000008],[-2.313999999999965,0.3733000000000004],[-3.509333333333302,0.5786666666666633],[0,-1.661333333333332],[3.098666666666645,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8579999999999472,-0.7467000000000041],[-1.381000000000085,0],[-1.268999999999892,0.7466999999999899],[-0.7089999999999463,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829999999999927,0],[1.269999999999982,-0.8960000000000008],[0.5979999999999563,-1.381299999999996],[1.754666666666708,0.8773333333333255],[-1.269000000000005,1.11999999999999],[-1.680000000000064,0.6346999999999952],[-1.903999999999996,0],[-1.828999999999951,-0.8960000000000008],[-1.008000000000038,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999963,0],[0,1.829333333333338],[-0.4293333333333749,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.717999999999961,0],[0,0],[0,0]],"o":[[-2.052999999999997,0],[-1.529999999999973,-0.7467000000000041],[-0.8580000000000609,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999573,-1.306700000000006],[1.530999999999949,-0.8960000000000008],[3.509333333333302,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666645,0.541333333333327],[-1.680000000000064,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.8959999999999582,0.7092999999999989],[1.717999999999961,0],[1.307000000000016,-0.7467000000000041],[0.7100000000000364,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.081999999999994,-0.9707000000000079],[-1.680000000000064,0],[-1.232000000000085,0.8586999999999989],[-1.754666666666708,-0.8773333333333255],[0.5599999999999454,-1.493300000000005],[1.269999999999982,-1.157300000000006],[1.717999999999961,-0.6347000000000094],[2.389999999999986,0],[1.866999999999962,0.8960000000000008],[1.045999999999935,1.568000000000012],[0,6.794666666666672],[-2.015999999999963,0],[0,-1.829333333333338],[0.4293333333333749,0.05599999999999739],[-0.70900000000006,1.2319999999999993],[-1.081999999999994,0.8960000000000008],[-1.380999999999972,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[159.072,42.392],[159.072,0],[165.4,0],[165.4,42.392],[159.072,42.392],[159.072,42.392],[159.072,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-14.13066666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,14.13066666666667],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[138.952,43.06400000000001],[130.888,40.992],[125.456,35.28],[123.496,27.16],[125.456,19.040000000000006],[130.832,13.49600000000001],[138.448,11.424000000000007],[144.552,12.600000000000009],[149.088,15.848],[151.888,20.49600000000001],[152.896,26.096],[152.84,27.608],[152.616,29.064000000000007],[128.48,29.064000000000007],[128.48,24.024],[149.032,24.024],[146.008,26.320000000000007],[145.616,21.448000000000008],[142.816,18.032],[138.448,16.744],[133.968,18.032],[130.944,21.616],[130.104,27.216000000000008],[130.944,32.592],[134.192,36.176],[139.008,37.464],[143.65599999999995,36.232],[146.736,33.040000000000006],[151.888,35.56],[149.088,39.42400000000001],[144.60799999999995,42.11200000000001],[138.952,43.06400000000001],[138.952,43.06400000000001],[138.952,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.307000000000016,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.836999999999989,0],[-1.79200000000003,-0.784000000000006],[-1.231999999999971,-1.381299999999996],[-0.6349999999999909,-1.754700000000014],[0,-1.978700000000003],[0.03699999999992087,-0.5227000000000004],[0.1119999999999663,-0.4480000000000075],[8.04533333333336,0],[0,1.680000000000007],[-6.850666666666712,0],[1.008000000000038,-0.7653333333333308],[0.6349999999999909,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.680000000000064,0],[1.307000000000016,-0.8586999999999989],[0.70900000000006,-1.567999999999998],[-0.1490000000000009,-2.202700000000007],[-0.7469999999999573,-1.530699999999996],[-1.380999999999972,-0.8586999999999989],[-1.79200000000003,0],[-1.268999999999892,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333386,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.755000000000109,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.315000000000055,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999937,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.277000000000044,0],[1.79200000000003,0.7839999999999918],[1.232000000000085,1.344000000000008],[0.6720000000000255,1.7547],[0,0.4852999999999952],[-0.03700000000003456,0.5227000000000004],[-8.04533333333336,0],[0,-1.680000000000007],[6.850666666666712,0],[-1.008000000000038,0.7653333333333308],[0.3729999999999336,-1.829300000000003],[-0.59699999999998,-1.456000000000003],[-1.232000000000085,-0.8586999999999989],[-1.67999999999995,0],[-1.306999999999903,0.8213000000000079],[-0.7089999999999463,1.530699999999996],[-0.1870000000000118,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418999999999983,0.8586999999999989],[1.828999999999951,0],[1.307000000000016,-0.8212999999999937],[1.717333333333386,0.8400000000000034],[-0.59699999999998,1.4187000000000012],[-1.231999999999971,1.11999999999999],[-1.716999999999985,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,7.951999999999998],[111.001,0.6720000000000041],[117.329,0.6720000000000041],[117.329,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,42.392],[111.001,12.096],[117.329,12.096],[117.329,42.392],[111.001,42.392],[111.001,42.392],[111.001,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[101.41,42.72800000000001],[94.01800000000003,40.040000000000006],[91.38599999999997,32.48],[91.38599999999997,17.808000000000007],[86.06600000000003,17.808000000000007],[86.06600000000003,12.096],[86.90599999999995,12.096],[90.21000000000004,10.864],[91.38599999999997,7.504000000000005],[91.38599999999997,5.152000000000001],[97.71400000000006,5.152000000000001],[97.71400000000006,12.096],[104.602,12.096],[104.602,17.808000000000007],[97.71400000000006,17.808000000000007],[97.71400000000006,32.2],[98.21799999999996,34.888000000000005],[99.84199999999998,36.568],[102.754,37.128],[103.76200000000006,37.072],[104.826,36.96000000000001],[104.826,42.392],[103.09,42.616],[101.41,42.72800000000001],[101.41,42.72800000000001],[101.41,42.72800000000001]],"i":[[0,0],[1.754999999999995,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333312,0],[0,1.903999999999996],[-0.2799999999999727,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999935,0],[0,-1.903999999999996],[2.295999999999935,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7469999999999573,-0.4106999999999914],[-1.19500000000005,0],[-0.3730000000000473,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6349999999999909,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333312,0],[0,-1.903999999999996],[0.2799999999999727,0],[1.419000000000096,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999935,0],[0,1.903999999999996],[-2.295999999999935,0],[0,4.797333333333327],[0,1.045299999999997],[0.3360000000000127,0.7092999999999989],[0.7470000000000709,0.3733000000000004],[0.2989999999999782,0],[0.3729999999999336,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[78.71499999999997,42.72800000000001],[71.32299999999998,40.040000000000006],[68.69100000000003,32.48],[68.69100000000003,17.808000000000007],[63.37099999999998,17.808000000000007],[63.37099999999998,12.096],[64.21100000000001,12.096],[67.51499999999999,10.864],[68.69100000000003,7.504000000000005],[68.69100000000003,5.152000000000001],[75.019,5.152000000000001],[75.019,12.096],[81.90700000000004,12.096],[81.90700000000004,17.808000000000007],[75.019,17.808000000000007],[75.019,32.2],[75.52300000000002,34.888000000000005],[77.14699999999999,36.568],[80.05900000000003,37.128],[81.06700000000001,37.072],[82.13099999999997,36.96000000000001],[82.13099999999997,42.392],[80.39499999999998,42.616],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001]],"i":[[0,0],[1.754000000000019,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7470000000000141,-0.4106999999999914],[-1.19500000000005,0],[-0.3740000000000236,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6340000000000146,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173999999999978,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418000000000006,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7460000000000377,0.3733000000000004],[0.297999999999945,0],[0.3730000000000473,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,37.184],[48.94799999999998,35.896],[52.19600000000003,32.36800000000001],[53.428,27.216000000000008],[52.19600000000003,22.12],[48.94799999999998,18.592],[44.18799999999999,17.304],[39.428,18.592],[36.12400000000002,22.12],[34.94799999999998,27.216000000000008],[36.12400000000002,32.36800000000001],[39.428,35.896],[44.18799999999999,37.184],[44.18799999999999,37.184],[44.18799999999999,37.184]],"i":[[0,0],[-1.381999999999948,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.381000000000029,0.8586999999999989],[1.79200000000003,0],[1.418000000000006,-0.8586999999999989],[0.8209999999999695,-1.493300000000005],[0,-1.904000000000011],[-0.7840000000000487,-1.5307000000000102],[-1.382000000000005,-0.8586999999999989],[-1.754999999999995,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.381000000000029,-0.8586999999999989],[0.8209999999999695,-1.5307000000000102],[0,-1.904000000000011],[-0.7840000000000487,-1.493300000000005],[-1.381999999999948,-0.8586999999999989],[-1.754999999999995,0],[-1.382000000000005,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.418000000000006,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,43.06400000000001],[36.18000000000001,40.992],[30.46800000000002,35.336],[28.33999999999997,27.216000000000008],[30.46800000000002,19.096],[36.18000000000001,13.49600000000001],[44.18799999999999,11.424000000000007],[52.19600000000003,13.49600000000001],[57.85199999999998,19.096],[59.98000000000002,27.216000000000008],[57.85199999999998,35.392],[52.139999999999986,41.048],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001]],"i":[[0,0],[2.425999999999988,1.381299999999996],[1.418000000000006,2.389300000000006],[0,3.024000000000001],[-1.41900000000004,2.352000000000004],[-2.389999999999986,1.343999999999994],[-2.949999999999989,0],[-2.352000000000032,-1.3813000000000102],[-1.381999999999948,-2.389300000000006],[0,-3.061300000000003],[1.418000000000006,-2.389299999999992],[2.38900000000001,-1.381299999999996],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.911999999999978,0],[-2.389999999999986,-1.381299999999996],[-1.41900000000004,-2.389299999999992],[0,-3.061300000000003],[1.418000000000006,-2.389300000000006],[2.38900000000001,-1.3813000000000102],[2.98599999999999,0],[2.388999999999953,1.343999999999994],[1.418000000000006,2.352000000000004],[0,3.061299999999989],[-1.418999999999983,2.389300000000006],[-2.389999999999986,1.343999999999994],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.608000000000004,0.6720000000000041],[6.608000000000004,36.512],[24.639999999999986,36.512],[24.639999999999986,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.202666666666687,0],[0,-11.94666666666666],[-6.01066666666668,0],[0,-1.959999999999994],[8.21333333333331,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.202666666666687,0],[0,11.94666666666667],[6.01066666666668,0],[0,1.959999999999994],[-8.21333333333331,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[98.08047485351562,-21.67217254638672],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[246.681,42.392],[246.681,0],[253.009,0],[253.009,18.032],[252.001,17.248],[255.585,12.936000000000007],[261.297,11.424000000000007],[267.23299999999995,12.88],[271.265,16.912000000000006],[272.721,22.792],[272.721,42.392],[266.449,42.392],[266.449,24.528000000000006],[265.553,20.664],[263.201,18.2],[259.729,17.304],[256.25699999999995,18.2],[253.849,20.664],[253.009,24.528000000000006],[253.009,42.392],[246.681,42.392],[246.681,42.392],[246.681,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.010666666666665],[0.3360000000000127,0.2613333333333259],[-1.643000000000029,0.9706999999999937],[-2.166000000000054,0],[-1.717999999999961,-0.9706999999999937],[-0.9710000000000036,-1.717300000000009],[0,-2.202699999999993],[0,-6.533333333333331],[2.090666666666721,0],[0,5.954666666666668],[0.59699999999998,1.045299999999997],[1.007999999999925,0.5600000000000023],[1.305999999999926,0],[1.045000000000073,-0.5973000000000042],[0.59699999999998,-1.082700000000003],[0,-1.493300000000005],[0,-5.954666666666668],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,6.010666666666665],[-0.3360000000000127,-0.2613333333333259],[0.7459999999999809,-1.903999999999996],[1.641999999999967,-1.0080000000000098],[2.240000000000009,0],[1.717000000000098,0.9707000000000079],[0.9700000000000273,1.717299999999994],[0,6.533333333333331],[-2.090666666666721,0],[0,-5.954666666666668],[0,-1.5307000000000102],[-0.5599999999999454,-1.082700000000003],[-1.008000000000038,-0.5973000000000042],[-1.270000000000095,0],[-1.007999999999953,0.5600000000000023],[-0.5600000000000023,1.082700000000003],[0,5.954666666666668],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[237.089,42.72800000000001],[229.697,40.040000000000006],[227.065,32.48],[227.065,17.808000000000007],[221.745,17.808000000000007],[221.745,12.096],[222.585,12.096],[225.889,10.864],[227.065,7.504000000000005],[227.065,5.152000000000001],[233.393,5.152000000000001],[233.393,12.096],[240.281,12.096],[240.281,17.808000000000007],[233.393,17.808000000000007],[233.393,32.2],[233.897,34.888000000000005],[235.521,36.568],[238.433,37.128],[239.441,37.072],[240.505,36.96000000000001],[240.505,42.392],[238.769,42.616],[237.089,42.72800000000001],[237.089,42.72800000000001],[237.089,42.72800000000001]],"i":[[0,0],[1.755000000000052,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7459999999999809,-0.4106999999999914],[-1.194000000000017,0],[-0.3729999999999905,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6350000000000477,-0.07469999999999288],[0.48599999999999,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.753999999999962,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418999999999983,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7470000000000141,0.3733000000000004],[0.2989999999999782,0],[0.3740000000000236,-0.03730000000000189],[0,1.810666666666663],[-0.5220000000000482,0.0747000000000071],[-0.6339999999999577,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,7.951999999999998],[210.259,0.6720000000000041],[216.587,0.6720000000000041],[216.587,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,42.392],[210.259,12.096],[216.587,12.096],[216.587,42.392],[210.259,42.392],[210.259,42.392],[210.259,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[169.688,42.392],[159.272,12.096],[165.992,12.096],[173.944,36.232],[171.592,36.232],[179.712,12.096],[185.48,12.096],[193.544,36.232],[191.192,36.232],[199.2,12.096],[205.92,12.096],[195.448,42.392],[189.736,42.392],[181.56,17.696],[183.632,17.696],[175.456,42.392],[169.688,42.392],[169.688,42.392],[169.688,42.392]],"i":[[0,0],[3.47199999999998,10.09866666666667],[-2.240000000000009,0],[-2.650666666666666,-8.045333333333332],[0.7839999999999918,0],[-2.706666666666649,8.045333333333332],[-1.922666666666657,0],[-2.687999999999988,-8.045333333333332],[0.7839999999999918,0],[-2.669333333333327,8.045333333333332],[-2.240000000000009,0],[3.490666666666641,-10.09866666666667],[1.903999999999996,0],[2.725333333333367,8.232],[-0.6906666666666865,0],[2.72533333333331,-8.232],[1.922666666666657,0],[0,0],[0,0]],"o":[[-3.47199999999998,-10.09866666666666],[2.240000000000009,0],[2.650666666666666,8.045333333333332],[-0.7839999999999918,0],[2.706666666666649,-8.045333333333332],[1.922666666666657,0],[2.687999999999988,8.045333333333332],[-0.7839999999999918,0],[2.669333333333327,-8.045333333333332],[2.240000000000009,0],[-3.490666666666641,10.09866666666666],[-1.903999999999996,0],[-2.725333333333367,-8.232],[0.6906666666666865,0],[-2.72533333333331,8.232],[-1.922666666666657,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[155.86146545410156,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[132.444,43.06400000000001],[124.38,40.992],[118.948,35.28],[116.988,27.16],[118.948,19.040000000000006],[124.324,13.49600000000001],[131.94,11.424000000000007],[138.044,12.600000000000009],[142.58,15.848],[145.38,20.49600000000001],[146.388,26.096],[146.332,27.608],[146.108,29.064000000000007],[121.972,29.064000000000007],[121.972,24.024],[142.524,24.024],[139.5,26.320000000000007],[139.108,21.448000000000008],[136.308,18.032],[131.94,16.744],[127.46,18.032],[124.436,21.616],[123.596,27.216000000000008],[124.436,32.592],[127.684,36.176],[132.5,37.464],[137.148,36.232],[140.228,33.040000000000006],[145.38,35.56],[142.58,39.42400000000001],[138.1,42.11200000000001],[132.444,43.06400000000001],[132.444,43.06400000000001],[132.444,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.305999999999983,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.838000000000022,0],[-1.79200000000003,-0.784000000000006],[-1.232000000000028,-1.381299999999996],[-0.6350000000000477,-1.754700000000014],[0,-1.978700000000003],[0.03699999999997772,-0.5227000000000004],[0.1120000000000232,-0.4480000000000075],[8.045333333333303,0],[0,1.680000000000007],[-6.850666666666655,0],[1.007999999999981,-0.7653333333333308],[0.6340000000000146,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.67999999999995,0],[1.305999999999983,-0.8586999999999989],[0.7090000000000032,-1.567999999999998],[-0.1499999999999773,-2.202700000000007],[-0.7470000000000141,-1.530699999999996],[-1.382000000000005,-0.8586999999999989],[-1.791999999999973,0],[-1.269999999999982,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333329,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.754000000000019,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.314999999999998,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999994,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.276999999999987,0],[1.791999999999973,0.7839999999999918],[1.231999999999971,1.344000000000008],[0.6719999999999686,1.7547],[0,0.4852999999999952],[-0.03800000000001091,0.5227000000000004],[-8.045333333333303,0],[0,-1.680000000000007],[6.850666666666655,0],[-1.007999999999981,0.7653333333333308],[0.3730000000000473,-1.829300000000003],[-0.5979999999999563,-1.456000000000003],[-1.232000000000028,-0.8586999999999989],[-1.680000000000007,0],[-1.307000000000016,0.8213000000000079],[-0.7100000000000364,1.530699999999996],[-0.186999999999955,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418000000000006,0.8586999999999989],[1.829000000000008,0],[1.305999999999983,-0.8212999999999937],[1.717333333333329,0.8400000000000034],[-0.5980000000000132,1.4187000000000012],[-1.232000000000028,1.11999999999999],[-1.718000000000018,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[95.32,37.184],[100.024,35.896],[103.328,32.36800000000001],[104.56,27.216000000000008],[103.328,22.12],[100.024,18.592],[95.32,17.304],[90.56,18.592],[87.256,22.12],[86.08000000000001,27.216000000000008],[87.256,32.36800000000001],[90.50399999999999,35.896],[95.32,37.184],[95.32,37.184],[95.32,37.184]],"i":[[0,0],[-1.381,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.820999999999998,1.493299999999991],[1.419000000000011,0.8586999999999989],[1.754999999999995,0],[1.418999999999983,-0.8586999999999989],[0.7839999999999918,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.381,-0.8586999999999989],[-1.792000000000002,0],[0,0],[0,0]],"o":[[1.754999999999995,0],[1.419000000000011,-0.8586999999999989],[0.820999999999998,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.381,-0.8586999999999989],[-1.754999999999995,0],[-1.419000000000011,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.7839999999999918,1.493299999999991],[1.419000000000011,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[94.70400000000001,43.06400000000001],[86.864,40.992],[81.43199999999999,35.28],[79.47200000000001,27.216000000000008],[81.488,19.152],[86.91999999999999,13.49600000000001],[94.648,11.424000000000007],[101.088,12.88],[105.512,16.912000000000006],[104.56,18.36800000000001],[104.56,0],[110.832,0],[110.832,42.392],[104.84,42.392],[104.84,36.232],[105.568,37.408],[101.088,41.608],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001]],"i":[[0,0],[2.314999999999998,1.381299999999996],[1.344000000000023,2.389300000000006],[0,2.986699999999999],[-1.343999999999994,2.389300000000006],[-2.276999999999987,1.381299999999996],[-2.875,0],[-1.8669999999999902,-0.9706999999999937],[-1.082999999999998,-1.717300000000009],[0.3173333333333233,-0.4853333333333296],[0,6.122666666666674],[-2.090666666666664,0],[0,-14.13066666666667],[1.99733333333333,0],[0,2.053333333333327],[-0.242666666666679,-0.3919999999999959],[1.941000000000003,-0.9707000000000079],[2.314999999999998,0],[0,0],[0,0]],"o":[[-2.912000000000006,0],[-2.277000000000015,-1.4187000000000012],[-1.306999999999988,-2.389300000000006],[0,-2.986699999999999],[1.343999999999994,-2.389300000000006],[2.277000000000015,-1.3813000000000102],[2.426999999999992,0],[1.867000000000019,0.9707000000000079],[-0.3173333333333233,0.4853333333333296],[0,-6.122666666666674],[2.090666666666664,0],[0,14.13066666666667],[-1.99733333333333,0],[0,-2.053333333333327],[0.242666666666679,0.3919999999999959],[-1.045000000000016,1.829300000000003],[-1.941000000000003,0.9706999999999937],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[57.40100000000001,43.06400000000001],[51.968999999999994,42],[48.38499999999999,38.976],[47.09700000000001,34.384],[48.161,29.960000000000008],[51.577,26.656000000000006],[57.345,24.752],[67.87299999999999,23.016000000000005],[67.87299999999999,28],[58.577,29.624],[54.881,31.248],[53.70500000000001,34.16],[54.992999999999995,37.016000000000005],[58.40899999999999,38.08],[62.88900000000001,36.96000000000001],[65.91300000000001,33.992],[66.977,29.792],[66.977,22.008],[65.297,18.36800000000001],[60.929,16.912000000000006],[56.505,18.256],[53.761,21.616],[48.496999999999986,18.98400000000001],[51.240999999999985,15.064000000000007],[55.66499999999999,12.376],[61.09700000000001,11.424000000000007],[67.42500000000001,12.768],[71.737,16.52],[73.305,22.008],[73.305,42.392],[67.257,42.392],[67.257,36.904],[68.54499999999999,37.072],[65.80099999999999,40.264],[62.04900000000001,42.336],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001]],"i":[[0,0],[1.568000000000012,0.7092999999999989],[0.8590000000000089,1.2693000000000012],[0,1.7547],[-0.7090000000000032,1.306699999999992],[-1.531000000000006,0.8960000000000008],[-2.314999999999998,0.3733000000000004],[-3.509333333333331,0.5786666666666633],[0,-1.661333333333332],[3.098666666666674,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8590000000000089,-0.7467000000000041],[-1.381,0],[-1.269000000000005,0.7466999999999899],[-0.7090000000000032,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829000000000008,0],[1.269000000000005,-0.8960000000000008],[0.5970000000000084,-1.381299999999996],[1.754666666666679,0.8773333333333255],[-1.268999999999977,1.11999999999999],[-1.680000000000007,0.6346999999999952],[-1.903999999999996,0],[-1.829000000000008,-0.8960000000000008],[-1.0080000000000098,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999991,0],[0,1.829333333333338],[-0.429333333333318,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.716999999999985,0],[0,0],[0,0]],"o":[[-2.053000000000026,0],[-1.531000000000006,-0.7467000000000041],[-0.8589999999999804,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999857,-1.306700000000006],[1.531000000000006,-0.8960000000000008],[3.509333333333331,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666674,0.541333333333327],[-1.680000000000007,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.896000000000015,0.7092999999999989],[1.717000000000013,0],[1.306999999999988,-0.7467000000000041],[0.7089999999999748,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.082999999999998,-0.9707000000000079],[-1.680000000000007,0],[-1.2319999999999993,0.8586999999999989],[-1.754666666666679,-0.8773333333333255],[0.5600000000000023,-1.493300000000005],[1.269000000000005,-1.157300000000006],[1.717000000000013,-0.6347000000000094],[2.388999999999982,0],[1.86699999999999,0.8960000000000008],[1.045000000000016,1.568000000000012],[0,6.794666666666672],[-2.015999999999991,0],[0,-1.829333333333338],[0.429333333333318,0.05599999999999739],[-0.7089999999999748,1.2319999999999993],[-1.082999999999998,0.8960000000000008],[-1.381,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.159999999999997,0.6720000000000041],[21.84,22.400000000000006],[18.75999999999999,22.400000000000006],[34.16,0.6720000000000041],[40.31999999999999,0.6720000000000041],[40.31999999999999,42.392],[33.768,42.392],[33.768,8.456000000000003],[36.232,9.128],[20.49600000000001,30.632000000000005],[19.824000000000012,30.632000000000005],[4.424000000000007,9.128],[6.608000000000004,8.456000000000003],[6.608000000000004,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.053333333333342,0],[-5.226666666666659,-7.242666666666665],[1.026666666666671,0],[-5.133333333333326,7.242666666666672],[-2.053333333333342,0],[0,-13.90666666666667],[2.183999999999997,0],[0,11.312],[-0.8213333333333424,-0.2240000000000038],[5.245333333333321,-7.168000000000006],[0.2239999999999895,0],[5.133333333333326,7.168000000000006],[-0.7280000000000086,0.2240000000000038],[0,-11.312],[2.202666666666659,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.053333333333342,0],[5.226666666666659,7.242666666666672],[-1.026666666666671,0],[5.133333333333326,-7.242666666666665],[2.053333333333342,0],[0,13.90666666666666],[-2.183999999999997,0],[0,-11.312],[0.8213333333333424,0.2240000000000038],[-5.245333333333321,7.168000000000006],[-0.2239999999999895,0],[-5.133333333333326,-7.168000000000006],[0.7280000000000086,-0.2240000000000038],[0,11.312],[-2.202666666666659,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[2.91259765625,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[702.6863719370097,144],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":72,"ix":2}},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[56.54167175292969,-0.000022762338630855083],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":80,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[122.0000003294881,25.00000012138912],"ix":2},"a":{"a":0,"k":[56.54167175292969,-0.00002288818359375],"ix":2},"s":{"a":0,"k":[34.403572049765366,34.403572049765366],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":668,"st":0,"bm":0},{"ddd":0,"ind":1,"ty":4,"nm":"Line (Group)","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[3.475,0.875],[-0.5,-3.292]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[-4.9,0.875],[-0.5,-3.292],[-0.528,9.25]],"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":4,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[9.5,12.125],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[27,24.984],"ix":2},"a":{"a":0,"k":[9.5,12.734],"ix":2},"s":{"a":1,"k":[{"t":25,"s":[0,0],"i":{"x":[0.658],"y":[1]},"o":{"x":[0.461],"y":[0]}},{"t":53,"s":[134,134],"i":{"x":[0.343],"y":[1]},"o":{"x":[0.41],"y":[0]}},{"t":75,"s":[100,100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":1,"k":[{"t":103,"s":[106.9561767578125,94.498046875],"i":{"x":[0.537],"y":[1]},"o":{"x":[0.457],"y":[0]},"ti":[0,0],"to":[-7.5,0]},{"t":223,"s":[61.9561767578125,94.498046875],"i":{"x":[0.565],"y":[0.988]},"o":{"x":[0.452],"y":[0]},"ti":[-7.5,0],"to":[0,0]},{"t":343,"s":[106.9561767578125,94.498046875],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":3,"s":[0,0],"i":{"x":[0.658],"y":[1]},"o":{"x":[0.461],"y":[0]}},{"t":47,"s":[231.297699213028,231.297699213028],"i":{"x":[0.598],"y":[1]},"o":{"x":[0.488],"y":[0]}},{"t":80,"s":[139.8142808675766,139.8142808675766],"i":{"x":[0.49],"y":[1]},"o":{"x":[0.392],"y":[0]}},{"t":103,"s":[172.61022329330444,172.61022329330444],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":668,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Line","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[17,0],[0,17],[-17,0],[0,-17],[17,0]],"i":[[0,0],[9.389,0],[0,9.389],[-9.389,0],[0,-9.389]],"o":[[0,9.389],[-9.389,0],[0,-9.389],[9.389,0],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":6,"ix":2},"lc":2,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[27,27],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":1,"k":[{"t":103,"s":[106.9561767578125,94.498046875],"i":{"x":[0.537],"y":[1]},"o":{"x":[0.457],"y":[0]},"ti":[0,0],"to":[-7.5,0]},{"t":223,"s":[61.9561767578125,94.498046875],"i":{"x":[0.565],"y":[0.988]},"o":{"x":[0.452],"y":[0]},"ti":[-7.5,0],"to":[0,0]},{"t":343,"s":[106.9561767578125,94.498046875],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":3,"s":[0,0],"i":{"x":[0.658],"y":[1]},"o":{"x":[0.461],"y":[0]}},{"t":47,"s":[231.297699213028,231.297699213028],"i":{"x":[0.598],"y":[1]},"o":{"x":[0.488],"y":[0]}},{"t":80,"s":[139.8142808675766,139.8142808675766],"i":{"x":[0.49],"y":[1]},"o":{"x":[0.392],"y":[0]}},{"t":103,"s":[172.61022329330444,172.61022329330444],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":668,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Line","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":"Line","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[-27.40187177921811,4.543100883332414],[-4.315255398302065,-18.988849854688404],[21.47271086195107,5.7461940883790295],[-4.3014465810274976,30.26202305721272],[-89.75731228468294,30.20506168595513]],"i":[[0,0],[-9.600580210142434,0.32450720595231525],[-0.0034522043186416516,-15.348500400680782],[18.757552165339415,-0.16397970513547847],[2.825629234808192,-0.01553491943388743]],"o":[[0,-16.734560434615407],[13.503297192366823,-0.4556909700606981],[0.0017261021593208258,9.40898287045782],[-2.3664860604288522,0.02071322591184991],[0,0]]}}},{"ty":"tm","s":{"a":1,"k":[{"t":73,"s":[36],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":100,"s":[21],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"e":{"a":1,"k":[{"t":73,"s":[36],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":100,"s":[57],"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":103,"s":[57],"i":{"x":[0.647],"y":[1]},"o":{"x":[0.336],"y":[0]}},{"t":223,"s":[95],"i":{"x":[0.653],"y":[1.011]},"o":{"x":[0.353],"y":[0]}},{"t":343,"s":[57],"i":{"x":[0.647],"y":[1]},"o":{"x":[0.336],"y":[0]}},{"t":463,"s":[95],"i":{"x":[0.67],"y":[1]},"o":{"x":[0.369],"y":[0]}},{"t":583,"s":[57],"i":{"x":[0.647],"y":[1]},"o":{"x":[0.336],"y":[0]}},{"t":703,"s":[95],"i":{"x":[0.715],"y":[0.99]},"o":{"x":[0.369],"y":[0]}},{"t":833,"s":[57],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":0,"k":-57,"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":8,"ix":2},"lc":2,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[169.95892333984375,104.85466003417969],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":668,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Line","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":"Line","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[-7.762281410465753,-27.947320061563488],[-21.576276991510323,-31.39434607372718],[-52.64611585928519,-0.0017261021593208258],[-21.576276991510323,31.39434607372718],[45.52594445208678,31.287327739849292]],"i":[[0,0],[4.962543708047374,0],[0,-17.15918156580833],[-17.15918156580833,0],[0,0]],"o":[[-4.161632306122511,-2.0695964890256704],[-17.15918156580833,0],[0,17.15918156580833],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":1,"k":[{"t":103,"s":[100],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":223,"s":[58],"i":{"x":[0.653],"y":[1]},"o":{"x":[0.322],"y":[0]}},{"t":343,"s":[100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"e":{"a":1,"k":[{"t":3,"s":[100],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":40,"s":[0],"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]}},{"t":103,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":8,"ix":2},"lc":2,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[61.2144775390625,103.66709899902344],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":668,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Line","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":"Line","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[-36.88335094036741,20.71322591184991],[4.543100883332414,-20.71322591184991],[36.88335094036741,-5.1800325801217975]],"i":[[0,0],[-22.879484121797546,0],[-7.593123398852313,-9.469396446034049]],"o":[[0,-22.879484121797546],[13.082128265492537,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":1,"k":[{"t":30,"s":[0],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":63,"s":[100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":8,"ix":2},"lc":2,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[78.67915344238281,50.26668167114258],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":668,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Line","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":"Line","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[-31.677426827855797,-3.4539304208009725],[-2.8446163585607205,-18.987123752529083],[31.677426827855797,15.534919433887433],[31.508268816242357,18.987123752529083]],"i":[[0,0],[-12.049919174218685,0],[0,-19.06652445185784],[0.11219664035585368,-1.1357752208331036]],"o":[[6.175993526049915,-9.358925907837518],[19.06652445185784,0],[0,1.1651189575415575],[0,0]]}}},{"ty":"tm","s":{"a":1,"k":[{"t":57,"s":[22],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":83,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"e":{"a":1,"k":[{"t":57,"s":[22],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0.223]}},{"t":83,"s":[100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":8,"ix":2},"lc":2,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[137.41671752929688,64.291259765625],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":668,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Line","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":"Line","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[14.544136794437279,1.0287568869552122],[-14.544136794437279,11.053958228290568]],"i":[[0,0],[8.373321574865326,-22.107916456581137]],"o":[[0,0],[0,0]]}}},{"ty":"tm","s":{"a":1,"k":[{"t":33,"s":[50],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":57,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"e":{"a":1,"k":[{"t":33,"s":[50],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":57,"s":[100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":4,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[61.34219741821289,102.09980773925781],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":668,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Line","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":"Line","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[14.544136794437279,1.0287568869552122],[-14.544136794437279,11.053958228290568]],"i":[[0,0],[8.373321574865326,-22.107916456581137]],"o":[[0,0],[0,0]]}}},{"ty":"tm","s":{"a":1,"k":[{"t":77,"s":[50],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":97,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"e":{"a":1,"k":[{"t":77,"s":[50],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":97,"s":[100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":4,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[109.67306518554688,72.75605773925781],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":668,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/frontend/public/lotties/groups.json b/frontend/public/lotties/groups.json new file mode 100644 index 000000000..553407502 --- /dev/null +++ b/frontend/public/lotties/groups.json @@ -0,0 +1 @@ +{"v":"5.7.5","fr":100,"ip":0,"op":102,"w":500,"h":500,"nm":"Comp 1","ddd":0,"metadata":{},"assets":[{"id":"0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"1","w":418,"h":397,"ind":2,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[41,62],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"h":1},{"t":2,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":1},{"ddd":0,"ind":3,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"2","w":418,"h":397,"ind":4,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[41,62],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":100,"s":[100],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":3},{"ddd":0,"ind":5,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"3","w":344,"h":210,"ind":6,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[78,292],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":100,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":5},{"ddd":0,"ind":7,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"4","w":210,"h":342,"ind":8,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[145,115],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":100,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":7},{"ddd":0,"ind":9,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"5","w":217,"h":258,"ind":10,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[-3,138],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":100,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":9},{"ddd":0,"ind":11,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"6","w":210,"h":326,"ind":12,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[46,-3],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":100,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":11},{"ddd":0,"ind":13,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"7","w":210,"h":327,"ind":14,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[239,-3],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":100,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":13},{"ddd":0,"ind":15,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"8","w":216,"h":260,"ind":16,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[286,137],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":100,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":15}]},{"id":"1","layers":[{"ddd":0,"ind":17,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-41,-62],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[46.875,52.108],[-46.875,52.108],[-62.526,36.458],[-46.875,20.809],[30.935,20.809],[-15.625,-20.809],[-46.875,-20.809],[-62.526,-36.458],[-46.875,-52.108],[-15.625,-52.108],[62.526,26.042],[62.526,36.458],[46.875,52.108]],"i":[[0,0],[0,0],[0,8.643],[-8.644,0],[0,0],[24.064,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-43.092],[0,0],[8.644,0]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[-2.61,-23.379],[0,0],[-8.644,0],[0,-8.643],[0,0],[43.092,0],[0,0],[0,8.643],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[395.837,270.826],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Group 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[46.875,52.108],[-46.875,52.108],[-62.526,36.458],[-62.526,26.042],[15.625,-52.108],[46.875,-52.108],[62.526,-36.458],[46.875,-20.809],[15.625,-20.809],[-30.935,20.809],[46.875,20.809],[62.526,36.458],[46.875,52.108]],"i":[[0,0],[0,0],[0,8.643],[0,0],[-43.093,0],[0,0],[0,-8.643],[8.644,0],[0,0],[2.61,-23.379],[0,0],[0,-8.643],[8.644,0]],"o":[[0,0],[-8.644,0],[0,0],[0,-43.092],[0,0],[8.644,0],[0,8.643],[0,0],[-24.064,0],[0,0],[8.644,0],[0,8.643],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[104.169,270.829],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","nm":"Group 3","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-93.435,20.81],[93.435,20.81],[46.874,-20.81],[-46.875,-20.81],[-93.435,20.81]],"i":[[0,0],[0,0],[24.066,0],[0,0],[2.608,-23.381]],"o":[[0,0],[-2.608,-23.381],[0,0],[-24.065,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[109.375,52.11],[-109.375,52.11],[-125.025,36.46],[-125.025,26.041],[-46.875,-52.11],[46.874,-52.11],[125.025,26.041],[125.025,36.46],[109.375,52.11]],"i":[[0,0],[0,0],[0,8.643],[0,0],[-43.092,0],[0,0],[0,-43.092],[0,0],[8.644,0]],"o":[[0,0],[-8.644,0],[0,0],[0,-43.092],[0,0],[43.093,0],[0,0],[0,8.643],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.004,406.242],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","nm":"Group 4","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-3.19,-36.434],[-26.017,-13.607],[-26.017,13.608],[-3.19,36.434],[26.017,36.434],[26.017,-13.607],[3.191,-36.434],[-3.19,-36.434]],"i":[[0,0],[0,-12.586],[0,0],[-12.587,0],[0,0],[0,0],[12.586,0],[0,0]],"o":[[-12.587,0],[0,0],[0,12.586],[0,0],[0,0],[0,-12.586],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[41.667,67.733],[-3.19,67.733],[-57.317,13.608],[-57.317,-13.607],[-3.19,-67.733],[3.191,-67.733],[57.317,-13.607],[57.317,52.083],[41.667,67.733]],"i":[[0,0],[0,0],[0,29.845],[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0],[8.644,0]],"o":[[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0],[29.845,0],[0,0],[0,8.643],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.003,265.62],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","nm":"Group 5","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-3.19,-36.434],[-26.017,-13.607],[-26.017,13.608],[-3.19,36.434],[26.017,36.434],[26.017,-13.607],[3.191,-36.434],[-3.19,-36.434]],"i":[[0,0],[0,-12.586],[0,0],[-12.587,0],[0,0],[0,0],[12.586,0],[0,0]],"o":[[-12.587,0],[0,0],[0,12.586],[0,0],[0,0],[0,-12.586],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[41.667,67.734],[-3.19,67.734],[-57.317,13.608],[-57.317,-13.607],[-3.19,-67.734],[3.191,-67.734],[57.317,-13.607],[57.317,52.084],[41.667,67.734]],"i":[[0,0],[0,0],[0,29.845],[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0],[8.644,0]],"o":[[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0],[29.845,0],[0,0],[0,8.643],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[151.044,130.202],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","nm":"Group 6","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-3.19,-36.434],[-26.016,-13.607],[-26.016,13.608],[-3.19,36.434],[26.016,36.434],[26.016,-13.607],[3.192,-36.434],[-3.19,-36.434]],"i":[[0,0],[0,-12.586],[0,0],[-12.587,0],[0,0],[0,0],[12.586,0],[0,0]],"o":[[-12.587,0],[0,0],[0,12.586],[0,0],[0,0],[0,-12.586],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[41.667,67.734],[-3.19,67.734],[-57.317,13.608],[-57.317,-13.607],[-3.19,-67.734],[3.192,-67.734],[57.317,-13.607],[57.317,52.084],[41.667,67.734]],"i":[[0,0],[0,0],[0,29.845],[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0],[8.644,0]],"o":[[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0],[29.845,0],[0,0],[0,8.643],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[343.753,130.202],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.003,260.41],"ix":2},"a":{"a":0,"k":[250.003,260.41],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"2","layers":[{"ddd":0,"ind":18,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-41,-62],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[46.875,52.108],[-46.875,52.108],[-62.526,36.458],[-46.875,20.809],[30.935,20.809],[-15.625,-20.809],[-46.875,-20.809],[-62.526,-36.458],[-46.875,-52.108],[-15.625,-52.108],[62.526,26.042],[62.526,36.458],[46.875,52.108]],"i":[[0,0],[0,0],[0,8.643],[-8.644,0],[0,0],[24.064,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-43.092],[0,0],[8.644,0]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[-2.61,-23.379],[0,0],[-8.644,0],[0,-8.643],[0,0],[43.092,0],[0,0],[0,8.643],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[395.837,270.826],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Group 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[46.875,52.108],[-46.875,52.108],[-62.526,36.458],[-62.526,26.042],[15.625,-52.108],[46.875,-52.108],[62.526,-36.458],[46.875,-20.809],[15.625,-20.809],[-30.935,20.809],[46.875,20.809],[62.526,36.458],[46.875,52.108]],"i":[[0,0],[0,0],[0,8.643],[0,0],[-43.093,0],[0,0],[0,-8.643],[8.644,0],[0,0],[2.61,-23.379],[0,0],[0,-8.643],[8.644,0]],"o":[[0,0],[-8.644,0],[0,0],[0,-43.092],[0,0],[8.644,0],[0,8.643],[0,0],[-24.064,0],[0,0],[8.644,0],[0,8.643],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[104.169,270.829],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","nm":"Group 3","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-93.435,20.81],[93.435,20.81],[46.874,-20.81],[-46.875,-20.81],[-93.435,20.81]],"i":[[0,0],[0,0],[24.066,0],[0,0],[2.608,-23.381]],"o":[[0,0],[-2.608,-23.381],[0,0],[-24.065,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[109.375,52.11],[-109.375,52.11],[-125.025,36.46],[-125.025,26.041],[-46.875,-52.11],[46.874,-52.11],[125.025,26.041],[125.025,36.46],[109.375,52.11]],"i":[[0,0],[0,0],[0,8.643],[0,0],[-43.092,0],[0,0],[0,-43.092],[0,0],[8.644,0]],"o":[[0,0],[-8.644,0],[0,0],[0,-43.092],[0,0],[43.093,0],[0,0],[0,8.643],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.004,406.242],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","nm":"Group 4","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-3.19,-36.434],[-26.017,-13.607],[-26.017,13.608],[-3.19,36.434],[26.017,36.434],[26.017,-13.607],[3.191,-36.434],[-3.19,-36.434]],"i":[[0,0],[0,-12.586],[0,0],[-12.587,0],[0,0],[0,0],[12.586,0],[0,0]],"o":[[-12.587,0],[0,0],[0,12.586],[0,0],[0,0],[0,-12.586],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[41.667,67.733],[-3.19,67.733],[-57.317,13.608],[-57.317,-13.607],[-3.19,-67.733],[3.191,-67.733],[57.317,-13.607],[57.317,52.083],[41.667,67.733]],"i":[[0,0],[0,0],[0,29.845],[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0],[8.644,0]],"o":[[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0],[29.845,0],[0,0],[0,8.643],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.003,265.62],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","nm":"Group 5","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-3.19,-36.434],[-26.017,-13.607],[-26.017,13.608],[-3.19,36.434],[26.017,36.434],[26.017,-13.607],[3.191,-36.434],[-3.19,-36.434]],"i":[[0,0],[0,-12.586],[0,0],[-12.587,0],[0,0],[0,0],[12.586,0],[0,0]],"o":[[-12.587,0],[0,0],[0,12.586],[0,0],[0,0],[0,-12.586],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[41.667,67.734],[-3.19,67.734],[-57.317,13.608],[-57.317,-13.607],[-3.19,-67.734],[3.191,-67.734],[57.317,-13.607],[57.317,52.084],[41.667,67.734]],"i":[[0,0],[0,0],[0,29.845],[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0],[8.644,0]],"o":[[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0],[29.845,0],[0,0],[0,8.643],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[151.044,130.202],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","nm":"Group 6","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-3.19,-36.434],[-26.016,-13.607],[-26.016,13.608],[-3.19,36.434],[26.016,36.434],[26.016,-13.607],[3.192,-36.434],[-3.19,-36.434]],"i":[[0,0],[0,-12.586],[0,0],[-12.587,0],[0,0],[0,0],[12.586,0],[0,0]],"o":[[-12.587,0],[0,0],[0,12.586],[0,0],[0,0],[0,-12.586],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[41.667,67.734],[-3.19,67.734],[-57.317,13.608],[-57.317,-13.607],[-3.19,-67.734],[3.192,-67.734],[57.317,-13.607],[57.317,52.084],[41.667,67.734]],"i":[[0,0],[0,0],[0,29.845],[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0],[8.644,0]],"o":[[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0],[29.845,0],[0,0],[0,8.643],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[343.753,130.202],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.003,260.41],"ix":2},"a":{"a":0,"k":[250.003,260.41],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"3","layers":[{"ddd":0,"ind":19,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-78,-292],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":true,"v":[[46.875,-36.46],[-46.875,-36.46],[-109.375,26.041],[-109.375,36.46],[109.375,36.46],[109.375,26.041],[46.875,-36.46]],"i":[[0,0],[0,0],[0,-34.517],[0,0],[0,0],[0,0],[34.518,0]],"o":[[0,0],[-34.518,0],[0,0],[0,0],[0,0],[0,-34.517],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]}},{"t":23,"s":[{"c":true,"v":[[67.784,-10.067],[-67.839,-10.067],[-109.375,31.469],[-109.375,47.459],[109.375,47.459],[109.375,31.524],[67.784,-10.067]],"i":[[0,0],[0,0],[0,-22.939],[0,0],[0,0],[0,0],[22.97,0]],"o":[[0,0],[-22.94,0],[0,0],[0,0],[0,0],[0,-22.969],[0,0]]}],"i":{"x":[0.413],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":50,"s":[{"c":true,"v":[[46.875,-51.46],[-46.875,-51.46],[-109.375,11.04],[-109.375,34.46],[109.375,34.46],[109.375,11.04],[46.875,-51.46]],"i":[[0,0],[0,0],[0,-34.517],[0,0],[0,0],[0,0],[34.518,0]],"o":[[0,0],[-34.518,0],[0,0],[0,0],[0,0],[0,-34.517],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":71,"s":[{"c":true,"v":[[46.875,-36.46],[-46.875,-36.46],[-109.375,26.041],[-109.375,36.46],[109.375,36.46],[109.375,26.041],[46.875,-36.46]],"i":[[0,0],[0,0],[0,-34.517],[0,0],[0,0],[0,0],[34.518,0]],"o":[[0,0],[-34.518,0],[0,0],[0,0],[0,0],[0,-34.517],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[250.00399780273438,406.24200439453125],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"4","layers":[{"ddd":0,"ind":20,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-145,-115],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[3.192,-52.084],[-3.19,-52.084],[-41.667,-13.607],[-41.667,13.608],[-3.19,52.084],[41.667,52.084],[41.667,-13.607],[3.192,-52.084]],"i":[[0,0],[0,0],[0,-21.251],[0,0],[-21.249,0],[0,0],[0,0],[21.249,0]],"o":[[0,0],[-21.249,0],[0,0],[0,21.249],[0,0],[0,0],[0,-21.251],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":1,"k":[{"t":0,"s":[250.003,265.62],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]}},{"t":23,"s":[250.003,341.327],"i":{"x":[0.572],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":50,"s":[250.003,230.62],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":71,"s":[250.003,287.62],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":90,"s":[250.003,265.62],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"5","layers":[{"ddd":0,"ind":21,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[3,-138],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[46.875,36.459],[-46.875,36.459],[-46.875,26.042],[15.625,-36.459],[46.875,-36.459]],"i":[[0,0],[0,0],[0,0],[-34.583,0],[0,0]],"o":[[0,0],[0,0],[0,-34.584],[0,0],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]}},{"t":28,"s":[{"c":false,"v":[[46.921,62.457],[-46.829,62.457],[-46.829,33.453],[-3.099,-10.277],[46.921,-10.277]],"i":[[0,0],[0,0],[0,0],[-24.197,0],[0,0]],"o":[[0,0],[0,0],[0,-24.198],[0,0],[0,0]]}],"i":{"x":[0.413],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":55,"s":[{"c":false,"v":[[46.875,36.459],[-46.875,36.459],[-46.875,-6.958],[15.625,-69.459],[46.875,-69.459]],"i":[[0,0],[0,0],[0,0],[-34.583,0],[0,0]],"o":[[0,0],[0,0],[0,-34.584],[0,0],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":76,"s":[{"c":false,"v":[[46.875,36.459],[-46.875,36.459],[-46.875,26.042],[15.625,-36.459],[46.875,-36.459]],"i":[[0,0],[0,0],[0,0],[-34.583,0],[0,0]],"o":[[0,0],[0,0],[0,-34.584],[0,0],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":95,"s":[{"c":false,"v":[[46.875,36.459],[-46.875,36.459],[-46.875,26.042],[15.625,-36.459],[46.875,-36.459]],"i":[[0,0],[0,0],[0,0],[-34.583,0],[0,0]],"o":[[0,0],[0,0],[0,-34.584],[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[104.16999816894531,270.8290100097656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"6","layers":[{"ddd":0,"ind":22,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-46,3],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[3.192,-52.084],[-3.19,-52.084],[-41.667,-13.607],[-41.667,13.608],[-3.19,52.084],[41.667,52.084],[41.667,-13.607],[3.192,-52.084]],"i":[[0,0],[0,0],[0,-21.251],[0,0],[-21.249,0],[0,0],[0,0],[21.249,0]],"o":[[0,0],[-21.249,0],[0,0],[0,21.249],[0,0],[0,0],[0,-21.251],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":1,"k":[{"t":0,"s":[151.044,130.202],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]}},{"t":28,"s":[151.044,207.541],"i":{"x":[0.572],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":55,"s":[151.044,95.202],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":76,"s":[151.044,140.202],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":95,"s":[151.044,130.202],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"7","layers":[{"ddd":0,"ind":23,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-239,3],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[3.192,-52.084],[-3.19,-52.084],[-41.667,-13.607],[-41.667,13.608],[-3.19,52.084],[41.667,52.084],[41.667,-13.607],[3.192,-52.084]],"i":[[0,0],[0,0],[0,-21.251],[0,0],[-21.249,0],[0,0],[0,0],[21.249,0]],"o":[[0,0],[-21.249,0],[0,0],[0,21.249],[0,0],[0,0],[0,-21.251],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":1,"k":[{"t":0,"s":[343.753,130.202],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":33,"s":[343.753,209.252],"i":{"x":[0.572],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":60,"s":[343.753,95.202],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":81,"s":[343.753,141.202],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":100,"s":[343.753,130.202],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"8","layers":[{"ddd":0,"ind":24,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-286,-137],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[-46.875,36.459],[46.875,36.459],[46.875,26.042],[-15.625,-36.459],[-46.875,-36.459]],"i":[[0,0],[0,0],[0,0],[34.583,0],[0,0]],"o":[[0,0],[0,0],[0,-34.584],[0,0],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":33,"s":[{"c":false,"v":[[-46.888,62.799],[46.862,62.799],[46.661,33.165],[4.955,-8.541],[-47.09,-8.541]],"i":[[0,0],[0,0],[0,0],[23.077,0],[0,0]],"o":[[0,0],[0,0],[0,-23.078],[0,0],[0,0]]}],"i":{"x":[0.413],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":60,"s":[{"c":false,"v":[[-46.875,36.459],[46.875,36.459],[46.588,-7.958],[-15.912,-70.459],[-47.162,-70.459]],"i":[[0,0],[0,0],[0,0],[34.583,0],[0,0]],"o":[[0,0],[0,0],[0,-34.584],[0,0],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":81,"s":[{"c":false,"v":[[-46.875,36.459],[46.875,36.459],[46.875,26.042],[-15.625,-36.459],[-46.875,-36.459]],"i":[[0,0],[0,0],[0,0],[34.583,0],[0,0]],"o":[[0,0],[0,0],[0,-34.584],[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[395.8380126953125,270.82598876953125],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":12345679,"ty":4,"nm":"Group Layer 8","sr":1,"ks":{"p":{"a":0,"k":[365,464.38524590163934,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[51.229508196721305,51.229508196721305,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[220.741,37.184],[225.501,35.896],[228.749,32.36800000000001],[229.981,27.216000000000008],[228.749,22.12],[225.501,18.592],[220.741,17.304],[215.981,18.592],[212.677,22.12],[211.501,27.216000000000008],[212.677,32.36800000000001],[215.981,35.896],[220.741,37.184],[220.741,37.184],[220.741,37.184]],"i":[[0,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.382000000000062,0.8586999999999989],[1.79200000000003,0],[1.418999999999983,-0.8586999999999989],[0.8220000000000027,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.382000000000062,-0.8586999999999989],[0.8220000000000027,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.418999999999983,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[221.357,43.06400000000001],[214.917,41.608],[210.49300000000005,37.408],[211.221,36.232],[211.221,42.392],[205.173,42.392],[205.173,0],[211.501,0],[211.501,18.36800000000001],[210.49300000000005,16.912000000000006],[214.973,12.88],[221.357,11.424000000000007],[229.085,13.49600000000001],[234.51700000000005,19.152],[236.533,27.216000000000008],[234.51700000000005,35.28],[229.141,40.992],[221.357,43.06400000000001],[221.357,43.06400000000001],[221.357,43.06400000000001]],"i":[[0,0],[1.942000000000007,0.9706999999999937],[1.045999999999935,1.829300000000003],[-0.2426666666666506,0.3919999999999959],[0,-2.053333333333327],[2.015999999999963,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.122666666666674],[0.3360000000000127,0.4853333333333296],[-1.865999999999985,0.9707000000000079],[-2.38900000000001,0],[-2.277000000000044,-1.3813000000000102],[-1.30600000000004,-2.389300000000006],[0,-2.986699999999999],[1.343999999999937,-2.389300000000006],[2.27800000000002,-1.4187000000000012],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.351999999999975,0],[-1.903999999999996,-0.9707000000000079],[0.2426666666666506,-0.3919999999999959],[0,2.053333333333327],[-2.015999999999963,0],[0,-14.13066666666667],[2.109333333333325,0],[0,6.122666666666667],[-0.3360000000000127,-0.4853333333333296],[1.120000000000005,-1.717300000000009],[1.867000000000075,-0.9706999999999937],[2.875,0],[2.314999999999941,1.381299999999996],[1.343999999999937,2.389300000000006],[0,2.986699999999999],[-1.30600000000004,2.389300000000006],[-2.27699999999993,1.381299999999996],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[181.87,43.06400000000001],[176.438,42],[172.854,38.976],[171.566,34.384],[172.63,29.960000000000008],[176.046,26.656000000000006],[181.814,24.752],[192.342,23.016000000000005],[192.342,28],[183.046,29.624],[179.35,31.248],[178.174,34.16],[179.462,37.016000000000005],[182.878,38.08],[187.35799999999995,36.96000000000001],[190.38199999999995,33.992],[191.446,29.792],[191.446,22.008],[189.766,18.36800000000001],[185.398,16.912000000000006],[180.974,18.256],[178.23,21.616],[172.966,18.98400000000001],[175.71,15.064000000000007],[180.134,12.376],[185.566,11.424000000000007],[191.894,12.768],[196.206,16.52],[197.774,22.008],[197.774,42.392],[191.726,42.392],[191.726,36.904],[193.014,37.072],[190.27,40.264],[186.518,42.336],[181.87,43.06400000000001],[181.87,43.06400000000001],[181.87,43.06400000000001]],"i":[[0,0],[1.567999999999984,0.7092999999999989],[0.8589999999999236,1.2693000000000012],[0,1.7547],[-0.7089999999999463,1.306699999999992],[-1.530000000000086,0.8960000000000008],[-2.313999999999965,0.3733000000000004],[-3.509333333333302,0.5786666666666633],[0,-1.661333333333332],[3.098666666666645,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8579999999999472,-0.7467000000000041],[-1.381000000000085,0],[-1.268999999999892,0.7466999999999899],[-0.7089999999999463,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829999999999927,0],[1.269999999999982,-0.8960000000000008],[0.5979999999999563,-1.381299999999996],[1.754666666666708,0.8773333333333255],[-1.269000000000005,1.11999999999999],[-1.680000000000064,0.6346999999999952],[-1.903999999999996,0],[-1.828999999999951,-0.8960000000000008],[-1.008000000000038,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999963,0],[0,1.829333333333338],[-0.4293333333333749,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.717999999999961,0],[0,0],[0,0]],"o":[[-2.052999999999997,0],[-1.529999999999973,-0.7467000000000041],[-0.8580000000000609,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999573,-1.306700000000006],[1.530999999999949,-0.8960000000000008],[3.509333333333302,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666645,0.541333333333327],[-1.680000000000064,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.8959999999999582,0.7092999999999989],[1.717999999999961,0],[1.307000000000016,-0.7467000000000041],[0.7100000000000364,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.081999999999994,-0.9707000000000079],[-1.680000000000064,0],[-1.232000000000085,0.8586999999999989],[-1.754666666666708,-0.8773333333333255],[0.5599999999999454,-1.493300000000005],[1.269999999999982,-1.157300000000006],[1.717999999999961,-0.6347000000000094],[2.389999999999986,0],[1.866999999999962,0.8960000000000008],[1.045999999999935,1.568000000000012],[0,6.794666666666672],[-2.015999999999963,0],[0,-1.829333333333338],[0.4293333333333749,0.05599999999999739],[-0.70900000000006,1.2319999999999993],[-1.081999999999994,0.8960000000000008],[-1.380999999999972,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[159.072,42.392],[159.072,0],[165.4,0],[165.4,42.392],[159.072,42.392],[159.072,42.392],[159.072,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-14.13066666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,14.13066666666667],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[138.952,43.06400000000001],[130.888,40.992],[125.456,35.28],[123.496,27.16],[125.456,19.040000000000006],[130.832,13.49600000000001],[138.448,11.424000000000007],[144.552,12.600000000000009],[149.088,15.848],[151.888,20.49600000000001],[152.896,26.096],[152.84,27.608],[152.616,29.064000000000007],[128.48,29.064000000000007],[128.48,24.024],[149.032,24.024],[146.008,26.320000000000007],[145.616,21.448000000000008],[142.816,18.032],[138.448,16.744],[133.968,18.032],[130.944,21.616],[130.104,27.216000000000008],[130.944,32.592],[134.192,36.176],[139.008,37.464],[143.65599999999995,36.232],[146.736,33.040000000000006],[151.888,35.56],[149.088,39.42400000000001],[144.60799999999995,42.11200000000001],[138.952,43.06400000000001],[138.952,43.06400000000001],[138.952,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.307000000000016,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.836999999999989,0],[-1.79200000000003,-0.784000000000006],[-1.231999999999971,-1.381299999999996],[-0.6349999999999909,-1.754700000000014],[0,-1.978700000000003],[0.03699999999992087,-0.5227000000000004],[0.1119999999999663,-0.4480000000000075],[8.04533333333336,0],[0,1.680000000000007],[-6.850666666666712,0],[1.008000000000038,-0.7653333333333308],[0.6349999999999909,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.680000000000064,0],[1.307000000000016,-0.8586999999999989],[0.70900000000006,-1.567999999999998],[-0.1490000000000009,-2.202700000000007],[-0.7469999999999573,-1.530699999999996],[-1.380999999999972,-0.8586999999999989],[-1.79200000000003,0],[-1.268999999999892,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333386,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.755000000000109,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.315000000000055,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999937,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.277000000000044,0],[1.79200000000003,0.7839999999999918],[1.232000000000085,1.344000000000008],[0.6720000000000255,1.7547],[0,0.4852999999999952],[-0.03700000000003456,0.5227000000000004],[-8.04533333333336,0],[0,-1.680000000000007],[6.850666666666712,0],[-1.008000000000038,0.7653333333333308],[0.3729999999999336,-1.829300000000003],[-0.59699999999998,-1.456000000000003],[-1.232000000000085,-0.8586999999999989],[-1.67999999999995,0],[-1.306999999999903,0.8213000000000079],[-0.7089999999999463,1.530699999999996],[-0.1870000000000118,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418999999999983,0.8586999999999989],[1.828999999999951,0],[1.307000000000016,-0.8212999999999937],[1.717333333333386,0.8400000000000034],[-0.59699999999998,1.4187000000000012],[-1.231999999999971,1.11999999999999],[-1.716999999999985,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,7.951999999999998],[111.001,0.6720000000000041],[117.329,0.6720000000000041],[117.329,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,42.392],[111.001,12.096],[117.329,12.096],[117.329,42.392],[111.001,42.392],[111.001,42.392],[111.001,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[101.41,42.72800000000001],[94.01800000000003,40.040000000000006],[91.38599999999997,32.48],[91.38599999999997,17.808000000000007],[86.06600000000003,17.808000000000007],[86.06600000000003,12.096],[86.90599999999995,12.096],[90.21000000000004,10.864],[91.38599999999997,7.504000000000005],[91.38599999999997,5.152000000000001],[97.71400000000006,5.152000000000001],[97.71400000000006,12.096],[104.602,12.096],[104.602,17.808000000000007],[97.71400000000006,17.808000000000007],[97.71400000000006,32.2],[98.21799999999996,34.888000000000005],[99.84199999999998,36.568],[102.754,37.128],[103.76200000000006,37.072],[104.826,36.96000000000001],[104.826,42.392],[103.09,42.616],[101.41,42.72800000000001],[101.41,42.72800000000001],[101.41,42.72800000000001]],"i":[[0,0],[1.754999999999995,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333312,0],[0,1.903999999999996],[-0.2799999999999727,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999935,0],[0,-1.903999999999996],[2.295999999999935,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7469999999999573,-0.4106999999999914],[-1.19500000000005,0],[-0.3730000000000473,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6349999999999909,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333312,0],[0,-1.903999999999996],[0.2799999999999727,0],[1.419000000000096,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999935,0],[0,1.903999999999996],[-2.295999999999935,0],[0,4.797333333333327],[0,1.045299999999997],[0.3360000000000127,0.7092999999999989],[0.7470000000000709,0.3733000000000004],[0.2989999999999782,0],[0.3729999999999336,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[78.71499999999997,42.72800000000001],[71.32299999999998,40.040000000000006],[68.69100000000003,32.48],[68.69100000000003,17.808000000000007],[63.37099999999998,17.808000000000007],[63.37099999999998,12.096],[64.21100000000001,12.096],[67.51499999999999,10.864],[68.69100000000003,7.504000000000005],[68.69100000000003,5.152000000000001],[75.019,5.152000000000001],[75.019,12.096],[81.90700000000004,12.096],[81.90700000000004,17.808000000000007],[75.019,17.808000000000007],[75.019,32.2],[75.52300000000002,34.888000000000005],[77.14699999999999,36.568],[80.05900000000003,37.128],[81.06700000000001,37.072],[82.13099999999997,36.96000000000001],[82.13099999999997,42.392],[80.39499999999998,42.616],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001]],"i":[[0,0],[1.754000000000019,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7470000000000141,-0.4106999999999914],[-1.19500000000005,0],[-0.3740000000000236,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6340000000000146,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173999999999978,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418000000000006,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7460000000000377,0.3733000000000004],[0.297999999999945,0],[0.3730000000000473,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,37.184],[48.94799999999998,35.896],[52.19600000000003,32.36800000000001],[53.428,27.216000000000008],[52.19600000000003,22.12],[48.94799999999998,18.592],[44.18799999999999,17.304],[39.428,18.592],[36.12400000000002,22.12],[34.94799999999998,27.216000000000008],[36.12400000000002,32.36800000000001],[39.428,35.896],[44.18799999999999,37.184],[44.18799999999999,37.184],[44.18799999999999,37.184]],"i":[[0,0],[-1.381999999999948,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.381000000000029,0.8586999999999989],[1.79200000000003,0],[1.418000000000006,-0.8586999999999989],[0.8209999999999695,-1.493300000000005],[0,-1.904000000000011],[-0.7840000000000487,-1.5307000000000102],[-1.382000000000005,-0.8586999999999989],[-1.754999999999995,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.381000000000029,-0.8586999999999989],[0.8209999999999695,-1.5307000000000102],[0,-1.904000000000011],[-0.7840000000000487,-1.493300000000005],[-1.381999999999948,-0.8586999999999989],[-1.754999999999995,0],[-1.382000000000005,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.418000000000006,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,43.06400000000001],[36.18000000000001,40.992],[30.46800000000002,35.336],[28.33999999999997,27.216000000000008],[30.46800000000002,19.096],[36.18000000000001,13.49600000000001],[44.18799999999999,11.424000000000007],[52.19600000000003,13.49600000000001],[57.85199999999998,19.096],[59.98000000000002,27.216000000000008],[57.85199999999998,35.392],[52.139999999999986,41.048],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001]],"i":[[0,0],[2.425999999999988,1.381299999999996],[1.418000000000006,2.389300000000006],[0,3.024000000000001],[-1.41900000000004,2.352000000000004],[-2.389999999999986,1.343999999999994],[-2.949999999999989,0],[-2.352000000000032,-1.3813000000000102],[-1.381999999999948,-2.389300000000006],[0,-3.061300000000003],[1.418000000000006,-2.389299999999992],[2.38900000000001,-1.381299999999996],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.911999999999978,0],[-2.389999999999986,-1.381299999999996],[-1.41900000000004,-2.389299999999992],[0,-3.061300000000003],[1.418000000000006,-2.389300000000006],[2.38900000000001,-1.3813000000000102],[2.98599999999999,0],[2.388999999999953,1.343999999999994],[1.418000000000006,2.352000000000004],[0,3.061299999999989],[-1.418999999999983,2.389300000000006],[-2.389999999999986,1.343999999999994],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.608000000000004,0.6720000000000041],[6.608000000000004,36.512],[24.639999999999986,36.512],[24.639999999999986,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.202666666666687,0],[0,-11.94666666666666],[-6.01066666666668,0],[0,-1.959999999999994],[8.21333333333331,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.202666666666687,0],[0,11.94666666666667],[6.01066666666668,0],[0,1.959999999999994],[-8.21333333333331,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[98.08047485351562,-21.67217254638672],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[246.681,42.392],[246.681,0],[253.009,0],[253.009,18.032],[252.001,17.248],[255.585,12.936000000000007],[261.297,11.424000000000007],[267.23299999999995,12.88],[271.265,16.912000000000006],[272.721,22.792],[272.721,42.392],[266.449,42.392],[266.449,24.528000000000006],[265.553,20.664],[263.201,18.2],[259.729,17.304],[256.25699999999995,18.2],[253.849,20.664],[253.009,24.528000000000006],[253.009,42.392],[246.681,42.392],[246.681,42.392],[246.681,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.010666666666665],[0.3360000000000127,0.2613333333333259],[-1.643000000000029,0.9706999999999937],[-2.166000000000054,0],[-1.717999999999961,-0.9706999999999937],[-0.9710000000000036,-1.717300000000009],[0,-2.202699999999993],[0,-6.533333333333331],[2.090666666666721,0],[0,5.954666666666668],[0.59699999999998,1.045299999999997],[1.007999999999925,0.5600000000000023],[1.305999999999926,0],[1.045000000000073,-0.5973000000000042],[0.59699999999998,-1.082700000000003],[0,-1.493300000000005],[0,-5.954666666666668],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,6.010666666666665],[-0.3360000000000127,-0.2613333333333259],[0.7459999999999809,-1.903999999999996],[1.641999999999967,-1.0080000000000098],[2.240000000000009,0],[1.717000000000098,0.9707000000000079],[0.9700000000000273,1.717299999999994],[0,6.533333333333331],[-2.090666666666721,0],[0,-5.954666666666668],[0,-1.5307000000000102],[-0.5599999999999454,-1.082700000000003],[-1.008000000000038,-0.5973000000000042],[-1.270000000000095,0],[-1.007999999999953,0.5600000000000023],[-0.5600000000000023,1.082700000000003],[0,5.954666666666668],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[237.089,42.72800000000001],[229.697,40.040000000000006],[227.065,32.48],[227.065,17.808000000000007],[221.745,17.808000000000007],[221.745,12.096],[222.585,12.096],[225.889,10.864],[227.065,7.504000000000005],[227.065,5.152000000000001],[233.393,5.152000000000001],[233.393,12.096],[240.281,12.096],[240.281,17.808000000000007],[233.393,17.808000000000007],[233.393,32.2],[233.897,34.888000000000005],[235.521,36.568],[238.433,37.128],[239.441,37.072],[240.505,36.96000000000001],[240.505,42.392],[238.769,42.616],[237.089,42.72800000000001],[237.089,42.72800000000001],[237.089,42.72800000000001]],"i":[[0,0],[1.755000000000052,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7459999999999809,-0.4106999999999914],[-1.194000000000017,0],[-0.3729999999999905,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6350000000000477,-0.07469999999999288],[0.48599999999999,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.753999999999962,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418999999999983,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7470000000000141,0.3733000000000004],[0.2989999999999782,0],[0.3740000000000236,-0.03730000000000189],[0,1.810666666666663],[-0.5220000000000482,0.0747000000000071],[-0.6339999999999577,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,7.951999999999998],[210.259,0.6720000000000041],[216.587,0.6720000000000041],[216.587,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,42.392],[210.259,12.096],[216.587,12.096],[216.587,42.392],[210.259,42.392],[210.259,42.392],[210.259,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[169.688,42.392],[159.272,12.096],[165.992,12.096],[173.944,36.232],[171.592,36.232],[179.712,12.096],[185.48,12.096],[193.544,36.232],[191.192,36.232],[199.2,12.096],[205.92,12.096],[195.448,42.392],[189.736,42.392],[181.56,17.696],[183.632,17.696],[175.456,42.392],[169.688,42.392],[169.688,42.392],[169.688,42.392]],"i":[[0,0],[3.47199999999998,10.09866666666667],[-2.240000000000009,0],[-2.650666666666666,-8.045333333333332],[0.7839999999999918,0],[-2.706666666666649,8.045333333333332],[-1.922666666666657,0],[-2.687999999999988,-8.045333333333332],[0.7839999999999918,0],[-2.669333333333327,8.045333333333332],[-2.240000000000009,0],[3.490666666666641,-10.09866666666667],[1.903999999999996,0],[2.725333333333367,8.232],[-0.6906666666666865,0],[2.72533333333331,-8.232],[1.922666666666657,0],[0,0],[0,0]],"o":[[-3.47199999999998,-10.09866666666666],[2.240000000000009,0],[2.650666666666666,8.045333333333332],[-0.7839999999999918,0],[2.706666666666649,-8.045333333333332],[1.922666666666657,0],[2.687999999999988,8.045333333333332],[-0.7839999999999918,0],[2.669333333333327,-8.045333333333332],[2.240000000000009,0],[-3.490666666666641,10.09866666666666],[-1.903999999999996,0],[-2.725333333333367,-8.232],[0.6906666666666865,0],[-2.72533333333331,8.232],[-1.922666666666657,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[155.86146545410156,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[132.444,43.06400000000001],[124.38,40.992],[118.948,35.28],[116.988,27.16],[118.948,19.040000000000006],[124.324,13.49600000000001],[131.94,11.424000000000007],[138.044,12.600000000000009],[142.58,15.848],[145.38,20.49600000000001],[146.388,26.096],[146.332,27.608],[146.108,29.064000000000007],[121.972,29.064000000000007],[121.972,24.024],[142.524,24.024],[139.5,26.320000000000007],[139.108,21.448000000000008],[136.308,18.032],[131.94,16.744],[127.46,18.032],[124.436,21.616],[123.596,27.216000000000008],[124.436,32.592],[127.684,36.176],[132.5,37.464],[137.148,36.232],[140.228,33.040000000000006],[145.38,35.56],[142.58,39.42400000000001],[138.1,42.11200000000001],[132.444,43.06400000000001],[132.444,43.06400000000001],[132.444,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.305999999999983,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.838000000000022,0],[-1.79200000000003,-0.784000000000006],[-1.232000000000028,-1.381299999999996],[-0.6350000000000477,-1.754700000000014],[0,-1.978700000000003],[0.03699999999997772,-0.5227000000000004],[0.1120000000000232,-0.4480000000000075],[8.045333333333303,0],[0,1.680000000000007],[-6.850666666666655,0],[1.007999999999981,-0.7653333333333308],[0.6340000000000146,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.67999999999995,0],[1.305999999999983,-0.8586999999999989],[0.7090000000000032,-1.567999999999998],[-0.1499999999999773,-2.202700000000007],[-0.7470000000000141,-1.530699999999996],[-1.382000000000005,-0.8586999999999989],[-1.791999999999973,0],[-1.269999999999982,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333329,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.754000000000019,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.314999999999998,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999994,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.276999999999987,0],[1.791999999999973,0.7839999999999918],[1.231999999999971,1.344000000000008],[0.6719999999999686,1.7547],[0,0.4852999999999952],[-0.03800000000001091,0.5227000000000004],[-8.045333333333303,0],[0,-1.680000000000007],[6.850666666666655,0],[-1.007999999999981,0.7653333333333308],[0.3730000000000473,-1.829300000000003],[-0.5979999999999563,-1.456000000000003],[-1.232000000000028,-0.8586999999999989],[-1.680000000000007,0],[-1.307000000000016,0.8213000000000079],[-0.7100000000000364,1.530699999999996],[-0.186999999999955,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418000000000006,0.8586999999999989],[1.829000000000008,0],[1.305999999999983,-0.8212999999999937],[1.717333333333329,0.8400000000000034],[-0.5980000000000132,1.4187000000000012],[-1.232000000000028,1.11999999999999],[-1.718000000000018,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[95.32,37.184],[100.024,35.896],[103.328,32.36800000000001],[104.56,27.216000000000008],[103.328,22.12],[100.024,18.592],[95.32,17.304],[90.56,18.592],[87.256,22.12],[86.08000000000001,27.216000000000008],[87.256,32.36800000000001],[90.50399999999999,35.896],[95.32,37.184],[95.32,37.184],[95.32,37.184]],"i":[[0,0],[-1.381,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.820999999999998,1.493299999999991],[1.419000000000011,0.8586999999999989],[1.754999999999995,0],[1.418999999999983,-0.8586999999999989],[0.7839999999999918,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.381,-0.8586999999999989],[-1.792000000000002,0],[0,0],[0,0]],"o":[[1.754999999999995,0],[1.419000000000011,-0.8586999999999989],[0.820999999999998,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.381,-0.8586999999999989],[-1.754999999999995,0],[-1.419000000000011,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.7839999999999918,1.493299999999991],[1.419000000000011,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[94.70400000000001,43.06400000000001],[86.864,40.992],[81.43199999999999,35.28],[79.47200000000001,27.216000000000008],[81.488,19.152],[86.91999999999999,13.49600000000001],[94.648,11.424000000000007],[101.088,12.88],[105.512,16.912000000000006],[104.56,18.36800000000001],[104.56,0],[110.832,0],[110.832,42.392],[104.84,42.392],[104.84,36.232],[105.568,37.408],[101.088,41.608],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001]],"i":[[0,0],[2.314999999999998,1.381299999999996],[1.344000000000023,2.389300000000006],[0,2.986699999999999],[-1.343999999999994,2.389300000000006],[-2.276999999999987,1.381299999999996],[-2.875,0],[-1.8669999999999902,-0.9706999999999937],[-1.082999999999998,-1.717300000000009],[0.3173333333333233,-0.4853333333333296],[0,6.122666666666674],[-2.090666666666664,0],[0,-14.13066666666667],[1.99733333333333,0],[0,2.053333333333327],[-0.242666666666679,-0.3919999999999959],[1.941000000000003,-0.9707000000000079],[2.314999999999998,0],[0,0],[0,0]],"o":[[-2.912000000000006,0],[-2.277000000000015,-1.4187000000000012],[-1.306999999999988,-2.389300000000006],[0,-2.986699999999999],[1.343999999999994,-2.389300000000006],[2.277000000000015,-1.3813000000000102],[2.426999999999992,0],[1.867000000000019,0.9707000000000079],[-0.3173333333333233,0.4853333333333296],[0,-6.122666666666674],[2.090666666666664,0],[0,14.13066666666667],[-1.99733333333333,0],[0,-2.053333333333327],[0.242666666666679,0.3919999999999959],[-1.045000000000016,1.829300000000003],[-1.941000000000003,0.9706999999999937],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[57.40100000000001,43.06400000000001],[51.968999999999994,42],[48.38499999999999,38.976],[47.09700000000001,34.384],[48.161,29.960000000000008],[51.577,26.656000000000006],[57.345,24.752],[67.87299999999999,23.016000000000005],[67.87299999999999,28],[58.577,29.624],[54.881,31.248],[53.70500000000001,34.16],[54.992999999999995,37.016000000000005],[58.40899999999999,38.08],[62.88900000000001,36.96000000000001],[65.91300000000001,33.992],[66.977,29.792],[66.977,22.008],[65.297,18.36800000000001],[60.929,16.912000000000006],[56.505,18.256],[53.761,21.616],[48.496999999999986,18.98400000000001],[51.240999999999985,15.064000000000007],[55.66499999999999,12.376],[61.09700000000001,11.424000000000007],[67.42500000000001,12.768],[71.737,16.52],[73.305,22.008],[73.305,42.392],[67.257,42.392],[67.257,36.904],[68.54499999999999,37.072],[65.80099999999999,40.264],[62.04900000000001,42.336],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001]],"i":[[0,0],[1.568000000000012,0.7092999999999989],[0.8590000000000089,1.2693000000000012],[0,1.7547],[-0.7090000000000032,1.306699999999992],[-1.531000000000006,0.8960000000000008],[-2.314999999999998,0.3733000000000004],[-3.509333333333331,0.5786666666666633],[0,-1.661333333333332],[3.098666666666674,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8590000000000089,-0.7467000000000041],[-1.381,0],[-1.269000000000005,0.7466999999999899],[-0.7090000000000032,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829000000000008,0],[1.269000000000005,-0.8960000000000008],[0.5970000000000084,-1.381299999999996],[1.754666666666679,0.8773333333333255],[-1.268999999999977,1.11999999999999],[-1.680000000000007,0.6346999999999952],[-1.903999999999996,0],[-1.829000000000008,-0.8960000000000008],[-1.0080000000000098,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999991,0],[0,1.829333333333338],[-0.429333333333318,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.716999999999985,0],[0,0],[0,0]],"o":[[-2.053000000000026,0],[-1.531000000000006,-0.7467000000000041],[-0.8589999999999804,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999857,-1.306700000000006],[1.531000000000006,-0.8960000000000008],[3.509333333333331,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666674,0.541333333333327],[-1.680000000000007,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.896000000000015,0.7092999999999989],[1.717000000000013,0],[1.306999999999988,-0.7467000000000041],[0.7089999999999748,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.082999999999998,-0.9707000000000079],[-1.680000000000007,0],[-1.2319999999999993,0.8586999999999989],[-1.754666666666679,-0.8773333333333255],[0.5600000000000023,-1.493300000000005],[1.269000000000005,-1.157300000000006],[1.717000000000013,-0.6347000000000094],[2.388999999999982,0],[1.86699999999999,0.8960000000000008],[1.045000000000016,1.568000000000012],[0,6.794666666666672],[-2.015999999999991,0],[0,-1.829333333333338],[0.429333333333318,0.05599999999999739],[-0.7089999999999748,1.2319999999999993],[-1.082999999999998,0.8960000000000008],[-1.381,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.159999999999997,0.6720000000000041],[21.84,22.400000000000006],[18.75999999999999,22.400000000000006],[34.16,0.6720000000000041],[40.31999999999999,0.6720000000000041],[40.31999999999999,42.392],[33.768,42.392],[33.768,8.456000000000003],[36.232,9.128],[20.49600000000001,30.632000000000005],[19.824000000000012,30.632000000000005],[4.424000000000007,9.128],[6.608000000000004,8.456000000000003],[6.608000000000004,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.053333333333342,0],[-5.226666666666659,-7.242666666666665],[1.026666666666671,0],[-5.133333333333326,7.242666666666672],[-2.053333333333342,0],[0,-13.90666666666667],[2.183999999999997,0],[0,11.312],[-0.8213333333333424,-0.2240000000000038],[5.245333333333321,-7.168000000000006],[0.2239999999999895,0],[5.133333333333326,7.168000000000006],[-0.7280000000000086,0.2240000000000038],[0,-11.312],[2.202666666666659,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.053333333333342,0],[5.226666666666659,7.242666666666672],[-1.026666666666671,0],[5.133333333333326,-7.242666666666665],[2.053333333333342,0],[0,13.90666666666666],[-2.183999999999997,0],[0,-11.312],[0.8213333333333424,0.2240000000000038],[-5.245333333333321,7.168000000000006],[-0.2239999999999895,0],[-5.133333333333326,-7.168000000000006],[0.7280000000000086,-0.2240000000000038],[0,11.312],[-2.202666666666659,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[2.91259765625,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[702.6863719370097,144],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":72,"ix":2}},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[56.54167175292969,-0.000022762338630855083],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":80,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[122.0000003294881,25.00000012138912],"ix":2},"a":{"a":0,"k":[56.54167175292969,-0.00002288818359375],"ix":2},"s":{"a":0,"k":[34.403572049765366,34.403572049765366],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"ind":25,"ty":3,"nm":"Color & Stroke Change","sr":1,"ks":{"p":{"a":0,"k":[250,250],"ix":2},"a":{"a":0,"k":[50,50],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"0","w":500,"h":500,"ind":26,"ty":0,"nm":"hover-groups","sr":1,"ks":{"p":{"a":0,"k":[50,50],"ix":2},"a":{"a":0,"k":[250,250],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":25}],"markers":[]} \ No newline at end of file diff --git a/frontend/public/lotties/jigsaw-puzzle.json b/frontend/public/lotties/jigsaw-puzzle.json new file mode 100644 index 000000000..a43968471 --- /dev/null +++ b/frontend/public/lotties/jigsaw-puzzle.json @@ -0,0 +1 @@ +{"v":"5.7.5","fr":100,"ip":0,"op":102,"w":500,"h":500,"nm":"Comp 1","ddd":0,"metadata":{},"assets":[{"id":"0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"1","w":397,"h":397,"ind":2,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[41,41],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":100,"s":[100],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":1},{"ddd":0,"ind":3,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"2","w":397,"h":397,"ind":4,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[41,41],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"h":1},{"t":2,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":3},{"ddd":0,"ind":5,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"3","w":504,"h":503,"ind":6,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[-3,-2],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":100,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":5}]},{"id":"1","layers":[{"ddd":0,"ind":7,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-41,-41],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","nm":".primary.design","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[96.279,156.229],[151.046,156.229],[156.229,151.045],[156.229,96.486],[148.546,96.486],[83.312,31.253],[102.143,-14.683],[102.399,-14.939],[148.546,-33.98],[156.229,-33.98],[156.229,-88.539],[151.046,-93.723],[88.546,-93.723],[72.896,-109.372],[72.896,-135.414],[60.717,-164.655],[60.493,-164.878],[31.254,-177.057],[-10.388,-135.414],[-10.388,-109.372],[-26.038,-93.723],[-88.538,-93.723],[-93.722,-88.539],[-93.722,-26.039],[-109.372,-10.389],[-135.414,-10.389],[-164.654,1.79],[-164.877,2.013],[-177.056,31.253],[-135.414,72.895],[-109.372,72.895],[-93.722,88.545],[-93.722,151.045],[-88.538,156.229],[-33.98,156.229],[-33.98,148.337],[31.254,83.312],[77.19,102.142],[77.449,102.401],[96.279,148.337],[96.279,156.229]],"i":[[0,0],[0,0],[0,2.81],[0,0],[0,0],[0,35.97],[-12.144,12.432],[-0.087,0.085],[-17.194,0],[0,0],[0,0],[2.81,0],[0,0],[0,8.643],[0,0],[7.854,7.698],[0.074,0.075],[11.159,0],[0,-22.961],[0,0],[8.644,0],[0,0],[0,-2.81],[0,0],[8.644,0],[0,0],[7.697,-7.854],[0.075,-0.074],[0,-11.159],[-22.962,0],[0,0],[0,-8.643],[0,0],[-2.81,0],[0,0],[0,0],[-35.97,0],[-12.431,-12.143],[-0.085,-0.087],[0,-17.191],[0,0]],"o":[[0,0],[2.81,0],[0,0],[0,0],[-35.97,0],[0,-17.19],[0.084,-0.086],[12.563,-12.279],[0,0],[0,0],[0,-2.81],[0,0],[-8.644,0],[0,0],[0,-11.158],[-0.075,-0.073],[-7.696,-7.854],[-22.962,0],[0,0],[0,8.643],[0,0],[-2.81,0],[0,0],[0,8.643],[0,0],[-11.158,0],[-0.073,0.075],[-7.854,7.697],[0,22.961],[0,0],[8.644,0],[0,0],[0,2.81],[0,0],[0,0],[0,-35.855],[17.192,0],[0.087,0.085],[12.143,12.431],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[151.046,187.528],[80.629,187.528],[64.979,171.879],[64.979,148.337],[55.188,124.405],[31.254,114.611],[-2.68,148.337],[-2.68,171.879],[-18.33,187.528],[-88.538,187.528],[-125.022,151.045],[-125.022,104.195],[-135.414,104.195],[-208.356,31.253],[-186.9,-20.228],[-135.414,-41.689],[-125.022,-41.689],[-125.022,-88.539],[-88.538,-125.022],[-41.688,-125.022],[-41.688,-135.414],[31.254,-208.356],[82.735,-186.9],[104.196,-135.414],[104.196,-125.022],[151.046,-125.022],[187.53,-88.539],[187.53,-18.33],[171.88,-2.681],[148.546,-2.681],[124.408,7.317],[114.612,31.253],[148.546,65.187],[171.88,65.187],[187.53,80.837],[187.53,151.045],[151.046,187.528]],"i":[[0,0],[0,0],[0,8.643],[0,0],[6.315,6.532],[8.909,0],[0,-18.596],[0,0],[8.644,0],[0,0],[0,20.117],[0,0],[0,0],[0,40.22],[-13.837,13.621],[-19.586,0],[0,0],[0,0],[-20.117,0],[0,0],[0,0],[-40.221,0],[-13.62,-13.838],[0,-19.586],[0,0],[0,0],[0,-20.117],[0,0],[8.644,0],[0,0],[6.662,-6.449],[0,-8.91],[-18.711,0],[0,0],[0,-8.643],[0,0],[20.117,0]],"o":[[0,0],[-8.644,0],[0,0],[0,-8.909],[-6.531,-6.316],[-18.711,0],[0,0],[0,8.643],[0,0],[-20.117,0],[0,0],[0,0],[-40.221,0],[0,-19.584],[13.622,-13.841],[0,0],[0,0],[0,-20.117],[0,0],[0,0],[0,-40.22],[19.585,0],[13.841,13.622],[0,0],[0,0],[20.117,0],[0,0],[0,8.643],[0,0],[-8.911,0],[-6.318,6.533],[0,18.711],[0,0],[8.644,0],[0,0],[0,20.117],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[239.58700561523438,239.58599853515625],"ix":2},"a":{"a":0,"k":[-10.413002014160156,-10.41400146484375],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"2","layers":[{"ddd":0,"ind":8,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-41,-41],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","nm":".primary.design","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[96.279,156.229],[151.046,156.229],[156.229,151.045],[156.229,96.486],[148.546,96.486],[83.312,31.253],[102.143,-14.683],[102.399,-14.939],[148.546,-33.98],[156.229,-33.98],[156.229,-88.539],[151.046,-93.723],[88.546,-93.723],[72.896,-109.372],[72.896,-135.414],[60.717,-164.655],[60.493,-164.878],[31.254,-177.057],[-10.388,-135.414],[-10.388,-109.372],[-26.038,-93.723],[-88.538,-93.723],[-93.722,-88.539],[-93.722,-26.039],[-109.372,-10.389],[-135.414,-10.389],[-164.654,1.79],[-164.877,2.013],[-177.056,31.253],[-135.414,72.895],[-109.372,72.895],[-93.722,88.545],[-93.722,151.045],[-88.538,156.229],[-33.98,156.229],[-33.98,148.337],[31.254,83.312],[77.19,102.142],[77.449,102.401],[96.279,148.337],[96.279,156.229]],"i":[[0,0],[0,0],[0,2.81],[0,0],[0,0],[0,35.97],[-12.144,12.432],[-0.087,0.085],[-17.194,0],[0,0],[0,0],[2.81,0],[0,0],[0,8.643],[0,0],[7.854,7.698],[0.074,0.075],[11.159,0],[0,-22.961],[0,0],[8.644,0],[0,0],[0,-2.81],[0,0],[8.644,0],[0,0],[7.697,-7.854],[0.075,-0.074],[0,-11.159],[-22.962,0],[0,0],[0,-8.643],[0,0],[-2.81,0],[0,0],[0,0],[-35.97,0],[-12.431,-12.143],[-0.085,-0.087],[0,-17.191],[0,0]],"o":[[0,0],[2.81,0],[0,0],[0,0],[-35.97,0],[0,-17.19],[0.084,-0.086],[12.563,-12.279],[0,0],[0,0],[0,-2.81],[0,0],[-8.644,0],[0,0],[0,-11.158],[-0.075,-0.073],[-7.696,-7.854],[-22.962,0],[0,0],[0,8.643],[0,0],[-2.81,0],[0,0],[0,8.643],[0,0],[-11.158,0],[-0.073,0.075],[-7.854,7.697],[0,22.961],[0,0],[8.644,0],[0,0],[0,2.81],[0,0],[0,0],[0,-35.855],[17.192,0],[0.087,0.085],[12.143,12.431],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[151.046,187.528],[80.629,187.528],[64.979,171.879],[64.979,148.337],[55.188,124.405],[31.254,114.611],[-2.68,148.337],[-2.68,171.879],[-18.33,187.528],[-88.538,187.528],[-125.022,151.045],[-125.022,104.195],[-135.414,104.195],[-208.356,31.253],[-186.9,-20.228],[-135.414,-41.689],[-125.022,-41.689],[-125.022,-88.539],[-88.538,-125.022],[-41.688,-125.022],[-41.688,-135.414],[31.254,-208.356],[82.735,-186.9],[104.196,-135.414],[104.196,-125.022],[151.046,-125.022],[187.53,-88.539],[187.53,-18.33],[171.88,-2.681],[148.546,-2.681],[124.408,7.317],[114.612,31.253],[148.546,65.187],[171.88,65.187],[187.53,80.837],[187.53,151.045],[151.046,187.528]],"i":[[0,0],[0,0],[0,8.643],[0,0],[6.315,6.532],[8.909,0],[0,-18.596],[0,0],[8.644,0],[0,0],[0,20.117],[0,0],[0,0],[0,40.22],[-13.837,13.621],[-19.586,0],[0,0],[0,0],[-20.117,0],[0,0],[0,0],[-40.221,0],[-13.62,-13.838],[0,-19.586],[0,0],[0,0],[0,-20.117],[0,0],[8.644,0],[0,0],[6.662,-6.449],[0,-8.91],[-18.711,0],[0,0],[0,-8.643],[0,0],[20.117,0]],"o":[[0,0],[-8.644,0],[0,0],[0,-8.909],[-6.531,-6.316],[-18.711,0],[0,0],[0,8.643],[0,0],[-20.117,0],[0,0],[0,0],[-40.221,0],[0,-19.584],[13.622,-13.841],[0,0],[0,0],[0,-20.117],[0,0],[0,0],[0,-40.22],[19.585,0],[13.841,13.622],[0,0],[0,0],[20.117,0],[0,0],[0,8.643],[0,0],[-8.911,0],[-6.318,6.533],[0,18.711],[0,0],[8.644,0],[0,0],[0,20.117],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[239.58700561523438,239.58599853515625],"ix":2},"a":{"a":0,"k":[-10.413002014160156,-10.41400146484375],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"3","layers":[{"ddd":0,"ind":9,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[3,2],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":2,"s":[{"c":true,"v":[[123.751,6.667],[109.376,41.667],[158.959,91.251],[182.293,91.251],[182.293,161.459],[161.459,182.293],[91.042,182.293],[91.042,158.751],[76.667,123.751],[41.667,109.376],[-7.917,158.751],[-7.917,182.293],[-78.125,182.293],[-98.959,161.459],[-98.959,98.959],[-125.001,98.959],[-182.293,41.667],[-165.418,1.25],[-125.001,-15.625],[-98.959,-15.625],[-98.959,-78.125],[-78.125,-98.959],[-15.625,-98.959],[-15.625,-125.001],[41.667,-182.293],[82.084,-165.418],[98.959,-125.001],[98.959,-98.959],[161.459,-98.959],[182.293,-78.125],[182.293,-7.917],[158.959,-7.917],[123.751,6.667]],"i":[[0,0],[0,-13.751],[-27.5,0],[0,0],[0,0],[11.458,0],[0,0],[0,0],[8.75,8.958],[13.749,0],[0,-27.292],[0,0],[0,0],[0,11.458],[0,0],[0,0],[0,31.666],[-10.417,10.208],[-15.834,0],[0,0],[0,0],[-11.458,0],[0,0],[0,0],[-31.667,0],[-10.208,-10.417],[0,-15.834],[0,0],[0,0],[0,-11.458],[0,0],[0,0],[9.167,-8.958]],"o":[[-8.75,8.958],[0,27.292],[0,0],[0,0],[0,11.458],[0,0],[0,0],[0,-13.751],[-8.958,-8.751],[-27.292,0],[0,0],[0,0],[-11.458,0],[0,0],[0,0],[-31.667,0],[0,-15.834],[10.208,-10.417],[0,0],[0,0],[0,-11.458],[0,0],[0,0],[0,-31.667],[15.834,0],[10.417,10.208],[0,0],[0,0],[11.458,0],[0,0],[0,0],[-13.751,0],[0,0]]}],"i":{"x":[0.22],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":100,"s":[{"c":true,"v":[[123.751,6.667],[109.376,41.667],[158.959,91.251],[182.293,91.251],[182.293,161.459],[161.459,182.293],[98.711,182.293],[98.711,209.185],[82.108,249.166],[41.683,265.587],[-15.586,209.185],[-15.586,182.293],[-78.125,182.293],[-98.959,161.459],[-98.959,98.959],[-125.001,98.959],[-182.293,41.667],[-165.418,1.25],[-125.001,-15.625],[-98.959,-15.625],[-98.959,-78.125],[-78.125,-98.959],[-8.086,-98.959],[-8.086,-76.288],[41.667,-26.413],[76.765,-41.104],[91.42,-76.288],[91.42,-98.959],[161.459,-98.959],[182.293,-78.125],[182.293,-7.917],[158.959,-7.917],[123.751,6.667]],"i":[[0,0],[0,-13.751],[-27.5,0],[0,0],[0,0],[11.458,0],[0,0],[0,0],[10.106,-10.233],[15.881,0],[0,31.176],[0,0],[0,0],[0,11.458],[0,0],[0,0],[0,31.666],[-10.417,10.208],[-15.834,0],[0,0],[0,0],[-11.458,0],[0,0],[0,0],[-27.5,0],[-8.865,9.068],[0,13.784],[0,0],[0,0],[0,-11.458],[0,0],[0,0],[9.167,-8.958]],"o":[[-8.75,8.958],[0,27.292],[0,0],[0,0],[0,11.458],[0,0],[0,0],[0,15.708],[-10.347,9.996],[-31.522,0],[0,0],[0,0],[-11.458,0],[0,0],[0,0],[-31.667,0],[0,-15.834],[10.208,-10.417],[0,0],[0,0],[0,-11.458],[0,0],[0,0],[0,27.568],[13.75,0],[9.046,-8.887],[0,0],[0,0],[11.458,0],[0,0],[0,0],[-13.751,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":1,"k":[{"t":2,"s":[239.587,239.586],"i":{"x":[0.22],"y":[1]},"o":{"x":[0.333],"y":[0]},"ti":[-14,0.167],"to":[14,-0.167]},{"t":100,"s":[323.587,238.586],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":1,"k":[{"t":2,"s":[0],"i":{"x":[0.22],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":100,"s":[90],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":12345679,"ty":4,"nm":"Group Layer 8","sr":1,"ks":{"p":{"a":0,"k":[365,464.38524590163934,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[51.229508196721305,51.229508196721305,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[220.741,37.184],[225.501,35.896],[228.749,32.36800000000001],[229.981,27.216000000000008],[228.749,22.12],[225.501,18.592],[220.741,17.304],[215.981,18.592],[212.677,22.12],[211.501,27.216000000000008],[212.677,32.36800000000001],[215.981,35.896],[220.741,37.184],[220.741,37.184],[220.741,37.184]],"i":[[0,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.382000000000062,0.8586999999999989],[1.79200000000003,0],[1.418999999999983,-0.8586999999999989],[0.8220000000000027,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.382000000000062,-0.8586999999999989],[0.8220000000000027,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.418999999999983,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[221.357,43.06400000000001],[214.917,41.608],[210.49300000000005,37.408],[211.221,36.232],[211.221,42.392],[205.173,42.392],[205.173,0],[211.501,0],[211.501,18.36800000000001],[210.49300000000005,16.912000000000006],[214.973,12.88],[221.357,11.424000000000007],[229.085,13.49600000000001],[234.51700000000005,19.152],[236.533,27.216000000000008],[234.51700000000005,35.28],[229.141,40.992],[221.357,43.06400000000001],[221.357,43.06400000000001],[221.357,43.06400000000001]],"i":[[0,0],[1.942000000000007,0.9706999999999937],[1.045999999999935,1.829300000000003],[-0.2426666666666506,0.3919999999999959],[0,-2.053333333333327],[2.015999999999963,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.122666666666674],[0.3360000000000127,0.4853333333333296],[-1.865999999999985,0.9707000000000079],[-2.38900000000001,0],[-2.277000000000044,-1.3813000000000102],[-1.30600000000004,-2.389300000000006],[0,-2.986699999999999],[1.343999999999937,-2.389300000000006],[2.27800000000002,-1.4187000000000012],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.351999999999975,0],[-1.903999999999996,-0.9707000000000079],[0.2426666666666506,-0.3919999999999959],[0,2.053333333333327],[-2.015999999999963,0],[0,-14.13066666666667],[2.109333333333325,0],[0,6.122666666666667],[-0.3360000000000127,-0.4853333333333296],[1.120000000000005,-1.717300000000009],[1.867000000000075,-0.9706999999999937],[2.875,0],[2.314999999999941,1.381299999999996],[1.343999999999937,2.389300000000006],[0,2.986699999999999],[-1.30600000000004,2.389300000000006],[-2.27699999999993,1.381299999999996],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[181.87,43.06400000000001],[176.438,42],[172.854,38.976],[171.566,34.384],[172.63,29.960000000000008],[176.046,26.656000000000006],[181.814,24.752],[192.342,23.016000000000005],[192.342,28],[183.046,29.624],[179.35,31.248],[178.174,34.16],[179.462,37.016000000000005],[182.878,38.08],[187.35799999999995,36.96000000000001],[190.38199999999995,33.992],[191.446,29.792],[191.446,22.008],[189.766,18.36800000000001],[185.398,16.912000000000006],[180.974,18.256],[178.23,21.616],[172.966,18.98400000000001],[175.71,15.064000000000007],[180.134,12.376],[185.566,11.424000000000007],[191.894,12.768],[196.206,16.52],[197.774,22.008],[197.774,42.392],[191.726,42.392],[191.726,36.904],[193.014,37.072],[190.27,40.264],[186.518,42.336],[181.87,43.06400000000001],[181.87,43.06400000000001],[181.87,43.06400000000001]],"i":[[0,0],[1.567999999999984,0.7092999999999989],[0.8589999999999236,1.2693000000000012],[0,1.7547],[-0.7089999999999463,1.306699999999992],[-1.530000000000086,0.8960000000000008],[-2.313999999999965,0.3733000000000004],[-3.509333333333302,0.5786666666666633],[0,-1.661333333333332],[3.098666666666645,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8579999999999472,-0.7467000000000041],[-1.381000000000085,0],[-1.268999999999892,0.7466999999999899],[-0.7089999999999463,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829999999999927,0],[1.269999999999982,-0.8960000000000008],[0.5979999999999563,-1.381299999999996],[1.754666666666708,0.8773333333333255],[-1.269000000000005,1.11999999999999],[-1.680000000000064,0.6346999999999952],[-1.903999999999996,0],[-1.828999999999951,-0.8960000000000008],[-1.008000000000038,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999963,0],[0,1.829333333333338],[-0.4293333333333749,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.717999999999961,0],[0,0],[0,0]],"o":[[-2.052999999999997,0],[-1.529999999999973,-0.7467000000000041],[-0.8580000000000609,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999573,-1.306700000000006],[1.530999999999949,-0.8960000000000008],[3.509333333333302,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666645,0.541333333333327],[-1.680000000000064,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.8959999999999582,0.7092999999999989],[1.717999999999961,0],[1.307000000000016,-0.7467000000000041],[0.7100000000000364,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.081999999999994,-0.9707000000000079],[-1.680000000000064,0],[-1.232000000000085,0.8586999999999989],[-1.754666666666708,-0.8773333333333255],[0.5599999999999454,-1.493300000000005],[1.269999999999982,-1.157300000000006],[1.717999999999961,-0.6347000000000094],[2.389999999999986,0],[1.866999999999962,0.8960000000000008],[1.045999999999935,1.568000000000012],[0,6.794666666666672],[-2.015999999999963,0],[0,-1.829333333333338],[0.4293333333333749,0.05599999999999739],[-0.70900000000006,1.2319999999999993],[-1.081999999999994,0.8960000000000008],[-1.380999999999972,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[159.072,42.392],[159.072,0],[165.4,0],[165.4,42.392],[159.072,42.392],[159.072,42.392],[159.072,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-14.13066666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,14.13066666666667],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[138.952,43.06400000000001],[130.888,40.992],[125.456,35.28],[123.496,27.16],[125.456,19.040000000000006],[130.832,13.49600000000001],[138.448,11.424000000000007],[144.552,12.600000000000009],[149.088,15.848],[151.888,20.49600000000001],[152.896,26.096],[152.84,27.608],[152.616,29.064000000000007],[128.48,29.064000000000007],[128.48,24.024],[149.032,24.024],[146.008,26.320000000000007],[145.616,21.448000000000008],[142.816,18.032],[138.448,16.744],[133.968,18.032],[130.944,21.616],[130.104,27.216000000000008],[130.944,32.592],[134.192,36.176],[139.008,37.464],[143.65599999999995,36.232],[146.736,33.040000000000006],[151.888,35.56],[149.088,39.42400000000001],[144.60799999999995,42.11200000000001],[138.952,43.06400000000001],[138.952,43.06400000000001],[138.952,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.307000000000016,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.836999999999989,0],[-1.79200000000003,-0.784000000000006],[-1.231999999999971,-1.381299999999996],[-0.6349999999999909,-1.754700000000014],[0,-1.978700000000003],[0.03699999999992087,-0.5227000000000004],[0.1119999999999663,-0.4480000000000075],[8.04533333333336,0],[0,1.680000000000007],[-6.850666666666712,0],[1.008000000000038,-0.7653333333333308],[0.6349999999999909,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.680000000000064,0],[1.307000000000016,-0.8586999999999989],[0.70900000000006,-1.567999999999998],[-0.1490000000000009,-2.202700000000007],[-0.7469999999999573,-1.530699999999996],[-1.380999999999972,-0.8586999999999989],[-1.79200000000003,0],[-1.268999999999892,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333386,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.755000000000109,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.315000000000055,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999937,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.277000000000044,0],[1.79200000000003,0.7839999999999918],[1.232000000000085,1.344000000000008],[0.6720000000000255,1.7547],[0,0.4852999999999952],[-0.03700000000003456,0.5227000000000004],[-8.04533333333336,0],[0,-1.680000000000007],[6.850666666666712,0],[-1.008000000000038,0.7653333333333308],[0.3729999999999336,-1.829300000000003],[-0.59699999999998,-1.456000000000003],[-1.232000000000085,-0.8586999999999989],[-1.67999999999995,0],[-1.306999999999903,0.8213000000000079],[-0.7089999999999463,1.530699999999996],[-0.1870000000000118,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418999999999983,0.8586999999999989],[1.828999999999951,0],[1.307000000000016,-0.8212999999999937],[1.717333333333386,0.8400000000000034],[-0.59699999999998,1.4187000000000012],[-1.231999999999971,1.11999999999999],[-1.716999999999985,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,7.951999999999998],[111.001,0.6720000000000041],[117.329,0.6720000000000041],[117.329,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,42.392],[111.001,12.096],[117.329,12.096],[117.329,42.392],[111.001,42.392],[111.001,42.392],[111.001,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[101.41,42.72800000000001],[94.01800000000003,40.040000000000006],[91.38599999999997,32.48],[91.38599999999997,17.808000000000007],[86.06600000000003,17.808000000000007],[86.06600000000003,12.096],[86.90599999999995,12.096],[90.21000000000004,10.864],[91.38599999999997,7.504000000000005],[91.38599999999997,5.152000000000001],[97.71400000000006,5.152000000000001],[97.71400000000006,12.096],[104.602,12.096],[104.602,17.808000000000007],[97.71400000000006,17.808000000000007],[97.71400000000006,32.2],[98.21799999999996,34.888000000000005],[99.84199999999998,36.568],[102.754,37.128],[103.76200000000006,37.072],[104.826,36.96000000000001],[104.826,42.392],[103.09,42.616],[101.41,42.72800000000001],[101.41,42.72800000000001],[101.41,42.72800000000001]],"i":[[0,0],[1.754999999999995,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333312,0],[0,1.903999999999996],[-0.2799999999999727,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999935,0],[0,-1.903999999999996],[2.295999999999935,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7469999999999573,-0.4106999999999914],[-1.19500000000005,0],[-0.3730000000000473,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6349999999999909,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333312,0],[0,-1.903999999999996],[0.2799999999999727,0],[1.419000000000096,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999935,0],[0,1.903999999999996],[-2.295999999999935,0],[0,4.797333333333327],[0,1.045299999999997],[0.3360000000000127,0.7092999999999989],[0.7470000000000709,0.3733000000000004],[0.2989999999999782,0],[0.3729999999999336,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[78.71499999999997,42.72800000000001],[71.32299999999998,40.040000000000006],[68.69100000000003,32.48],[68.69100000000003,17.808000000000007],[63.37099999999998,17.808000000000007],[63.37099999999998,12.096],[64.21100000000001,12.096],[67.51499999999999,10.864],[68.69100000000003,7.504000000000005],[68.69100000000003,5.152000000000001],[75.019,5.152000000000001],[75.019,12.096],[81.90700000000004,12.096],[81.90700000000004,17.808000000000007],[75.019,17.808000000000007],[75.019,32.2],[75.52300000000002,34.888000000000005],[77.14699999999999,36.568],[80.05900000000003,37.128],[81.06700000000001,37.072],[82.13099999999997,36.96000000000001],[82.13099999999997,42.392],[80.39499999999998,42.616],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001]],"i":[[0,0],[1.754000000000019,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7470000000000141,-0.4106999999999914],[-1.19500000000005,0],[-0.3740000000000236,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6340000000000146,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173999999999978,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418000000000006,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7460000000000377,0.3733000000000004],[0.297999999999945,0],[0.3730000000000473,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,37.184],[48.94799999999998,35.896],[52.19600000000003,32.36800000000001],[53.428,27.216000000000008],[52.19600000000003,22.12],[48.94799999999998,18.592],[44.18799999999999,17.304],[39.428,18.592],[36.12400000000002,22.12],[34.94799999999998,27.216000000000008],[36.12400000000002,32.36800000000001],[39.428,35.896],[44.18799999999999,37.184],[44.18799999999999,37.184],[44.18799999999999,37.184]],"i":[[0,0],[-1.381999999999948,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.381000000000029,0.8586999999999989],[1.79200000000003,0],[1.418000000000006,-0.8586999999999989],[0.8209999999999695,-1.493300000000005],[0,-1.904000000000011],[-0.7840000000000487,-1.5307000000000102],[-1.382000000000005,-0.8586999999999989],[-1.754999999999995,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.381000000000029,-0.8586999999999989],[0.8209999999999695,-1.5307000000000102],[0,-1.904000000000011],[-0.7840000000000487,-1.493300000000005],[-1.381999999999948,-0.8586999999999989],[-1.754999999999995,0],[-1.382000000000005,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.418000000000006,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,43.06400000000001],[36.18000000000001,40.992],[30.46800000000002,35.336],[28.33999999999997,27.216000000000008],[30.46800000000002,19.096],[36.18000000000001,13.49600000000001],[44.18799999999999,11.424000000000007],[52.19600000000003,13.49600000000001],[57.85199999999998,19.096],[59.98000000000002,27.216000000000008],[57.85199999999998,35.392],[52.139999999999986,41.048],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001]],"i":[[0,0],[2.425999999999988,1.381299999999996],[1.418000000000006,2.389300000000006],[0,3.024000000000001],[-1.41900000000004,2.352000000000004],[-2.389999999999986,1.343999999999994],[-2.949999999999989,0],[-2.352000000000032,-1.3813000000000102],[-1.381999999999948,-2.389300000000006],[0,-3.061300000000003],[1.418000000000006,-2.389299999999992],[2.38900000000001,-1.381299999999996],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.911999999999978,0],[-2.389999999999986,-1.381299999999996],[-1.41900000000004,-2.389299999999992],[0,-3.061300000000003],[1.418000000000006,-2.389300000000006],[2.38900000000001,-1.3813000000000102],[2.98599999999999,0],[2.388999999999953,1.343999999999994],[1.418000000000006,2.352000000000004],[0,3.061299999999989],[-1.418999999999983,2.389300000000006],[-2.389999999999986,1.343999999999994],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.608000000000004,0.6720000000000041],[6.608000000000004,36.512],[24.639999999999986,36.512],[24.639999999999986,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.202666666666687,0],[0,-11.94666666666666],[-6.01066666666668,0],[0,-1.959999999999994],[8.21333333333331,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.202666666666687,0],[0,11.94666666666667],[6.01066666666668,0],[0,1.959999999999994],[-8.21333333333331,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[98.08047485351562,-21.67217254638672],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[246.681,42.392],[246.681,0],[253.009,0],[253.009,18.032],[252.001,17.248],[255.585,12.936000000000007],[261.297,11.424000000000007],[267.23299999999995,12.88],[271.265,16.912000000000006],[272.721,22.792],[272.721,42.392],[266.449,42.392],[266.449,24.528000000000006],[265.553,20.664],[263.201,18.2],[259.729,17.304],[256.25699999999995,18.2],[253.849,20.664],[253.009,24.528000000000006],[253.009,42.392],[246.681,42.392],[246.681,42.392],[246.681,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.010666666666665],[0.3360000000000127,0.2613333333333259],[-1.643000000000029,0.9706999999999937],[-2.166000000000054,0],[-1.717999999999961,-0.9706999999999937],[-0.9710000000000036,-1.717300000000009],[0,-2.202699999999993],[0,-6.533333333333331],[2.090666666666721,0],[0,5.954666666666668],[0.59699999999998,1.045299999999997],[1.007999999999925,0.5600000000000023],[1.305999999999926,0],[1.045000000000073,-0.5973000000000042],[0.59699999999998,-1.082700000000003],[0,-1.493300000000005],[0,-5.954666666666668],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,6.010666666666665],[-0.3360000000000127,-0.2613333333333259],[0.7459999999999809,-1.903999999999996],[1.641999999999967,-1.0080000000000098],[2.240000000000009,0],[1.717000000000098,0.9707000000000079],[0.9700000000000273,1.717299999999994],[0,6.533333333333331],[-2.090666666666721,0],[0,-5.954666666666668],[0,-1.5307000000000102],[-0.5599999999999454,-1.082700000000003],[-1.008000000000038,-0.5973000000000042],[-1.270000000000095,0],[-1.007999999999953,0.5600000000000023],[-0.5600000000000023,1.082700000000003],[0,5.954666666666668],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[237.089,42.72800000000001],[229.697,40.040000000000006],[227.065,32.48],[227.065,17.808000000000007],[221.745,17.808000000000007],[221.745,12.096],[222.585,12.096],[225.889,10.864],[227.065,7.504000000000005],[227.065,5.152000000000001],[233.393,5.152000000000001],[233.393,12.096],[240.281,12.096],[240.281,17.808000000000007],[233.393,17.808000000000007],[233.393,32.2],[233.897,34.888000000000005],[235.521,36.568],[238.433,37.128],[239.441,37.072],[240.505,36.96000000000001],[240.505,42.392],[238.769,42.616],[237.089,42.72800000000001],[237.089,42.72800000000001],[237.089,42.72800000000001]],"i":[[0,0],[1.755000000000052,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7459999999999809,-0.4106999999999914],[-1.194000000000017,0],[-0.3729999999999905,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6350000000000477,-0.07469999999999288],[0.48599999999999,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.753999999999962,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418999999999983,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7470000000000141,0.3733000000000004],[0.2989999999999782,0],[0.3740000000000236,-0.03730000000000189],[0,1.810666666666663],[-0.5220000000000482,0.0747000000000071],[-0.6339999999999577,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,7.951999999999998],[210.259,0.6720000000000041],[216.587,0.6720000000000041],[216.587,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,42.392],[210.259,12.096],[216.587,12.096],[216.587,42.392],[210.259,42.392],[210.259,42.392],[210.259,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[169.688,42.392],[159.272,12.096],[165.992,12.096],[173.944,36.232],[171.592,36.232],[179.712,12.096],[185.48,12.096],[193.544,36.232],[191.192,36.232],[199.2,12.096],[205.92,12.096],[195.448,42.392],[189.736,42.392],[181.56,17.696],[183.632,17.696],[175.456,42.392],[169.688,42.392],[169.688,42.392],[169.688,42.392]],"i":[[0,0],[3.47199999999998,10.09866666666667],[-2.240000000000009,0],[-2.650666666666666,-8.045333333333332],[0.7839999999999918,0],[-2.706666666666649,8.045333333333332],[-1.922666666666657,0],[-2.687999999999988,-8.045333333333332],[0.7839999999999918,0],[-2.669333333333327,8.045333333333332],[-2.240000000000009,0],[3.490666666666641,-10.09866666666667],[1.903999999999996,0],[2.725333333333367,8.232],[-0.6906666666666865,0],[2.72533333333331,-8.232],[1.922666666666657,0],[0,0],[0,0]],"o":[[-3.47199999999998,-10.09866666666666],[2.240000000000009,0],[2.650666666666666,8.045333333333332],[-0.7839999999999918,0],[2.706666666666649,-8.045333333333332],[1.922666666666657,0],[2.687999999999988,8.045333333333332],[-0.7839999999999918,0],[2.669333333333327,-8.045333333333332],[2.240000000000009,0],[-3.490666666666641,10.09866666666666],[-1.903999999999996,0],[-2.725333333333367,-8.232],[0.6906666666666865,0],[-2.72533333333331,8.232],[-1.922666666666657,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[155.86146545410156,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[132.444,43.06400000000001],[124.38,40.992],[118.948,35.28],[116.988,27.16],[118.948,19.040000000000006],[124.324,13.49600000000001],[131.94,11.424000000000007],[138.044,12.600000000000009],[142.58,15.848],[145.38,20.49600000000001],[146.388,26.096],[146.332,27.608],[146.108,29.064000000000007],[121.972,29.064000000000007],[121.972,24.024],[142.524,24.024],[139.5,26.320000000000007],[139.108,21.448000000000008],[136.308,18.032],[131.94,16.744],[127.46,18.032],[124.436,21.616],[123.596,27.216000000000008],[124.436,32.592],[127.684,36.176],[132.5,37.464],[137.148,36.232],[140.228,33.040000000000006],[145.38,35.56],[142.58,39.42400000000001],[138.1,42.11200000000001],[132.444,43.06400000000001],[132.444,43.06400000000001],[132.444,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.305999999999983,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.838000000000022,0],[-1.79200000000003,-0.784000000000006],[-1.232000000000028,-1.381299999999996],[-0.6350000000000477,-1.754700000000014],[0,-1.978700000000003],[0.03699999999997772,-0.5227000000000004],[0.1120000000000232,-0.4480000000000075],[8.045333333333303,0],[0,1.680000000000007],[-6.850666666666655,0],[1.007999999999981,-0.7653333333333308],[0.6340000000000146,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.67999999999995,0],[1.305999999999983,-0.8586999999999989],[0.7090000000000032,-1.567999999999998],[-0.1499999999999773,-2.202700000000007],[-0.7470000000000141,-1.530699999999996],[-1.382000000000005,-0.8586999999999989],[-1.791999999999973,0],[-1.269999999999982,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333329,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.754000000000019,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.314999999999998,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999994,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.276999999999987,0],[1.791999999999973,0.7839999999999918],[1.231999999999971,1.344000000000008],[0.6719999999999686,1.7547],[0,0.4852999999999952],[-0.03800000000001091,0.5227000000000004],[-8.045333333333303,0],[0,-1.680000000000007],[6.850666666666655,0],[-1.007999999999981,0.7653333333333308],[0.3730000000000473,-1.829300000000003],[-0.5979999999999563,-1.456000000000003],[-1.232000000000028,-0.8586999999999989],[-1.680000000000007,0],[-1.307000000000016,0.8213000000000079],[-0.7100000000000364,1.530699999999996],[-0.186999999999955,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418000000000006,0.8586999999999989],[1.829000000000008,0],[1.305999999999983,-0.8212999999999937],[1.717333333333329,0.8400000000000034],[-0.5980000000000132,1.4187000000000012],[-1.232000000000028,1.11999999999999],[-1.718000000000018,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[95.32,37.184],[100.024,35.896],[103.328,32.36800000000001],[104.56,27.216000000000008],[103.328,22.12],[100.024,18.592],[95.32,17.304],[90.56,18.592],[87.256,22.12],[86.08000000000001,27.216000000000008],[87.256,32.36800000000001],[90.50399999999999,35.896],[95.32,37.184],[95.32,37.184],[95.32,37.184]],"i":[[0,0],[-1.381,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.820999999999998,1.493299999999991],[1.419000000000011,0.8586999999999989],[1.754999999999995,0],[1.418999999999983,-0.8586999999999989],[0.7839999999999918,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.381,-0.8586999999999989],[-1.792000000000002,0],[0,0],[0,0]],"o":[[1.754999999999995,0],[1.419000000000011,-0.8586999999999989],[0.820999999999998,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.381,-0.8586999999999989],[-1.754999999999995,0],[-1.419000000000011,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.7839999999999918,1.493299999999991],[1.419000000000011,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[94.70400000000001,43.06400000000001],[86.864,40.992],[81.43199999999999,35.28],[79.47200000000001,27.216000000000008],[81.488,19.152],[86.91999999999999,13.49600000000001],[94.648,11.424000000000007],[101.088,12.88],[105.512,16.912000000000006],[104.56,18.36800000000001],[104.56,0],[110.832,0],[110.832,42.392],[104.84,42.392],[104.84,36.232],[105.568,37.408],[101.088,41.608],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001]],"i":[[0,0],[2.314999999999998,1.381299999999996],[1.344000000000023,2.389300000000006],[0,2.986699999999999],[-1.343999999999994,2.389300000000006],[-2.276999999999987,1.381299999999996],[-2.875,0],[-1.8669999999999902,-0.9706999999999937],[-1.082999999999998,-1.717300000000009],[0.3173333333333233,-0.4853333333333296],[0,6.122666666666674],[-2.090666666666664,0],[0,-14.13066666666667],[1.99733333333333,0],[0,2.053333333333327],[-0.242666666666679,-0.3919999999999959],[1.941000000000003,-0.9707000000000079],[2.314999999999998,0],[0,0],[0,0]],"o":[[-2.912000000000006,0],[-2.277000000000015,-1.4187000000000012],[-1.306999999999988,-2.389300000000006],[0,-2.986699999999999],[1.343999999999994,-2.389300000000006],[2.277000000000015,-1.3813000000000102],[2.426999999999992,0],[1.867000000000019,0.9707000000000079],[-0.3173333333333233,0.4853333333333296],[0,-6.122666666666674],[2.090666666666664,0],[0,14.13066666666667],[-1.99733333333333,0],[0,-2.053333333333327],[0.242666666666679,0.3919999999999959],[-1.045000000000016,1.829300000000003],[-1.941000000000003,0.9706999999999937],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[57.40100000000001,43.06400000000001],[51.968999999999994,42],[48.38499999999999,38.976],[47.09700000000001,34.384],[48.161,29.960000000000008],[51.577,26.656000000000006],[57.345,24.752],[67.87299999999999,23.016000000000005],[67.87299999999999,28],[58.577,29.624],[54.881,31.248],[53.70500000000001,34.16],[54.992999999999995,37.016000000000005],[58.40899999999999,38.08],[62.88900000000001,36.96000000000001],[65.91300000000001,33.992],[66.977,29.792],[66.977,22.008],[65.297,18.36800000000001],[60.929,16.912000000000006],[56.505,18.256],[53.761,21.616],[48.496999999999986,18.98400000000001],[51.240999999999985,15.064000000000007],[55.66499999999999,12.376],[61.09700000000001,11.424000000000007],[67.42500000000001,12.768],[71.737,16.52],[73.305,22.008],[73.305,42.392],[67.257,42.392],[67.257,36.904],[68.54499999999999,37.072],[65.80099999999999,40.264],[62.04900000000001,42.336],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001]],"i":[[0,0],[1.568000000000012,0.7092999999999989],[0.8590000000000089,1.2693000000000012],[0,1.7547],[-0.7090000000000032,1.306699999999992],[-1.531000000000006,0.8960000000000008],[-2.314999999999998,0.3733000000000004],[-3.509333333333331,0.5786666666666633],[0,-1.661333333333332],[3.098666666666674,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8590000000000089,-0.7467000000000041],[-1.381,0],[-1.269000000000005,0.7466999999999899],[-0.7090000000000032,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829000000000008,0],[1.269000000000005,-0.8960000000000008],[0.5970000000000084,-1.381299999999996],[1.754666666666679,0.8773333333333255],[-1.268999999999977,1.11999999999999],[-1.680000000000007,0.6346999999999952],[-1.903999999999996,0],[-1.829000000000008,-0.8960000000000008],[-1.0080000000000098,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999991,0],[0,1.829333333333338],[-0.429333333333318,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.716999999999985,0],[0,0],[0,0]],"o":[[-2.053000000000026,0],[-1.531000000000006,-0.7467000000000041],[-0.8589999999999804,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999857,-1.306700000000006],[1.531000000000006,-0.8960000000000008],[3.509333333333331,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666674,0.541333333333327],[-1.680000000000007,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.896000000000015,0.7092999999999989],[1.717000000000013,0],[1.306999999999988,-0.7467000000000041],[0.7089999999999748,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.082999999999998,-0.9707000000000079],[-1.680000000000007,0],[-1.2319999999999993,0.8586999999999989],[-1.754666666666679,-0.8773333333333255],[0.5600000000000023,-1.493300000000005],[1.269000000000005,-1.157300000000006],[1.717000000000013,-0.6347000000000094],[2.388999999999982,0],[1.86699999999999,0.8960000000000008],[1.045000000000016,1.568000000000012],[0,6.794666666666672],[-2.015999999999991,0],[0,-1.829333333333338],[0.429333333333318,0.05599999999999739],[-0.7089999999999748,1.2319999999999993],[-1.082999999999998,0.8960000000000008],[-1.381,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.159999999999997,0.6720000000000041],[21.84,22.400000000000006],[18.75999999999999,22.400000000000006],[34.16,0.6720000000000041],[40.31999999999999,0.6720000000000041],[40.31999999999999,42.392],[33.768,42.392],[33.768,8.456000000000003],[36.232,9.128],[20.49600000000001,30.632000000000005],[19.824000000000012,30.632000000000005],[4.424000000000007,9.128],[6.608000000000004,8.456000000000003],[6.608000000000004,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.053333333333342,0],[-5.226666666666659,-7.242666666666665],[1.026666666666671,0],[-5.133333333333326,7.242666666666672],[-2.053333333333342,0],[0,-13.90666666666667],[2.183999999999997,0],[0,11.312],[-0.8213333333333424,-0.2240000000000038],[5.245333333333321,-7.168000000000006],[0.2239999999999895,0],[5.133333333333326,7.168000000000006],[-0.7280000000000086,0.2240000000000038],[0,-11.312],[2.202666666666659,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.053333333333342,0],[5.226666666666659,7.242666666666672],[-1.026666666666671,0],[5.133333333333326,-7.242666666666665],[2.053333333333342,0],[0,13.90666666666666],[-2.183999999999997,0],[0,-11.312],[0.8213333333333424,0.2240000000000038],[-5.245333333333321,7.168000000000006],[-0.2239999999999895,0],[-5.133333333333326,-7.168000000000006],[0.7280000000000086,-0.2240000000000038],[0,11.312],[-2.202666666666659,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[2.91259765625,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[702.6863719370097,144],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":72,"ix":2}},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[56.54167175292969,-0.000022762338630855083],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":80,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[122.0000003294881,25.00000012138912],"ix":2},"a":{"a":0,"k":[56.54167175292969,-0.00002288818359375],"ix":2},"s":{"a":0,"k":[34.403572049765366,34.403572049765366],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":3,"nm":"Color & Stroke Change","sr":1,"ks":{"p":{"a":0,"k":[-55,-102],"ix":2},"a":{"a":0,"k":[50,50],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"0","w":500,"h":500,"ind":11,"ty":0,"nm":"hover-extension","sr":1,"ks":{"p":{"a":0,"k":[355,402],"ix":2},"a":{"a":0,"k":[250,250],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":10}],"markers":[]} \ No newline at end of file diff --git a/frontend/public/lotties/key-user.json b/frontend/public/lotties/key-user.json new file mode 100644 index 000000000..268b10a6f --- /dev/null +++ b/frontend/public/lotties/key-user.json @@ -0,0 +1 @@ +{"v":"5.7.5","fr":100,"ip":0,"op":254,"w":512,"h":512,"nm":"Comp 1","ddd":0,"metadata":{},"assets":[],"layers":[{"ddd":0,"ind":12345679,"ty":4,"nm":"Group Layer 8","sr":1,"ks":{"p":{"a":0,"k":[373.76,475.53049180327866,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[52.459016393442624,52.459016393442624,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[220.741,37.184],[225.501,35.896],[228.749,32.36800000000001],[229.981,27.216000000000008],[228.749,22.12],[225.501,18.592],[220.741,17.304],[215.981,18.592],[212.677,22.12],[211.501,27.216000000000008],[212.677,32.36800000000001],[215.981,35.896],[220.741,37.184],[220.741,37.184],[220.741,37.184]],"i":[[0,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.382000000000062,0.8586999999999989],[1.79200000000003,0],[1.418999999999983,-0.8586999999999989],[0.8220000000000027,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.382000000000062,-0.8586999999999989],[0.8220000000000027,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.418999999999983,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[221.357,43.06400000000001],[214.917,41.608],[210.49300000000005,37.408],[211.221,36.232],[211.221,42.392],[205.173,42.392],[205.173,0],[211.501,0],[211.501,18.36800000000001],[210.49300000000005,16.912000000000006],[214.973,12.88],[221.357,11.424000000000007],[229.085,13.49600000000001],[234.51700000000005,19.152],[236.533,27.216000000000008],[234.51700000000005,35.28],[229.141,40.992],[221.357,43.06400000000001],[221.357,43.06400000000001],[221.357,43.06400000000001]],"i":[[0,0],[1.942000000000007,0.9706999999999937],[1.045999999999935,1.829300000000003],[-0.2426666666666506,0.3919999999999959],[0,-2.053333333333327],[2.015999999999963,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.122666666666674],[0.3360000000000127,0.4853333333333296],[-1.865999999999985,0.9707000000000079],[-2.38900000000001,0],[-2.277000000000044,-1.3813000000000102],[-1.30600000000004,-2.389300000000006],[0,-2.986699999999999],[1.343999999999937,-2.389300000000006],[2.27800000000002,-1.4187000000000012],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.351999999999975,0],[-1.903999999999996,-0.9707000000000079],[0.2426666666666506,-0.3919999999999959],[0,2.053333333333327],[-2.015999999999963,0],[0,-14.13066666666667],[2.109333333333325,0],[0,6.122666666666667],[-0.3360000000000127,-0.4853333333333296],[1.120000000000005,-1.717300000000009],[1.867000000000075,-0.9706999999999937],[2.875,0],[2.314999999999941,1.381299999999996],[1.343999999999937,2.389300000000006],[0,2.986699999999999],[-1.30600000000004,2.389300000000006],[-2.27699999999993,1.381299999999996],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[181.87,43.06400000000001],[176.438,42],[172.854,38.976],[171.566,34.384],[172.63,29.960000000000008],[176.046,26.656000000000006],[181.814,24.752],[192.342,23.016000000000005],[192.342,28],[183.046,29.624],[179.35,31.248],[178.174,34.16],[179.462,37.016000000000005],[182.878,38.08],[187.35799999999995,36.96000000000001],[190.38199999999995,33.992],[191.446,29.792],[191.446,22.008],[189.766,18.36800000000001],[185.398,16.912000000000006],[180.974,18.256],[178.23,21.616],[172.966,18.98400000000001],[175.71,15.064000000000007],[180.134,12.376],[185.566,11.424000000000007],[191.894,12.768],[196.206,16.52],[197.774,22.008],[197.774,42.392],[191.726,42.392],[191.726,36.904],[193.014,37.072],[190.27,40.264],[186.518,42.336],[181.87,43.06400000000001],[181.87,43.06400000000001],[181.87,43.06400000000001]],"i":[[0,0],[1.567999999999984,0.7092999999999989],[0.8589999999999236,1.2693000000000012],[0,1.7547],[-0.7089999999999463,1.306699999999992],[-1.530000000000086,0.8960000000000008],[-2.313999999999965,0.3733000000000004],[-3.509333333333302,0.5786666666666633],[0,-1.661333333333332],[3.098666666666645,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8579999999999472,-0.7467000000000041],[-1.381000000000085,0],[-1.268999999999892,0.7466999999999899],[-0.7089999999999463,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829999999999927,0],[1.269999999999982,-0.8960000000000008],[0.5979999999999563,-1.381299999999996],[1.754666666666708,0.8773333333333255],[-1.269000000000005,1.11999999999999],[-1.680000000000064,0.6346999999999952],[-1.903999999999996,0],[-1.828999999999951,-0.8960000000000008],[-1.008000000000038,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999963,0],[0,1.829333333333338],[-0.4293333333333749,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.717999999999961,0],[0,0],[0,0]],"o":[[-2.052999999999997,0],[-1.529999999999973,-0.7467000000000041],[-0.8580000000000609,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999573,-1.306700000000006],[1.530999999999949,-0.8960000000000008],[3.509333333333302,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666645,0.541333333333327],[-1.680000000000064,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.8959999999999582,0.7092999999999989],[1.717999999999961,0],[1.307000000000016,-0.7467000000000041],[0.7100000000000364,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.081999999999994,-0.9707000000000079],[-1.680000000000064,0],[-1.232000000000085,0.8586999999999989],[-1.754666666666708,-0.8773333333333255],[0.5599999999999454,-1.493300000000005],[1.269999999999982,-1.157300000000006],[1.717999999999961,-0.6347000000000094],[2.389999999999986,0],[1.866999999999962,0.8960000000000008],[1.045999999999935,1.568000000000012],[0,6.794666666666672],[-2.015999999999963,0],[0,-1.829333333333338],[0.4293333333333749,0.05599999999999739],[-0.70900000000006,1.2319999999999993],[-1.081999999999994,0.8960000000000008],[-1.380999999999972,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[159.072,42.392],[159.072,0],[165.4,0],[165.4,42.392],[159.072,42.392],[159.072,42.392],[159.072,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-14.13066666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,14.13066666666667],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[138.952,43.06400000000001],[130.888,40.992],[125.456,35.28],[123.496,27.16],[125.456,19.040000000000006],[130.832,13.49600000000001],[138.448,11.424000000000007],[144.552,12.600000000000009],[149.088,15.848],[151.888,20.49600000000001],[152.896,26.096],[152.84,27.608],[152.616,29.064000000000007],[128.48,29.064000000000007],[128.48,24.024],[149.032,24.024],[146.008,26.320000000000007],[145.616,21.448000000000008],[142.816,18.032],[138.448,16.744],[133.968,18.032],[130.944,21.616],[130.104,27.216000000000008],[130.944,32.592],[134.192,36.176],[139.008,37.464],[143.65599999999995,36.232],[146.736,33.040000000000006],[151.888,35.56],[149.088,39.42400000000001],[144.60799999999995,42.11200000000001],[138.952,43.06400000000001],[138.952,43.06400000000001],[138.952,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.307000000000016,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.836999999999989,0],[-1.79200000000003,-0.784000000000006],[-1.231999999999971,-1.381299999999996],[-0.6349999999999909,-1.754700000000014],[0,-1.978700000000003],[0.03699999999992087,-0.5227000000000004],[0.1119999999999663,-0.4480000000000075],[8.04533333333336,0],[0,1.680000000000007],[-6.850666666666712,0],[1.008000000000038,-0.7653333333333308],[0.6349999999999909,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.680000000000064,0],[1.307000000000016,-0.8586999999999989],[0.70900000000006,-1.567999999999998],[-0.1490000000000009,-2.202700000000007],[-0.7469999999999573,-1.530699999999996],[-1.380999999999972,-0.8586999999999989],[-1.79200000000003,0],[-1.268999999999892,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333386,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.755000000000109,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.315000000000055,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999937,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.277000000000044,0],[1.79200000000003,0.7839999999999918],[1.232000000000085,1.344000000000008],[0.6720000000000255,1.7547],[0,0.4852999999999952],[-0.03700000000003456,0.5227000000000004],[-8.04533333333336,0],[0,-1.680000000000007],[6.850666666666712,0],[-1.008000000000038,0.7653333333333308],[0.3729999999999336,-1.829300000000003],[-0.59699999999998,-1.456000000000003],[-1.232000000000085,-0.8586999999999989],[-1.67999999999995,0],[-1.306999999999903,0.8213000000000079],[-0.7089999999999463,1.530699999999996],[-0.1870000000000118,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418999999999983,0.8586999999999989],[1.828999999999951,0],[1.307000000000016,-0.8212999999999937],[1.717333333333386,0.8400000000000034],[-0.59699999999998,1.4187000000000012],[-1.231999999999971,1.11999999999999],[-1.716999999999985,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,7.951999999999998],[111.001,0.6720000000000041],[117.329,0.6720000000000041],[117.329,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,42.392],[111.001,12.096],[117.329,12.096],[117.329,42.392],[111.001,42.392],[111.001,42.392],[111.001,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[101.41,42.72800000000001],[94.01800000000003,40.040000000000006],[91.38599999999997,32.48],[91.38599999999997,17.808000000000007],[86.06600000000003,17.808000000000007],[86.06600000000003,12.096],[86.90599999999995,12.096],[90.21000000000004,10.864],[91.38599999999997,7.504000000000005],[91.38599999999997,5.152000000000001],[97.71400000000006,5.152000000000001],[97.71400000000006,12.096],[104.602,12.096],[104.602,17.808000000000007],[97.71400000000006,17.808000000000007],[97.71400000000006,32.2],[98.21799999999996,34.888000000000005],[99.84199999999998,36.568],[102.754,37.128],[103.76200000000006,37.072],[104.826,36.96000000000001],[104.826,42.392],[103.09,42.616],[101.41,42.72800000000001],[101.41,42.72800000000001],[101.41,42.72800000000001]],"i":[[0,0],[1.754999999999995,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333312,0],[0,1.903999999999996],[-0.2799999999999727,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999935,0],[0,-1.903999999999996],[2.295999999999935,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7469999999999573,-0.4106999999999914],[-1.19500000000005,0],[-0.3730000000000473,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6349999999999909,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333312,0],[0,-1.903999999999996],[0.2799999999999727,0],[1.419000000000096,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999935,0],[0,1.903999999999996],[-2.295999999999935,0],[0,4.797333333333327],[0,1.045299999999997],[0.3360000000000127,0.7092999999999989],[0.7470000000000709,0.3733000000000004],[0.2989999999999782,0],[0.3729999999999336,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[78.71499999999997,42.72800000000001],[71.32299999999998,40.040000000000006],[68.69100000000003,32.48],[68.69100000000003,17.808000000000007],[63.37099999999998,17.808000000000007],[63.37099999999998,12.096],[64.21100000000001,12.096],[67.51499999999999,10.864],[68.69100000000003,7.504000000000005],[68.69100000000003,5.152000000000001],[75.019,5.152000000000001],[75.019,12.096],[81.90700000000004,12.096],[81.90700000000004,17.808000000000007],[75.019,17.808000000000007],[75.019,32.2],[75.52300000000002,34.888000000000005],[77.14699999999999,36.568],[80.05900000000003,37.128],[81.06700000000001,37.072],[82.13099999999997,36.96000000000001],[82.13099999999997,42.392],[80.39499999999998,42.616],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001]],"i":[[0,0],[1.754000000000019,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7470000000000141,-0.4106999999999914],[-1.19500000000005,0],[-0.3740000000000236,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6340000000000146,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173999999999978,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418000000000006,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7460000000000377,0.3733000000000004],[0.297999999999945,0],[0.3730000000000473,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,37.184],[48.94799999999998,35.896],[52.19600000000003,32.36800000000001],[53.428,27.216000000000008],[52.19600000000003,22.12],[48.94799999999998,18.592],[44.18799999999999,17.304],[39.428,18.592],[36.12400000000002,22.12],[34.94799999999998,27.216000000000008],[36.12400000000002,32.36800000000001],[39.428,35.896],[44.18799999999999,37.184],[44.18799999999999,37.184],[44.18799999999999,37.184]],"i":[[0,0],[-1.381999999999948,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.381000000000029,0.8586999999999989],[1.79200000000003,0],[1.418000000000006,-0.8586999999999989],[0.8209999999999695,-1.493300000000005],[0,-1.904000000000011],[-0.7840000000000487,-1.5307000000000102],[-1.382000000000005,-0.8586999999999989],[-1.754999999999995,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.381000000000029,-0.8586999999999989],[0.8209999999999695,-1.5307000000000102],[0,-1.904000000000011],[-0.7840000000000487,-1.493300000000005],[-1.381999999999948,-0.8586999999999989],[-1.754999999999995,0],[-1.382000000000005,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.418000000000006,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,43.06400000000001],[36.18000000000001,40.992],[30.46800000000002,35.336],[28.33999999999997,27.216000000000008],[30.46800000000002,19.096],[36.18000000000001,13.49600000000001],[44.18799999999999,11.424000000000007],[52.19600000000003,13.49600000000001],[57.85199999999998,19.096],[59.98000000000002,27.216000000000008],[57.85199999999998,35.392],[52.139999999999986,41.048],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001]],"i":[[0,0],[2.425999999999988,1.381299999999996],[1.418000000000006,2.389300000000006],[0,3.024000000000001],[-1.41900000000004,2.352000000000004],[-2.389999999999986,1.343999999999994],[-2.949999999999989,0],[-2.352000000000032,-1.3813000000000102],[-1.381999999999948,-2.389300000000006],[0,-3.061300000000003],[1.418000000000006,-2.389299999999992],[2.38900000000001,-1.381299999999996],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.911999999999978,0],[-2.389999999999986,-1.381299999999996],[-1.41900000000004,-2.389299999999992],[0,-3.061300000000003],[1.418000000000006,-2.389300000000006],[2.38900000000001,-1.3813000000000102],[2.98599999999999,0],[2.388999999999953,1.343999999999994],[1.418000000000006,2.352000000000004],[0,3.061299999999989],[-1.418999999999983,2.389300000000006],[-2.389999999999986,1.343999999999994],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.608000000000004,0.6720000000000041],[6.608000000000004,36.512],[24.639999999999986,36.512],[24.639999999999986,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.202666666666687,0],[0,-11.94666666666666],[-6.01066666666668,0],[0,-1.959999999999994],[8.21333333333331,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.202666666666687,0],[0,11.94666666666667],[6.01066666666668,0],[0,1.959999999999994],[-8.21333333333331,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[98.08047485351562,-21.67217254638672],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[246.681,42.392],[246.681,0],[253.009,0],[253.009,18.032],[252.001,17.248],[255.585,12.936000000000007],[261.297,11.424000000000007],[267.23299999999995,12.88],[271.265,16.912000000000006],[272.721,22.792],[272.721,42.392],[266.449,42.392],[266.449,24.528000000000006],[265.553,20.664],[263.201,18.2],[259.729,17.304],[256.25699999999995,18.2],[253.849,20.664],[253.009,24.528000000000006],[253.009,42.392],[246.681,42.392],[246.681,42.392],[246.681,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.010666666666665],[0.3360000000000127,0.2613333333333259],[-1.643000000000029,0.9706999999999937],[-2.166000000000054,0],[-1.717999999999961,-0.9706999999999937],[-0.9710000000000036,-1.717300000000009],[0,-2.202699999999993],[0,-6.533333333333331],[2.090666666666721,0],[0,5.954666666666668],[0.59699999999998,1.045299999999997],[1.007999999999925,0.5600000000000023],[1.305999999999926,0],[1.045000000000073,-0.5973000000000042],[0.59699999999998,-1.082700000000003],[0,-1.493300000000005],[0,-5.954666666666668],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,6.010666666666665],[-0.3360000000000127,-0.2613333333333259],[0.7459999999999809,-1.903999999999996],[1.641999999999967,-1.0080000000000098],[2.240000000000009,0],[1.717000000000098,0.9707000000000079],[0.9700000000000273,1.717299999999994],[0,6.533333333333331],[-2.090666666666721,0],[0,-5.954666666666668],[0,-1.5307000000000102],[-0.5599999999999454,-1.082700000000003],[-1.008000000000038,-0.5973000000000042],[-1.270000000000095,0],[-1.007999999999953,0.5600000000000023],[-0.5600000000000023,1.082700000000003],[0,5.954666666666668],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[237.089,42.72800000000001],[229.697,40.040000000000006],[227.065,32.48],[227.065,17.808000000000007],[221.745,17.808000000000007],[221.745,12.096],[222.585,12.096],[225.889,10.864],[227.065,7.504000000000005],[227.065,5.152000000000001],[233.393,5.152000000000001],[233.393,12.096],[240.281,12.096],[240.281,17.808000000000007],[233.393,17.808000000000007],[233.393,32.2],[233.897,34.888000000000005],[235.521,36.568],[238.433,37.128],[239.441,37.072],[240.505,36.96000000000001],[240.505,42.392],[238.769,42.616],[237.089,42.72800000000001],[237.089,42.72800000000001],[237.089,42.72800000000001]],"i":[[0,0],[1.755000000000052,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7459999999999809,-0.4106999999999914],[-1.194000000000017,0],[-0.3729999999999905,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6350000000000477,-0.07469999999999288],[0.48599999999999,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.753999999999962,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418999999999983,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7470000000000141,0.3733000000000004],[0.2989999999999782,0],[0.3740000000000236,-0.03730000000000189],[0,1.810666666666663],[-0.5220000000000482,0.0747000000000071],[-0.6339999999999577,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,7.951999999999998],[210.259,0.6720000000000041],[216.587,0.6720000000000041],[216.587,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,42.392],[210.259,12.096],[216.587,12.096],[216.587,42.392],[210.259,42.392],[210.259,42.392],[210.259,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[169.688,42.392],[159.272,12.096],[165.992,12.096],[173.944,36.232],[171.592,36.232],[179.712,12.096],[185.48,12.096],[193.544,36.232],[191.192,36.232],[199.2,12.096],[205.92,12.096],[195.448,42.392],[189.736,42.392],[181.56,17.696],[183.632,17.696],[175.456,42.392],[169.688,42.392],[169.688,42.392],[169.688,42.392]],"i":[[0,0],[3.47199999999998,10.09866666666667],[-2.240000000000009,0],[-2.650666666666666,-8.045333333333332],[0.7839999999999918,0],[-2.706666666666649,8.045333333333332],[-1.922666666666657,0],[-2.687999999999988,-8.045333333333332],[0.7839999999999918,0],[-2.669333333333327,8.045333333333332],[-2.240000000000009,0],[3.490666666666641,-10.09866666666667],[1.903999999999996,0],[2.725333333333367,8.232],[-0.6906666666666865,0],[2.72533333333331,-8.232],[1.922666666666657,0],[0,0],[0,0]],"o":[[-3.47199999999998,-10.09866666666666],[2.240000000000009,0],[2.650666666666666,8.045333333333332],[-0.7839999999999918,0],[2.706666666666649,-8.045333333333332],[1.922666666666657,0],[2.687999999999988,8.045333333333332],[-0.7839999999999918,0],[2.669333333333327,-8.045333333333332],[2.240000000000009,0],[-3.490666666666641,10.09866666666666],[-1.903999999999996,0],[-2.725333333333367,-8.232],[0.6906666666666865,0],[-2.72533333333331,8.232],[-1.922666666666657,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[155.86146545410156,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[132.444,43.06400000000001],[124.38,40.992],[118.948,35.28],[116.988,27.16],[118.948,19.040000000000006],[124.324,13.49600000000001],[131.94,11.424000000000007],[138.044,12.600000000000009],[142.58,15.848],[145.38,20.49600000000001],[146.388,26.096],[146.332,27.608],[146.108,29.064000000000007],[121.972,29.064000000000007],[121.972,24.024],[142.524,24.024],[139.5,26.320000000000007],[139.108,21.448000000000008],[136.308,18.032],[131.94,16.744],[127.46,18.032],[124.436,21.616],[123.596,27.216000000000008],[124.436,32.592],[127.684,36.176],[132.5,37.464],[137.148,36.232],[140.228,33.040000000000006],[145.38,35.56],[142.58,39.42400000000001],[138.1,42.11200000000001],[132.444,43.06400000000001],[132.444,43.06400000000001],[132.444,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.305999999999983,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.838000000000022,0],[-1.79200000000003,-0.784000000000006],[-1.232000000000028,-1.381299999999996],[-0.6350000000000477,-1.754700000000014],[0,-1.978700000000003],[0.03699999999997772,-0.5227000000000004],[0.1120000000000232,-0.4480000000000075],[8.045333333333303,0],[0,1.680000000000007],[-6.850666666666655,0],[1.007999999999981,-0.7653333333333308],[0.6340000000000146,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.67999999999995,0],[1.305999999999983,-0.8586999999999989],[0.7090000000000032,-1.567999999999998],[-0.1499999999999773,-2.202700000000007],[-0.7470000000000141,-1.530699999999996],[-1.382000000000005,-0.8586999999999989],[-1.791999999999973,0],[-1.269999999999982,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333329,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.754000000000019,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.314999999999998,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999994,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.276999999999987,0],[1.791999999999973,0.7839999999999918],[1.231999999999971,1.344000000000008],[0.6719999999999686,1.7547],[0,0.4852999999999952],[-0.03800000000001091,0.5227000000000004],[-8.045333333333303,0],[0,-1.680000000000007],[6.850666666666655,0],[-1.007999999999981,0.7653333333333308],[0.3730000000000473,-1.829300000000003],[-0.5979999999999563,-1.456000000000003],[-1.232000000000028,-0.8586999999999989],[-1.680000000000007,0],[-1.307000000000016,0.8213000000000079],[-0.7100000000000364,1.530699999999996],[-0.186999999999955,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418000000000006,0.8586999999999989],[1.829000000000008,0],[1.305999999999983,-0.8212999999999937],[1.717333333333329,0.8400000000000034],[-0.5980000000000132,1.4187000000000012],[-1.232000000000028,1.11999999999999],[-1.718000000000018,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[95.32,37.184],[100.024,35.896],[103.328,32.36800000000001],[104.56,27.216000000000008],[103.328,22.12],[100.024,18.592],[95.32,17.304],[90.56,18.592],[87.256,22.12],[86.08000000000001,27.216000000000008],[87.256,32.36800000000001],[90.50399999999999,35.896],[95.32,37.184],[95.32,37.184],[95.32,37.184]],"i":[[0,0],[-1.381,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.820999999999998,1.493299999999991],[1.419000000000011,0.8586999999999989],[1.754999999999995,0],[1.418999999999983,-0.8586999999999989],[0.7839999999999918,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.381,-0.8586999999999989],[-1.792000000000002,0],[0,0],[0,0]],"o":[[1.754999999999995,0],[1.419000000000011,-0.8586999999999989],[0.820999999999998,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.381,-0.8586999999999989],[-1.754999999999995,0],[-1.419000000000011,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.7839999999999918,1.493299999999991],[1.419000000000011,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[94.70400000000001,43.06400000000001],[86.864,40.992],[81.43199999999999,35.28],[79.47200000000001,27.216000000000008],[81.488,19.152],[86.91999999999999,13.49600000000001],[94.648,11.424000000000007],[101.088,12.88],[105.512,16.912000000000006],[104.56,18.36800000000001],[104.56,0],[110.832,0],[110.832,42.392],[104.84,42.392],[104.84,36.232],[105.568,37.408],[101.088,41.608],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001]],"i":[[0,0],[2.314999999999998,1.381299999999996],[1.344000000000023,2.389300000000006],[0,2.986699999999999],[-1.343999999999994,2.389300000000006],[-2.276999999999987,1.381299999999996],[-2.875,0],[-1.8669999999999902,-0.9706999999999937],[-1.082999999999998,-1.717300000000009],[0.3173333333333233,-0.4853333333333296],[0,6.122666666666674],[-2.090666666666664,0],[0,-14.13066666666667],[1.99733333333333,0],[0,2.053333333333327],[-0.242666666666679,-0.3919999999999959],[1.941000000000003,-0.9707000000000079],[2.314999999999998,0],[0,0],[0,0]],"o":[[-2.912000000000006,0],[-2.277000000000015,-1.4187000000000012],[-1.306999999999988,-2.389300000000006],[0,-2.986699999999999],[1.343999999999994,-2.389300000000006],[2.277000000000015,-1.3813000000000102],[2.426999999999992,0],[1.867000000000019,0.9707000000000079],[-0.3173333333333233,0.4853333333333296],[0,-6.122666666666674],[2.090666666666664,0],[0,14.13066666666667],[-1.99733333333333,0],[0,-2.053333333333327],[0.242666666666679,0.3919999999999959],[-1.045000000000016,1.829300000000003],[-1.941000000000003,0.9706999999999937],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[57.40100000000001,43.06400000000001],[51.968999999999994,42],[48.38499999999999,38.976],[47.09700000000001,34.384],[48.161,29.960000000000008],[51.577,26.656000000000006],[57.345,24.752],[67.87299999999999,23.016000000000005],[67.87299999999999,28],[58.577,29.624],[54.881,31.248],[53.70500000000001,34.16],[54.992999999999995,37.016000000000005],[58.40899999999999,38.08],[62.88900000000001,36.96000000000001],[65.91300000000001,33.992],[66.977,29.792],[66.977,22.008],[65.297,18.36800000000001],[60.929,16.912000000000006],[56.505,18.256],[53.761,21.616],[48.496999999999986,18.98400000000001],[51.240999999999985,15.064000000000007],[55.66499999999999,12.376],[61.09700000000001,11.424000000000007],[67.42500000000001,12.768],[71.737,16.52],[73.305,22.008],[73.305,42.392],[67.257,42.392],[67.257,36.904],[68.54499999999999,37.072],[65.80099999999999,40.264],[62.04900000000001,42.336],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001]],"i":[[0,0],[1.568000000000012,0.7092999999999989],[0.8590000000000089,1.2693000000000012],[0,1.7547],[-0.7090000000000032,1.306699999999992],[-1.531000000000006,0.8960000000000008],[-2.314999999999998,0.3733000000000004],[-3.509333333333331,0.5786666666666633],[0,-1.661333333333332],[3.098666666666674,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8590000000000089,-0.7467000000000041],[-1.381,0],[-1.269000000000005,0.7466999999999899],[-0.7090000000000032,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829000000000008,0],[1.269000000000005,-0.8960000000000008],[0.5970000000000084,-1.381299999999996],[1.754666666666679,0.8773333333333255],[-1.268999999999977,1.11999999999999],[-1.680000000000007,0.6346999999999952],[-1.903999999999996,0],[-1.829000000000008,-0.8960000000000008],[-1.0080000000000098,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999991,0],[0,1.829333333333338],[-0.429333333333318,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.716999999999985,0],[0,0],[0,0]],"o":[[-2.053000000000026,0],[-1.531000000000006,-0.7467000000000041],[-0.8589999999999804,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999857,-1.306700000000006],[1.531000000000006,-0.8960000000000008],[3.509333333333331,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666674,0.541333333333327],[-1.680000000000007,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.896000000000015,0.7092999999999989],[1.717000000000013,0],[1.306999999999988,-0.7467000000000041],[0.7089999999999748,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.082999999999998,-0.9707000000000079],[-1.680000000000007,0],[-1.2319999999999993,0.8586999999999989],[-1.754666666666679,-0.8773333333333255],[0.5600000000000023,-1.493300000000005],[1.269000000000005,-1.157300000000006],[1.717000000000013,-0.6347000000000094],[2.388999999999982,0],[1.86699999999999,0.8960000000000008],[1.045000000000016,1.568000000000012],[0,6.794666666666672],[-2.015999999999991,0],[0,-1.829333333333338],[0.429333333333318,0.05599999999999739],[-0.7089999999999748,1.2319999999999993],[-1.082999999999998,0.8960000000000008],[-1.381,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.159999999999997,0.6720000000000041],[21.84,22.400000000000006],[18.75999999999999,22.400000000000006],[34.16,0.6720000000000041],[40.31999999999999,0.6720000000000041],[40.31999999999999,42.392],[33.768,42.392],[33.768,8.456000000000003],[36.232,9.128],[20.49600000000001,30.632000000000005],[19.824000000000012,30.632000000000005],[4.424000000000007,9.128],[6.608000000000004,8.456000000000003],[6.608000000000004,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.053333333333342,0],[-5.226666666666659,-7.242666666666665],[1.026666666666671,0],[-5.133333333333326,7.242666666666672],[-2.053333333333342,0],[0,-13.90666666666667],[2.183999999999997,0],[0,11.312],[-0.8213333333333424,-0.2240000000000038],[5.245333333333321,-7.168000000000006],[0.2239999999999895,0],[5.133333333333326,7.168000000000006],[-0.7280000000000086,0.2240000000000038],[0,-11.312],[2.202666666666659,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.053333333333342,0],[5.226666666666659,7.242666666666672],[-1.026666666666671,0],[5.133333333333326,-7.242666666666665],[2.053333333333342,0],[0,13.90666666666666],[-2.183999999999997,0],[0,-11.312],[0.8213333333333424,0.2240000000000038],[-5.245333333333321,7.168000000000006],[-0.2239999999999895,0],[-5.133333333333326,-7.168000000000006],[0.7280000000000086,-0.2240000000000038],[0,11.312],[-2.202666666666659,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[2.91259765625,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[702.6863719370097,144],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":72,"ix":2}},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[56.54167175292969,-0.000022762338630855083],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":80,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[122.0000003294881,25.00000012138912],"ix":2},"a":{"a":0,"k":[56.54167175292969,-0.00002288818359375],"ix":2},"s":{"a":0,"k":[34.403572049765366,34.403572049765366],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":255,"st":0,"bm":0},{"ddd":0,"ind":1,"ty":4,"nm":"key-920.svg 1","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","nm":"SVG","it":[{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[22.252499999999987,14.641499999999992],[18.446499999999986,16.21549999999999],[18.446499999999986,23.827499999999993],[26.058499999999988,23.827499999999993],[26.058499999999988,16.215499999999995],[22.252499999999987,14.641499999999992],[22.252499999999987,14.641499999999992]],"i":[[0,0],[1.0489999999999995,-1.0489999999999995],[-2.099,-2.0980000000000025],[-2.1000000000000014,2.099],[2.099,2.097999999999999],[1.3779999999999966,0],[0,0]],"o":[[-1.3790000000000013,0],[-2.099,2.099],[2.099,2.097999999999999],[2.099,-2.099],[-1.0500000000000043,-1.049000000000003],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[22.252499999999987,28.381499999999992],[16.33849999999999,25.93549999999999],[16.33849999999999,14.107499999999991],[28.16649999999999,14.107499999999991],[28.16649999999999,25.93549999999999],[22.252499999999987,28.381499999999992],[22.252499999999987,28.381499999999992]],"i":[[0,0],[1.629999999999999,1.6300000000000026],[-3.2609999999999992,3.2609999999999992],[-3.2609999999999992,-3.26],[3.2609999999999992,-3.2609999999999992],[2.1409999999999982,0],[0,0]],"o":[[-2.1419999999999995,0],[-3.2609999999999992,-3.2609999999999992],[3.2609999999999992,-3.26],[3.2609999999999992,3.2609999999999992],[-1.6310000000000038,1.6310000000000002],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[68.07149999999999,71.6065],[70.06249999999999,72.3885],[70.99949999999998,74.8385],[70.59849999999999,79.04849999999999],[79.01449999999998,87.44749999999999],[87.01849999999999,86.56649999999999],[87.01849999999999,76.95249999999999],[50.197499999999984,40.14549999999999],[50.66749999999998,39.18849999999999],[45.89349999999998,10.334499999999991],[10.33349999999998,10.334499999999991],[10.33349999999998,45.894499999999994],[40.19849999999998,50.15149999999999],[41.17849999999998,49.61249999999999],[46.033499999999975,54.46649999999999],[50.018499999999975,53.61949999999999],[52.70949999999998,54.42049999999999],[53.512499999999974,57.11149999999999],[52.673499999999976,61.125499999999995],[57.10449999999997,65.53750000000001],[61.73249999999997,64.87150000000001],[64.12249999999997,65.60350000000001],[65.09449999999997,67.90450000000001],[64.93349999999997,72.16050000000001],[67.54249999999996,71.65550000000002],[68.07149999999999,71.6065],[68.07149999999999,71.6065]],"i":[[0,0],[-0.5529999999999973,-0.5090000000000003],[0.08899999999999864,-0.9260000000000019],[0.13366666666667015,-1.403333333333336],[-2.805333333333337,-2.799666666666667],[-2.6680000000000064,0.29366666666666674],[0,3.204666666666668],[12.27366666666667,12.268999999999998],[-0.1566666666666663,0.3190000000000026],[7.597999999999999,7.599],[9.803,-9.803],[-9.804,-9.804000000000002],[-9.736,5.341000000000001],[-0.326666666666668,0.17966666666666953],[-1.6183333333333323,-1.618000000000002],[-1.3283333333333331,0.28233333333333377],[-0.7060000000000031,-0.7070000000000007],[0.20400000000000063,-0.9780000000000015],[0.27966666666666384,-1.338000000000001],[-1.4769999999999968,-1.4706666666666734],[-1.542666666666669,0.2219999999999942],[-0.6510000000000034,-0.5879999999999939],[0.03300000000000125,-0.875],[0.05366666666667186,-1.4186666666666667],[-0.8696666666666601,0.16833333333333655],[-0.17499999999999716,0],[0,0]],"o":[[0.7339999999999947,0],[0.6839999999999975,0.6310000000000002],[-0.13366666666667015,1.403333333333336],[2.805333333333337,2.799666666666667],[2.6680000000000064,-0.29366666666666674],[0,-3.204666666666668],[-12.27366666666667,-12.268999999999991],[0.1566666666666663,-0.3190000000000026],[4.743000000000002,-9.66],[-9.804000000000002,-9.804],[-9.804,9.804000000000002],[7.849,7.848999999999997],[0.326666666666668,-0.17966666666666953],[1.6183333333333323,1.618000000000002],[1.3283333333333331,-0.28233333333333377],[0.9789999999999992,-0.2079999999999984],[0.7070000000000007,0.7070000000000007],[-0.27966666666666384,1.338000000000001],[1.4769999999999968,1.4706666666666663],[1.542666666666669,-0.2219999999999942],[0.8699999999999974,-0.12300000000000466],[0.6500000000000057,0.5859999999999985],[-0.05366666666667186,1.4186666666666667],[0.8696666666666601,-0.16833333333333655],[0.17600000000003035,-0.03300000000001546],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[78.99149999999999,90.4215],[76.90749999999998,89.5565],[68.48249999999999,81.13250000000001],[67.63149999999999,78.76750000000001],[68.03249999999998,74.55550000000001],[65.42349999999999,75.0635],[62.96649999999999,74.38550000000001],[61.95349999999999,72.04750000000001],[62.11449999999999,67.79150000000001],[57.49549999999999,68.47950000000002],[54.99549999999999,67.64350000000002],[50.555499999999995,63.20450000000001],[49.75449999999999,60.51050000000001],[50.59349999999999,56.499500000000005],[46.616499999999995,57.374500000000005],[43.924499999999995,56.5735],[40.634499999999996,53.283500000000004],[8.224499999999999,48.0005],[8.224499999999999,8.224499999999999],[48.0005,8.224499999999999],[53.7975,39.527499999999996],[89.1365,74.86550000000001],[90.0005,76.95250000000001],[90.0005,86.59350000000002],[87.3685,89.52750000000002],[79.31349999999999,90.40250000000002],[78.99149999999999,90.4215],[78.99149999999999,90.4215]],"i":[[0,0],[0.5580000000000069,0.5579999999999927],[2.808333333333337,2.8079999999999927],[-0.08299999999999841,0.8780000000000001],[-0.13366666666667015,1.4039999999999964],[0.8696666666666601,-0.16933333333332712],[0.6780000000000044,0.5900000000000034],[-0.03399999999999892,0.8969999999999914],[-0.05366666666666475,1.4186666666666667],[1.539666666666669,-0.2293333333333294],[0.652000000000001,0.6529999999999916],[1.4799999999999969,1.4796666666666738],[-0.20599999999999596,0.9770000000000039],[-0.27966666666666384,1.3370000000000033],[1.3256666666666632,-0.2916666666666643],[0.7070000000000007,0.7070000000000007],[1.096666666666664,1.096666666666664],[8.514999999999997,8.515999999999998],[-10.965999999999998,10.966000000000005],[-10.966000000000008,-10.966],[4.715000000000003,-10.600999999999996],[-11.779666666666671,-11.779333333333348],[0,-0.7890000000000015],[0,-3.2136666666666684],[1.5,-0.1629999999999967],[2.6850000000000023,-0.2916666666666714],[0.10699999999999932,0],[0,0]],"o":[[-0.7800000000000011,0],[-2.808333333333337,-2.8079999999999927],[-0.6239999999999952,-0.6260000000000048],[0.13366666666667015,-1.4039999999999964],[-0.8696666666666601,0.16933333333332712],[-0.8790000000000049,0.15699999999999648],[-0.6769999999999996,-0.5889999999999986],[0.05366666666666475,-1.4186666666666667],[-1.539666666666669,0.2293333333333294],[-0.9140000000000015,0.132000000000005],[-1.4799999999999969,-1.4796666666666596],[-0.7079999999999984,-0.7100000000000009],[0.27966666666666384,-1.3370000000000033],[-1.3256666666666632,0.2916666666666643],[-0.9810000000000016,0.20700000000000074],[-1.096666666666664,-1.096666666666664],[-10.721,5.331000000000003],[-10.965999999999998,-10.966000000000001],[10.965,-10.966],[8.246000000000002,8.247],[11.779666666666671,11.779333333333334],[0.5570000000000022,0.5570000000000022],[0,3.2136666666666684],[0,1.5090000000000003],[-2.6850000000000023,0.2916666666666714],[-0.10699999999999932,0.012999999999976808],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2,"ix":2},"lc":1,"lj":1,"ml":4},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[128.41790771484375,127.32901763916016],"ix":2},"a":{"a":0,"k":[45.00025177001953,45.210750579833984],"ix":2},"s":{"a":0,"k":[280.9999942779541,280.9999942779541],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[53.66798400878906,226.4100341796875],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[106.49479392662286,106.49479392662285],"ix":2},"r":{"a":0,"k":-45,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":255,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Animation - 1739985524883.json","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[61.405,0.002],[0.003,61.404],[-61.405,0.002],[0.003,-61.404],[61.405,0.002]],"i":[[0,0],[33.91,0],[0,33.91],[-33.915,0],[0,-33.911]],"o":[[0,33.91],[-33.915,0],[0,-33.911],[33.91,0],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":32,"ix":2},"lc":2,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[121.405,121.405],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":1,"k":[{"t":137,"s":[291.0716552734375,587.243005324116],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]},"ti":[0,46.5],"to":[0,-61.167]},{"t":160,"s":[291.0716552734375,220.24300532411598],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]},"ti":[0,-4.833],"to":[0,-46.5]},{"t":177,"s":[291.0716552734375,308.243005324116],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]},"ti":[0,6.667],"to":[0,4.833]},{"t":194,"s":[291.0716552734375,249.24300532411598],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]},"ti":[0,-1.167],"to":[0,-6.667]},{"t":210,"s":[291.0716552734375,268.243005324116],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]},"ti":[0,2],"to":[0,1.167]},{"t":227,"s":[291.0716552734375,256.243005324116],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[66.06338024139404,66.06338024139404],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"avatar_body","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[-72.67896713876723,36.34080483698845],[0,-36.34080483698845],[72.67896713876723,36.34080483698845]],"i":[[0,0],[-40.140770468473434,0],[0,-40.140770468473434]],"o":[[0,-40.140770468473434],[40.138127933263775,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":1,"k":[{"t":110,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":133,"s":[100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":32,"ix":2},"lc":2,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[371.27587890625,447.1807861328125],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"line_01","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[-0.0024501811066720562,256.3418873988745],[-137.86066505745657,256.3418873988745],[-239.29448760201967,163.07350668959697],[-239.29448760201967,-163.07350668959697],[-137.86066505745657,-256.3418873988745],[137.86066505745657,-256.3418873988745],[239.29448760201967,-163.07350668959697]],"i":[[0,0],[0,0],[0,51.508991585593975],[0,0],[-56.02339100405656,0],[0,0],[0,-51.51349746772542]],"o":[[0,0],[-56.02339100405656,0],[0,0],[0,-51.51349746772542],[0,0],[56.018490641843215,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":47,"s":[100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":0,"k":205,"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":32,"ix":2},"lc":2,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[219.6200408935547,227.17970275878906],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[65.90681457519531,58.5899658203125],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[81.9148063659668,81.9148063659668],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":255,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/frontend/public/lotties/lock-closed.json b/frontend/public/lotties/lock-closed.json new file mode 100644 index 000000000..33d9e5e90 --- /dev/null +++ b/frontend/public/lotties/lock-closed.json @@ -0,0 +1 @@ +{"v":"5.7.5","fr":100,"ip":0,"op":52,"w":500,"h":500,"nm":"Comp 1","ddd":0,"metadata":{},"assets":[{"id":"0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":53,"st":0,"bm":0},{"ddd":0,"refId":"1","w":334,"h":418,"ind":2,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[83,41],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":50,"s":[100],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":53,"st":0,"bm":0,"parent":1},{"ddd":0,"ind":3,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":53,"st":0,"bm":0},{"ddd":0,"refId":"2","w":334,"h":418,"ind":4,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[83,41],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"h":1},{"t":2,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":53,"st":0,"bm":0,"parent":3},{"ddd":0,"ind":5,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":53,"st":0,"bm":0},{"ddd":0,"refId":"3","w":505,"h":398,"ind":6,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[-3,103],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":50,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":53,"st":0,"bm":0,"parent":5},{"ddd":0,"ind":7,"ty":3,"nm":".primary.design (Group)","sr":1,"ks":{"p":{"a":1,"k":[{"t":0,"s":[249.997,333.333],"i":{"x":[0.267],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":18,"s":[192.997,333.333],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":33,"s":[273.997,333.333],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":48,"s":[249.997,333.333],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[249.997,333.333],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[0.267],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":25,"s":[-18],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":38,"s":[14],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":52,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":53,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":53,"st":0,"bm":0,"parent":7},{"ddd":0,"refId":"4","w":344,"h":293,"ind":9,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[78,-6],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":50,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":53,"st":0,"bm":0,"parent":8}]},{"id":"1","layers":[{"ddd":0,"ind":10,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-83,-41],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[135.388,151.044],[109.372,177.06],[-109.377,177.06],[-135.394,151.044],[-135.394,-10.393],[135.388,-10.393],[135.388,151.044]],"i":[[0,0],[14.346,0],[0,0],[0,14.346],[0,0],[0,0],[0,0]],"o":[[0,14.346],[0,0],[-14.346,0],[0,0],[0,0],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-93.727,-88.54],[-5.21,-177.057],[5.206,-177.057],[93.723,-88.54],[93.723,-41.693],[-93.727,-41.693],[-93.727,-88.54]],"i":[[0,0],[-48.809,0],[0,0],[0,-48.809],[0,0],[0,0],[0,0]],"o":[[0,-48.809],[0,0],[48.809,0],[0,0],[0,0],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 3","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[151.038,-41.693],[125.023,-41.693],[125.023,-88.54],[5.206,-208.357],[-5.21,-208.357],[-125.027,-88.54],[-125.027,-41.693],[-151.044,-41.693],[-166.694,-26.043],[-166.694,151.044],[-109.377,208.36],[109.372,208.36],[166.688,151.044],[166.688,-26.043],[151.038,-41.693]],"i":[[0,0],[0,0],[0,0],[66.067,0],[0,0],[0,-66.067],[0,0],[0,0],[0,-8.643],[0,0],[-31.604,0],[0,0],[0,31.604],[0,0],[8.644,0]],"o":[[0,0],[0,0],[0,-66.067],[0,0],[-66.067,0],[0,0],[0,0],[-8.644,0],[0,0],[0,31.604],[0,0],[31.604,0],[0,0],[0,-8.643],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Group 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-25.495,-16.37],[-25.335,-15.12],[-25.125,-13.88],[-24.505,-11.45],[-23.655,-9.09],[-22.585,-6.82],[-21.306,-4.66],[-20.585,-3.63],[-19.806,-2.64],[-18.985,-1.68],[-18.115,-0.77],[-17.205,0.1],[-16.245,0.92],[-15.641,1.396],[-15.641,28.86],[0.01,44.51],[15.66,28.86],[15.66,1.381],[16.245,0.92],[17.205,0.1],[18.125,-0.77],[18.995,-1.68],[19.814,-2.64],[20.585,-3.63],[21.305,-4.66],[22.595,-6.82],[23.665,-9.09],[24.515,-11.45],[25.125,-13.88],[25.345,-15.12],[25.505,-16.37],[25.595,-17.63],[25.625,-18.89],[25.595,-20.15],[25.505,-21.41],[25.345,-22.66],[25.125,-23.9],[24.515,-26.33],[23.665,-28.69],[22.595,-30.96],[21.305,-33.11],[20.585,-34.14],[19.814,-35.14],[18.995,-36.09],[18.125,-37.01],[17.205,-37.88],[16.245,-38.7],[15.255,-39.47],[14.225,-40.19],[12.074,-41.48],[9.805,-42.55],[7.444,-43.4],[5.005,-44.01],[3.774,-44.23],[2.524,-44.39],[1.265,-44.48],[-1.255,-44.48],[-2.516,-44.39],[-3.766,-44.23],[-5.005,-44.01],[-7.436,-43.4],[-9.795,-42.55],[-12.065,-41.48],[-14.226,-40.19],[-15.255,-39.47],[-16.245,-38.7],[-17.205,-37.88],[-18.115,-37.01],[-18.985,-36.09],[-19.806,-35.14],[-20.585,-34.14],[-21.306,-33.11],[-22.585,-30.96],[-23.655,-28.69],[-24.505,-26.33],[-25.125,-23.9],[-25.335,-22.66],[-25.495,-21.41],[-25.596,-20.15],[-25.625,-18.89],[-25.596,-17.63],[-25.495,-16.37]],"i":[[0,0],[-0.07,-0.41],[-0.08,-0.41],[-0.24,-0.8],[-0.32,-0.77],[-0.4,-0.73],[-0.46,-0.7],[-0.24,-0.34],[-0.27,-0.32],[-0.28,-0.31],[-0.3,-0.29],[-0.311,-0.28],[-0.33,-0.26],[-0.204,-0.157],[0,0],[-8.644,0],[0,8.643],[0,0],[-0.189,0.159],[-0.311,0.28],[-0.301,0.3],[-0.28,0.31],[-0.27,0.32],[-0.25,0.33],[-0.23,0.34],[-0.39,0.74],[-0.32,0.77],[-0.25,0.8],[-0.16,0.81],[-0.06,0.41],[-0.05,0.42],[-0.021,0.42],[0,0.42],[0.02,0.42],[0.04,0.42],[0.06,0.42],[0.08,0.41],[0.24,0.8],[0.319,0.77],[0.39,0.74],[0.46,0.69],[0.25,0.33],[0.26,0.33],[0.279,0.31],[0.29,0.3],[0.31,0.28],[0.329,0.26],[0.34,0.25],[0.35,0.23],[0.73,0.39],[0.77,0.32],[0.79,0.25],[0.819,0.16],[0.41,0.06],[0.41,0.04],[0.42,0.02],[0.829,-0.04],[0.42,-0.04],[0.41,-0.06],[0.409,-0.08],[0.8,-0.24],[0.77,-0.32],[0.73,-0.39],[0.7,-0.46],[0.34,-0.25],[0.319,-0.26],[0.31,-0.28],[0.29,-0.29],[0.28,-0.31],[0.261,-0.32],[0.25,-0.34],[0.23,-0.35],[0.39,-0.73],[0.31,-0.77],[0.239,-0.8],[0.17,-0.81],[0.06,-0.41],[0.04,-0.41],[0.03,-0.42],[0,-0.42],[-0.02,-0.42],[-0.04,-0.42]],"o":[[0.04,0.42],[0.06,0.41],[0.17,0.81],[0.239,0.8],[0.31,0.77],[0.39,0.74],[0.23,0.34],[0.25,0.33],[0.261,0.32],[0.28,0.31],[0.29,0.3],[0.31,0.28],[0.195,0.165],[0,0],[0,8.643],[8.644,0],[0,0],[0.196,-0.153],[0.329,-0.26],[0.31,-0.28],[0.29,-0.29],[0.279,-0.31],[0.26,-0.32],[0.25,-0.34],[0.46,-0.7],[0.39,-0.73],[0.319,-0.77],[0.24,-0.8],[0.08,-0.41],[0.06,-0.41],[0.04,-0.42],[0.02,-0.42],[0,-0.42],[-0.021,-0.42],[-0.05,-0.41],[-0.06,-0.41],[-0.16,-0.81],[-0.25,-0.8],[-0.32,-0.77],[-0.39,-0.73],[-0.23,-0.35],[-0.25,-0.34],[-0.27,-0.32],[-0.28,-0.31],[-0.301,-0.29],[-0.311,-0.28],[-0.32,-0.26],[-0.33,-0.25],[-0.689,-0.46],[-0.739,-0.39],[-0.77,-0.32],[-0.8,-0.24],[-0.4,-0.08],[-0.42,-0.06],[-0.42,-0.04],[-0.84,-0.04],[-0.421,0.02],[-0.42,0.04],[-0.41,0.06],[-0.811,0.16],[-0.8,0.25],[-0.771,0.32],[-0.74,0.39],[-0.34,0.23],[-0.33,0.25],[-0.33,0.26],[-0.311,0.28],[-0.3,0.3],[-0.28,0.31],[-0.27,0.33],[-0.24,0.33],[-0.46,0.69],[-0.4,0.74],[-0.32,0.77],[-0.24,0.8],[-0.08,0.41],[-0.07,0.42],[-0.04,0.42],[-0.02,0.42],[0,0.42],[0.03,0.42],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,81],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250,250],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":53,"st":0,"bm":0}]},{"id":"2","layers":[{"ddd":0,"ind":11,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-83,-41],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[135.388,151.044],[109.372,177.06],[-109.377,177.06],[-135.394,151.044],[-135.394,-10.393],[135.388,-10.393],[135.388,151.044]],"i":[[0,0],[14.346,0],[0,0],[0,14.346],[0,0],[0,0],[0,0]],"o":[[0,14.346],[0,0],[-14.346,0],[0,0],[0,0],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-93.727,-88.54],[-5.21,-177.057],[5.206,-177.057],[93.723,-88.54],[93.723,-41.693],[-93.727,-41.693],[-93.727,-88.54]],"i":[[0,0],[-48.809,0],[0,0],[0,-48.809],[0,0],[0,0],[0,0]],"o":[[0,-48.809],[0,0],[48.809,0],[0,0],[0,0],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 3","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[151.038,-41.693],[125.023,-41.693],[125.023,-88.54],[5.206,-208.357],[-5.21,-208.357],[-125.027,-88.54],[-125.027,-41.693],[-151.044,-41.693],[-166.694,-26.043],[-166.694,151.044],[-109.377,208.36],[109.372,208.36],[166.688,151.044],[166.688,-26.043],[151.038,-41.693]],"i":[[0,0],[0,0],[0,0],[66.067,0],[0,0],[0,-66.067],[0,0],[0,0],[0,-8.643],[0,0],[-31.604,0],[0,0],[0,31.604],[0,0],[8.644,0]],"o":[[0,0],[0,0],[0,-66.067],[0,0],[-66.067,0],[0,0],[0,0],[-8.644,0],[0,0],[0,31.604],[0,0],[31.604,0],[0,0],[0,-8.643],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Group 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-25.495,-16.37],[-25.335,-15.12],[-25.125,-13.88],[-24.505,-11.45],[-23.655,-9.09],[-22.585,-6.82],[-21.306,-4.66],[-20.585,-3.63],[-19.806,-2.64],[-18.985,-1.68],[-18.115,-0.77],[-17.205,0.1],[-16.245,0.92],[-15.641,1.396],[-15.641,28.86],[0.01,44.51],[15.66,28.86],[15.66,1.381],[16.245,0.92],[17.205,0.1],[18.125,-0.77],[18.995,-1.68],[19.814,-2.64],[20.585,-3.63],[21.305,-4.66],[22.595,-6.82],[23.665,-9.09],[24.515,-11.45],[25.125,-13.88],[25.345,-15.12],[25.505,-16.37],[25.595,-17.63],[25.625,-18.89],[25.595,-20.15],[25.505,-21.41],[25.345,-22.66],[25.125,-23.9],[24.515,-26.33],[23.665,-28.69],[22.595,-30.96],[21.305,-33.11],[20.585,-34.14],[19.814,-35.14],[18.995,-36.09],[18.125,-37.01],[17.205,-37.88],[16.245,-38.7],[15.255,-39.47],[14.225,-40.19],[12.074,-41.48],[9.805,-42.55],[7.444,-43.4],[5.005,-44.01],[3.774,-44.23],[2.524,-44.39],[1.265,-44.48],[-1.255,-44.48],[-2.516,-44.39],[-3.766,-44.23],[-5.005,-44.01],[-7.436,-43.4],[-9.795,-42.55],[-12.065,-41.48],[-14.226,-40.19],[-15.255,-39.47],[-16.245,-38.7],[-17.205,-37.88],[-18.115,-37.01],[-18.985,-36.09],[-19.806,-35.14],[-20.585,-34.14],[-21.306,-33.11],[-22.585,-30.96],[-23.655,-28.69],[-24.505,-26.33],[-25.125,-23.9],[-25.335,-22.66],[-25.495,-21.41],[-25.596,-20.15],[-25.625,-18.89],[-25.596,-17.63],[-25.495,-16.37]],"i":[[0,0],[-0.07,-0.41],[-0.08,-0.41],[-0.24,-0.8],[-0.32,-0.77],[-0.4,-0.73],[-0.46,-0.7],[-0.24,-0.34],[-0.27,-0.32],[-0.28,-0.31],[-0.3,-0.29],[-0.311,-0.28],[-0.33,-0.26],[-0.204,-0.157],[0,0],[-8.644,0],[0,8.643],[0,0],[-0.189,0.159],[-0.311,0.28],[-0.301,0.3],[-0.28,0.31],[-0.27,0.32],[-0.25,0.33],[-0.23,0.34],[-0.39,0.74],[-0.32,0.77],[-0.25,0.8],[-0.16,0.81],[-0.06,0.41],[-0.05,0.42],[-0.021,0.42],[0,0.42],[0.02,0.42],[0.04,0.42],[0.06,0.42],[0.08,0.41],[0.24,0.8],[0.319,0.77],[0.39,0.74],[0.46,0.69],[0.25,0.33],[0.26,0.33],[0.279,0.31],[0.29,0.3],[0.31,0.28],[0.329,0.26],[0.34,0.25],[0.35,0.23],[0.73,0.39],[0.77,0.32],[0.79,0.25],[0.819,0.16],[0.41,0.06],[0.41,0.04],[0.42,0.02],[0.829,-0.04],[0.42,-0.04],[0.41,-0.06],[0.409,-0.08],[0.8,-0.24],[0.77,-0.32],[0.73,-0.39],[0.7,-0.46],[0.34,-0.25],[0.319,-0.26],[0.31,-0.28],[0.29,-0.29],[0.28,-0.31],[0.261,-0.32],[0.25,-0.34],[0.23,-0.35],[0.39,-0.73],[0.31,-0.77],[0.239,-0.8],[0.17,-0.81],[0.06,-0.41],[0.04,-0.41],[0.03,-0.42],[0,-0.42],[-0.02,-0.42],[-0.04,-0.42]],"o":[[0.04,0.42],[0.06,0.41],[0.17,0.81],[0.239,0.8],[0.31,0.77],[0.39,0.74],[0.23,0.34],[0.25,0.33],[0.261,0.32],[0.28,0.31],[0.29,0.3],[0.31,0.28],[0.195,0.165],[0,0],[0,8.643],[8.644,0],[0,0],[0.196,-0.153],[0.329,-0.26],[0.31,-0.28],[0.29,-0.29],[0.279,-0.31],[0.26,-0.32],[0.25,-0.34],[0.46,-0.7],[0.39,-0.73],[0.319,-0.77],[0.24,-0.8],[0.08,-0.41],[0.06,-0.41],[0.04,-0.42],[0.02,-0.42],[0,-0.42],[-0.021,-0.42],[-0.05,-0.41],[-0.06,-0.41],[-0.16,-0.81],[-0.25,-0.8],[-0.32,-0.77],[-0.39,-0.73],[-0.23,-0.35],[-0.25,-0.34],[-0.27,-0.32],[-0.28,-0.31],[-0.301,-0.29],[-0.311,-0.28],[-0.32,-0.26],[-0.33,-0.25],[-0.689,-0.46],[-0.739,-0.39],[-0.77,-0.32],[-0.8,-0.24],[-0.4,-0.08],[-0.42,-0.06],[-0.42,-0.04],[-0.84,-0.04],[-0.421,0.02],[-0.42,0.04],[-0.41,0.06],[-0.811,0.16],[-0.8,0.25],[-0.771,0.32],[-0.74,0.39],[-0.34,0.23],[-0.33,0.25],[-0.33,0.26],[-0.311,0.28],[-0.3,0.3],[-0.28,0.31],[-0.27,0.33],[-0.24,0.33],[-0.46,0.69],[-0.4,0.74],[-0.32,0.77],[-0.24,0.8],[-0.08,0.41],[-0.07,0.42],[-0.04,0.42],[-0.02,0.42],[0,0.42],[0.03,0.42],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,81],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250,250],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":53,"st":0,"bm":0}]},{"id":"3","layers":[{"ddd":0,"ind":12,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[3,-103],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,0],[0,0]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":51.25,"ix":2},"lc":2,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[249.998,312.498],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Group 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[0,-26.478],[0,26.478]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[250.005,333.768],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Group 3","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-151.041,-109.377],[-151.041,67.71],[-109.374,109.377],[109.374,109.377],[151.041,67.71],[151.041,-109.377],[-151.041,-109.377]],"i":[[0,0],[0,0],[-23.012,0],[0,0],[0,23.012],[0,0],[0,0]],"o":[[0,0],[0,23.012],[0,0],[23.013,0],[0,0],[0,0],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[249.997,333.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":1,"k":[{"t":0,"s":[249.997,333.333],"i":{"x":[0.267],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":18,"s":[192.997,333.333],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":33,"s":[273.997,333.333],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":48,"s":[249.997,333.333],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[249.997,333.333],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[0.267],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":25,"s":[-18],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":38,"s":[14],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":52,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":53,"st":0,"bm":0}]},{"id":"4","layers":[{"ddd":0,"ind":13,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-78,6],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[109.375,83.332],[109.375,20.835],[5.208,-83.332],[-5.208,-83.332],[-109.375,20.835],[-109.375,83.332]],"i":[[0,0],[0,0],[57.53,0],[0,0],[0,-57.53],[0,0]],"o":[[0,0],[0,-57.53],[0,0],[-57.53,0],[0,0],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":10,"s":[{"c":false,"v":[[109.375,83.332],[109.375,40.835],[5.208,-63.332],[-5.208,-63.332],[-109.375,40.835],[-109.375,83.332]],"i":[[0,0],[0,0],[57.53,0],[0,0],[0,-57.53],[0,0]],"o":[[0,0],[0,-57.53],[0,0],[-57.53,0],[0,0],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":22,"s":[{"c":false,"v":[[109.375,83.332],[109.375,20.835],[5.208,-83.332],[-5.208,-83.332],[-109.375,20.835],[-109.375,83.332]],"i":[[0,0],[0,0],[57.53,0],[0,0],[0,-57.53],[0,0]],"o":[[0,0],[0,-57.53],[0,0],[-57.53,0],[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[249.9980010986328,140.625],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":53,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":12345679,"ty":4,"nm":"Group Layer 8","sr":1,"ks":{"p":{"a":0,"k":[365,464.38524590163934,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[51.229508196721305,51.229508196721305,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[220.741,37.184],[225.501,35.896],[228.749,32.36800000000001],[229.981,27.216000000000008],[228.749,22.12],[225.501,18.592],[220.741,17.304],[215.981,18.592],[212.677,22.12],[211.501,27.216000000000008],[212.677,32.36800000000001],[215.981,35.896],[220.741,37.184],[220.741,37.184],[220.741,37.184]],"i":[[0,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.382000000000062,0.8586999999999989],[1.79200000000003,0],[1.418999999999983,-0.8586999999999989],[0.8220000000000027,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.382000000000062,-0.8586999999999989],[0.8220000000000027,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.418999999999983,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[221.357,43.06400000000001],[214.917,41.608],[210.49300000000005,37.408],[211.221,36.232],[211.221,42.392],[205.173,42.392],[205.173,0],[211.501,0],[211.501,18.36800000000001],[210.49300000000005,16.912000000000006],[214.973,12.88],[221.357,11.424000000000007],[229.085,13.49600000000001],[234.51700000000005,19.152],[236.533,27.216000000000008],[234.51700000000005,35.28],[229.141,40.992],[221.357,43.06400000000001],[221.357,43.06400000000001],[221.357,43.06400000000001]],"i":[[0,0],[1.942000000000007,0.9706999999999937],[1.045999999999935,1.829300000000003],[-0.2426666666666506,0.3919999999999959],[0,-2.053333333333327],[2.015999999999963,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.122666666666674],[0.3360000000000127,0.4853333333333296],[-1.865999999999985,0.9707000000000079],[-2.38900000000001,0],[-2.277000000000044,-1.3813000000000102],[-1.30600000000004,-2.389300000000006],[0,-2.986699999999999],[1.343999999999937,-2.389300000000006],[2.27800000000002,-1.4187000000000012],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.351999999999975,0],[-1.903999999999996,-0.9707000000000079],[0.2426666666666506,-0.3919999999999959],[0,2.053333333333327],[-2.015999999999963,0],[0,-14.13066666666667],[2.109333333333325,0],[0,6.122666666666667],[-0.3360000000000127,-0.4853333333333296],[1.120000000000005,-1.717300000000009],[1.867000000000075,-0.9706999999999937],[2.875,0],[2.314999999999941,1.381299999999996],[1.343999999999937,2.389300000000006],[0,2.986699999999999],[-1.30600000000004,2.389300000000006],[-2.27699999999993,1.381299999999996],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[181.87,43.06400000000001],[176.438,42],[172.854,38.976],[171.566,34.384],[172.63,29.960000000000008],[176.046,26.656000000000006],[181.814,24.752],[192.342,23.016000000000005],[192.342,28],[183.046,29.624],[179.35,31.248],[178.174,34.16],[179.462,37.016000000000005],[182.878,38.08],[187.35799999999995,36.96000000000001],[190.38199999999995,33.992],[191.446,29.792],[191.446,22.008],[189.766,18.36800000000001],[185.398,16.912000000000006],[180.974,18.256],[178.23,21.616],[172.966,18.98400000000001],[175.71,15.064000000000007],[180.134,12.376],[185.566,11.424000000000007],[191.894,12.768],[196.206,16.52],[197.774,22.008],[197.774,42.392],[191.726,42.392],[191.726,36.904],[193.014,37.072],[190.27,40.264],[186.518,42.336],[181.87,43.06400000000001],[181.87,43.06400000000001],[181.87,43.06400000000001]],"i":[[0,0],[1.567999999999984,0.7092999999999989],[0.8589999999999236,1.2693000000000012],[0,1.7547],[-0.7089999999999463,1.306699999999992],[-1.530000000000086,0.8960000000000008],[-2.313999999999965,0.3733000000000004],[-3.509333333333302,0.5786666666666633],[0,-1.661333333333332],[3.098666666666645,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8579999999999472,-0.7467000000000041],[-1.381000000000085,0],[-1.268999999999892,0.7466999999999899],[-0.7089999999999463,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829999999999927,0],[1.269999999999982,-0.8960000000000008],[0.5979999999999563,-1.381299999999996],[1.754666666666708,0.8773333333333255],[-1.269000000000005,1.11999999999999],[-1.680000000000064,0.6346999999999952],[-1.903999999999996,0],[-1.828999999999951,-0.8960000000000008],[-1.008000000000038,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999963,0],[0,1.829333333333338],[-0.4293333333333749,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.717999999999961,0],[0,0],[0,0]],"o":[[-2.052999999999997,0],[-1.529999999999973,-0.7467000000000041],[-0.8580000000000609,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999573,-1.306700000000006],[1.530999999999949,-0.8960000000000008],[3.509333333333302,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666645,0.541333333333327],[-1.680000000000064,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.8959999999999582,0.7092999999999989],[1.717999999999961,0],[1.307000000000016,-0.7467000000000041],[0.7100000000000364,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.081999999999994,-0.9707000000000079],[-1.680000000000064,0],[-1.232000000000085,0.8586999999999989],[-1.754666666666708,-0.8773333333333255],[0.5599999999999454,-1.493300000000005],[1.269999999999982,-1.157300000000006],[1.717999999999961,-0.6347000000000094],[2.389999999999986,0],[1.866999999999962,0.8960000000000008],[1.045999999999935,1.568000000000012],[0,6.794666666666672],[-2.015999999999963,0],[0,-1.829333333333338],[0.4293333333333749,0.05599999999999739],[-0.70900000000006,1.2319999999999993],[-1.081999999999994,0.8960000000000008],[-1.380999999999972,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[159.072,42.392],[159.072,0],[165.4,0],[165.4,42.392],[159.072,42.392],[159.072,42.392],[159.072,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-14.13066666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,14.13066666666667],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[138.952,43.06400000000001],[130.888,40.992],[125.456,35.28],[123.496,27.16],[125.456,19.040000000000006],[130.832,13.49600000000001],[138.448,11.424000000000007],[144.552,12.600000000000009],[149.088,15.848],[151.888,20.49600000000001],[152.896,26.096],[152.84,27.608],[152.616,29.064000000000007],[128.48,29.064000000000007],[128.48,24.024],[149.032,24.024],[146.008,26.320000000000007],[145.616,21.448000000000008],[142.816,18.032],[138.448,16.744],[133.968,18.032],[130.944,21.616],[130.104,27.216000000000008],[130.944,32.592],[134.192,36.176],[139.008,37.464],[143.65599999999995,36.232],[146.736,33.040000000000006],[151.888,35.56],[149.088,39.42400000000001],[144.60799999999995,42.11200000000001],[138.952,43.06400000000001],[138.952,43.06400000000001],[138.952,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.307000000000016,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.836999999999989,0],[-1.79200000000003,-0.784000000000006],[-1.231999999999971,-1.381299999999996],[-0.6349999999999909,-1.754700000000014],[0,-1.978700000000003],[0.03699999999992087,-0.5227000000000004],[0.1119999999999663,-0.4480000000000075],[8.04533333333336,0],[0,1.680000000000007],[-6.850666666666712,0],[1.008000000000038,-0.7653333333333308],[0.6349999999999909,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.680000000000064,0],[1.307000000000016,-0.8586999999999989],[0.70900000000006,-1.567999999999998],[-0.1490000000000009,-2.202700000000007],[-0.7469999999999573,-1.530699999999996],[-1.380999999999972,-0.8586999999999989],[-1.79200000000003,0],[-1.268999999999892,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333386,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.755000000000109,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.315000000000055,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999937,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.277000000000044,0],[1.79200000000003,0.7839999999999918],[1.232000000000085,1.344000000000008],[0.6720000000000255,1.7547],[0,0.4852999999999952],[-0.03700000000003456,0.5227000000000004],[-8.04533333333336,0],[0,-1.680000000000007],[6.850666666666712,0],[-1.008000000000038,0.7653333333333308],[0.3729999999999336,-1.829300000000003],[-0.59699999999998,-1.456000000000003],[-1.232000000000085,-0.8586999999999989],[-1.67999999999995,0],[-1.306999999999903,0.8213000000000079],[-0.7089999999999463,1.530699999999996],[-0.1870000000000118,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418999999999983,0.8586999999999989],[1.828999999999951,0],[1.307000000000016,-0.8212999999999937],[1.717333333333386,0.8400000000000034],[-0.59699999999998,1.4187000000000012],[-1.231999999999971,1.11999999999999],[-1.716999999999985,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,7.951999999999998],[111.001,0.6720000000000041],[117.329,0.6720000000000041],[117.329,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,42.392],[111.001,12.096],[117.329,12.096],[117.329,42.392],[111.001,42.392],[111.001,42.392],[111.001,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[101.41,42.72800000000001],[94.01800000000003,40.040000000000006],[91.38599999999997,32.48],[91.38599999999997,17.808000000000007],[86.06600000000003,17.808000000000007],[86.06600000000003,12.096],[86.90599999999995,12.096],[90.21000000000004,10.864],[91.38599999999997,7.504000000000005],[91.38599999999997,5.152000000000001],[97.71400000000006,5.152000000000001],[97.71400000000006,12.096],[104.602,12.096],[104.602,17.808000000000007],[97.71400000000006,17.808000000000007],[97.71400000000006,32.2],[98.21799999999996,34.888000000000005],[99.84199999999998,36.568],[102.754,37.128],[103.76200000000006,37.072],[104.826,36.96000000000001],[104.826,42.392],[103.09,42.616],[101.41,42.72800000000001],[101.41,42.72800000000001],[101.41,42.72800000000001]],"i":[[0,0],[1.754999999999995,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333312,0],[0,1.903999999999996],[-0.2799999999999727,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999935,0],[0,-1.903999999999996],[2.295999999999935,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7469999999999573,-0.4106999999999914],[-1.19500000000005,0],[-0.3730000000000473,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6349999999999909,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333312,0],[0,-1.903999999999996],[0.2799999999999727,0],[1.419000000000096,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999935,0],[0,1.903999999999996],[-2.295999999999935,0],[0,4.797333333333327],[0,1.045299999999997],[0.3360000000000127,0.7092999999999989],[0.7470000000000709,0.3733000000000004],[0.2989999999999782,0],[0.3729999999999336,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[78.71499999999997,42.72800000000001],[71.32299999999998,40.040000000000006],[68.69100000000003,32.48],[68.69100000000003,17.808000000000007],[63.37099999999998,17.808000000000007],[63.37099999999998,12.096],[64.21100000000001,12.096],[67.51499999999999,10.864],[68.69100000000003,7.504000000000005],[68.69100000000003,5.152000000000001],[75.019,5.152000000000001],[75.019,12.096],[81.90700000000004,12.096],[81.90700000000004,17.808000000000007],[75.019,17.808000000000007],[75.019,32.2],[75.52300000000002,34.888000000000005],[77.14699999999999,36.568],[80.05900000000003,37.128],[81.06700000000001,37.072],[82.13099999999997,36.96000000000001],[82.13099999999997,42.392],[80.39499999999998,42.616],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001]],"i":[[0,0],[1.754000000000019,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7470000000000141,-0.4106999999999914],[-1.19500000000005,0],[-0.3740000000000236,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6340000000000146,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173999999999978,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418000000000006,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7460000000000377,0.3733000000000004],[0.297999999999945,0],[0.3730000000000473,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,37.184],[48.94799999999998,35.896],[52.19600000000003,32.36800000000001],[53.428,27.216000000000008],[52.19600000000003,22.12],[48.94799999999998,18.592],[44.18799999999999,17.304],[39.428,18.592],[36.12400000000002,22.12],[34.94799999999998,27.216000000000008],[36.12400000000002,32.36800000000001],[39.428,35.896],[44.18799999999999,37.184],[44.18799999999999,37.184],[44.18799999999999,37.184]],"i":[[0,0],[-1.381999999999948,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.381000000000029,0.8586999999999989],[1.79200000000003,0],[1.418000000000006,-0.8586999999999989],[0.8209999999999695,-1.493300000000005],[0,-1.904000000000011],[-0.7840000000000487,-1.5307000000000102],[-1.382000000000005,-0.8586999999999989],[-1.754999999999995,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.381000000000029,-0.8586999999999989],[0.8209999999999695,-1.5307000000000102],[0,-1.904000000000011],[-0.7840000000000487,-1.493300000000005],[-1.381999999999948,-0.8586999999999989],[-1.754999999999995,0],[-1.382000000000005,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.418000000000006,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,43.06400000000001],[36.18000000000001,40.992],[30.46800000000002,35.336],[28.33999999999997,27.216000000000008],[30.46800000000002,19.096],[36.18000000000001,13.49600000000001],[44.18799999999999,11.424000000000007],[52.19600000000003,13.49600000000001],[57.85199999999998,19.096],[59.98000000000002,27.216000000000008],[57.85199999999998,35.392],[52.139999999999986,41.048],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001]],"i":[[0,0],[2.425999999999988,1.381299999999996],[1.418000000000006,2.389300000000006],[0,3.024000000000001],[-1.41900000000004,2.352000000000004],[-2.389999999999986,1.343999999999994],[-2.949999999999989,0],[-2.352000000000032,-1.3813000000000102],[-1.381999999999948,-2.389300000000006],[0,-3.061300000000003],[1.418000000000006,-2.389299999999992],[2.38900000000001,-1.381299999999996],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.911999999999978,0],[-2.389999999999986,-1.381299999999996],[-1.41900000000004,-2.389299999999992],[0,-3.061300000000003],[1.418000000000006,-2.389300000000006],[2.38900000000001,-1.3813000000000102],[2.98599999999999,0],[2.388999999999953,1.343999999999994],[1.418000000000006,2.352000000000004],[0,3.061299999999989],[-1.418999999999983,2.389300000000006],[-2.389999999999986,1.343999999999994],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.608000000000004,0.6720000000000041],[6.608000000000004,36.512],[24.639999999999986,36.512],[24.639999999999986,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.202666666666687,0],[0,-11.94666666666666],[-6.01066666666668,0],[0,-1.959999999999994],[8.21333333333331,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.202666666666687,0],[0,11.94666666666667],[6.01066666666668,0],[0,1.959999999999994],[-8.21333333333331,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[98.08047485351562,-21.67217254638672],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[246.681,42.392],[246.681,0],[253.009,0],[253.009,18.032],[252.001,17.248],[255.585,12.936000000000007],[261.297,11.424000000000007],[267.23299999999995,12.88],[271.265,16.912000000000006],[272.721,22.792],[272.721,42.392],[266.449,42.392],[266.449,24.528000000000006],[265.553,20.664],[263.201,18.2],[259.729,17.304],[256.25699999999995,18.2],[253.849,20.664],[253.009,24.528000000000006],[253.009,42.392],[246.681,42.392],[246.681,42.392],[246.681,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.010666666666665],[0.3360000000000127,0.2613333333333259],[-1.643000000000029,0.9706999999999937],[-2.166000000000054,0],[-1.717999999999961,-0.9706999999999937],[-0.9710000000000036,-1.717300000000009],[0,-2.202699999999993],[0,-6.533333333333331],[2.090666666666721,0],[0,5.954666666666668],[0.59699999999998,1.045299999999997],[1.007999999999925,0.5600000000000023],[1.305999999999926,0],[1.045000000000073,-0.5973000000000042],[0.59699999999998,-1.082700000000003],[0,-1.493300000000005],[0,-5.954666666666668],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,6.010666666666665],[-0.3360000000000127,-0.2613333333333259],[0.7459999999999809,-1.903999999999996],[1.641999999999967,-1.0080000000000098],[2.240000000000009,0],[1.717000000000098,0.9707000000000079],[0.9700000000000273,1.717299999999994],[0,6.533333333333331],[-2.090666666666721,0],[0,-5.954666666666668],[0,-1.5307000000000102],[-0.5599999999999454,-1.082700000000003],[-1.008000000000038,-0.5973000000000042],[-1.270000000000095,0],[-1.007999999999953,0.5600000000000023],[-0.5600000000000023,1.082700000000003],[0,5.954666666666668],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[237.089,42.72800000000001],[229.697,40.040000000000006],[227.065,32.48],[227.065,17.808000000000007],[221.745,17.808000000000007],[221.745,12.096],[222.585,12.096],[225.889,10.864],[227.065,7.504000000000005],[227.065,5.152000000000001],[233.393,5.152000000000001],[233.393,12.096],[240.281,12.096],[240.281,17.808000000000007],[233.393,17.808000000000007],[233.393,32.2],[233.897,34.888000000000005],[235.521,36.568],[238.433,37.128],[239.441,37.072],[240.505,36.96000000000001],[240.505,42.392],[238.769,42.616],[237.089,42.72800000000001],[237.089,42.72800000000001],[237.089,42.72800000000001]],"i":[[0,0],[1.755000000000052,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7459999999999809,-0.4106999999999914],[-1.194000000000017,0],[-0.3729999999999905,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6350000000000477,-0.07469999999999288],[0.48599999999999,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.753999999999962,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418999999999983,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7470000000000141,0.3733000000000004],[0.2989999999999782,0],[0.3740000000000236,-0.03730000000000189],[0,1.810666666666663],[-0.5220000000000482,0.0747000000000071],[-0.6339999999999577,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,7.951999999999998],[210.259,0.6720000000000041],[216.587,0.6720000000000041],[216.587,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,42.392],[210.259,12.096],[216.587,12.096],[216.587,42.392],[210.259,42.392],[210.259,42.392],[210.259,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[169.688,42.392],[159.272,12.096],[165.992,12.096],[173.944,36.232],[171.592,36.232],[179.712,12.096],[185.48,12.096],[193.544,36.232],[191.192,36.232],[199.2,12.096],[205.92,12.096],[195.448,42.392],[189.736,42.392],[181.56,17.696],[183.632,17.696],[175.456,42.392],[169.688,42.392],[169.688,42.392],[169.688,42.392]],"i":[[0,0],[3.47199999999998,10.09866666666667],[-2.240000000000009,0],[-2.650666666666666,-8.045333333333332],[0.7839999999999918,0],[-2.706666666666649,8.045333333333332],[-1.922666666666657,0],[-2.687999999999988,-8.045333333333332],[0.7839999999999918,0],[-2.669333333333327,8.045333333333332],[-2.240000000000009,0],[3.490666666666641,-10.09866666666667],[1.903999999999996,0],[2.725333333333367,8.232],[-0.6906666666666865,0],[2.72533333333331,-8.232],[1.922666666666657,0],[0,0],[0,0]],"o":[[-3.47199999999998,-10.09866666666666],[2.240000000000009,0],[2.650666666666666,8.045333333333332],[-0.7839999999999918,0],[2.706666666666649,-8.045333333333332],[1.922666666666657,0],[2.687999999999988,8.045333333333332],[-0.7839999999999918,0],[2.669333333333327,-8.045333333333332],[2.240000000000009,0],[-3.490666666666641,10.09866666666666],[-1.903999999999996,0],[-2.725333333333367,-8.232],[0.6906666666666865,0],[-2.72533333333331,8.232],[-1.922666666666657,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[155.86146545410156,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[132.444,43.06400000000001],[124.38,40.992],[118.948,35.28],[116.988,27.16],[118.948,19.040000000000006],[124.324,13.49600000000001],[131.94,11.424000000000007],[138.044,12.600000000000009],[142.58,15.848],[145.38,20.49600000000001],[146.388,26.096],[146.332,27.608],[146.108,29.064000000000007],[121.972,29.064000000000007],[121.972,24.024],[142.524,24.024],[139.5,26.320000000000007],[139.108,21.448000000000008],[136.308,18.032],[131.94,16.744],[127.46,18.032],[124.436,21.616],[123.596,27.216000000000008],[124.436,32.592],[127.684,36.176],[132.5,37.464],[137.148,36.232],[140.228,33.040000000000006],[145.38,35.56],[142.58,39.42400000000001],[138.1,42.11200000000001],[132.444,43.06400000000001],[132.444,43.06400000000001],[132.444,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.305999999999983,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.838000000000022,0],[-1.79200000000003,-0.784000000000006],[-1.232000000000028,-1.381299999999996],[-0.6350000000000477,-1.754700000000014],[0,-1.978700000000003],[0.03699999999997772,-0.5227000000000004],[0.1120000000000232,-0.4480000000000075],[8.045333333333303,0],[0,1.680000000000007],[-6.850666666666655,0],[1.007999999999981,-0.7653333333333308],[0.6340000000000146,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.67999999999995,0],[1.305999999999983,-0.8586999999999989],[0.7090000000000032,-1.567999999999998],[-0.1499999999999773,-2.202700000000007],[-0.7470000000000141,-1.530699999999996],[-1.382000000000005,-0.8586999999999989],[-1.791999999999973,0],[-1.269999999999982,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333329,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.754000000000019,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.314999999999998,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999994,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.276999999999987,0],[1.791999999999973,0.7839999999999918],[1.231999999999971,1.344000000000008],[0.6719999999999686,1.7547],[0,0.4852999999999952],[-0.03800000000001091,0.5227000000000004],[-8.045333333333303,0],[0,-1.680000000000007],[6.850666666666655,0],[-1.007999999999981,0.7653333333333308],[0.3730000000000473,-1.829300000000003],[-0.5979999999999563,-1.456000000000003],[-1.232000000000028,-0.8586999999999989],[-1.680000000000007,0],[-1.307000000000016,0.8213000000000079],[-0.7100000000000364,1.530699999999996],[-0.186999999999955,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418000000000006,0.8586999999999989],[1.829000000000008,0],[1.305999999999983,-0.8212999999999937],[1.717333333333329,0.8400000000000034],[-0.5980000000000132,1.4187000000000012],[-1.232000000000028,1.11999999999999],[-1.718000000000018,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[95.32,37.184],[100.024,35.896],[103.328,32.36800000000001],[104.56,27.216000000000008],[103.328,22.12],[100.024,18.592],[95.32,17.304],[90.56,18.592],[87.256,22.12],[86.08000000000001,27.216000000000008],[87.256,32.36800000000001],[90.50399999999999,35.896],[95.32,37.184],[95.32,37.184],[95.32,37.184]],"i":[[0,0],[-1.381,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.820999999999998,1.493299999999991],[1.419000000000011,0.8586999999999989],[1.754999999999995,0],[1.418999999999983,-0.8586999999999989],[0.7839999999999918,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.381,-0.8586999999999989],[-1.792000000000002,0],[0,0],[0,0]],"o":[[1.754999999999995,0],[1.419000000000011,-0.8586999999999989],[0.820999999999998,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.381,-0.8586999999999989],[-1.754999999999995,0],[-1.419000000000011,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.7839999999999918,1.493299999999991],[1.419000000000011,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[94.70400000000001,43.06400000000001],[86.864,40.992],[81.43199999999999,35.28],[79.47200000000001,27.216000000000008],[81.488,19.152],[86.91999999999999,13.49600000000001],[94.648,11.424000000000007],[101.088,12.88],[105.512,16.912000000000006],[104.56,18.36800000000001],[104.56,0],[110.832,0],[110.832,42.392],[104.84,42.392],[104.84,36.232],[105.568,37.408],[101.088,41.608],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001]],"i":[[0,0],[2.314999999999998,1.381299999999996],[1.344000000000023,2.389300000000006],[0,2.986699999999999],[-1.343999999999994,2.389300000000006],[-2.276999999999987,1.381299999999996],[-2.875,0],[-1.8669999999999902,-0.9706999999999937],[-1.082999999999998,-1.717300000000009],[0.3173333333333233,-0.4853333333333296],[0,6.122666666666674],[-2.090666666666664,0],[0,-14.13066666666667],[1.99733333333333,0],[0,2.053333333333327],[-0.242666666666679,-0.3919999999999959],[1.941000000000003,-0.9707000000000079],[2.314999999999998,0],[0,0],[0,0]],"o":[[-2.912000000000006,0],[-2.277000000000015,-1.4187000000000012],[-1.306999999999988,-2.389300000000006],[0,-2.986699999999999],[1.343999999999994,-2.389300000000006],[2.277000000000015,-1.3813000000000102],[2.426999999999992,0],[1.867000000000019,0.9707000000000079],[-0.3173333333333233,0.4853333333333296],[0,-6.122666666666674],[2.090666666666664,0],[0,14.13066666666667],[-1.99733333333333,0],[0,-2.053333333333327],[0.242666666666679,0.3919999999999959],[-1.045000000000016,1.829300000000003],[-1.941000000000003,0.9706999999999937],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[57.40100000000001,43.06400000000001],[51.968999999999994,42],[48.38499999999999,38.976],[47.09700000000001,34.384],[48.161,29.960000000000008],[51.577,26.656000000000006],[57.345,24.752],[67.87299999999999,23.016000000000005],[67.87299999999999,28],[58.577,29.624],[54.881,31.248],[53.70500000000001,34.16],[54.992999999999995,37.016000000000005],[58.40899999999999,38.08],[62.88900000000001,36.96000000000001],[65.91300000000001,33.992],[66.977,29.792],[66.977,22.008],[65.297,18.36800000000001],[60.929,16.912000000000006],[56.505,18.256],[53.761,21.616],[48.496999999999986,18.98400000000001],[51.240999999999985,15.064000000000007],[55.66499999999999,12.376],[61.09700000000001,11.424000000000007],[67.42500000000001,12.768],[71.737,16.52],[73.305,22.008],[73.305,42.392],[67.257,42.392],[67.257,36.904],[68.54499999999999,37.072],[65.80099999999999,40.264],[62.04900000000001,42.336],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001]],"i":[[0,0],[1.568000000000012,0.7092999999999989],[0.8590000000000089,1.2693000000000012],[0,1.7547],[-0.7090000000000032,1.306699999999992],[-1.531000000000006,0.8960000000000008],[-2.314999999999998,0.3733000000000004],[-3.509333333333331,0.5786666666666633],[0,-1.661333333333332],[3.098666666666674,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8590000000000089,-0.7467000000000041],[-1.381,0],[-1.269000000000005,0.7466999999999899],[-0.7090000000000032,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829000000000008,0],[1.269000000000005,-0.8960000000000008],[0.5970000000000084,-1.381299999999996],[1.754666666666679,0.8773333333333255],[-1.268999999999977,1.11999999999999],[-1.680000000000007,0.6346999999999952],[-1.903999999999996,0],[-1.829000000000008,-0.8960000000000008],[-1.0080000000000098,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999991,0],[0,1.829333333333338],[-0.429333333333318,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.716999999999985,0],[0,0],[0,0]],"o":[[-2.053000000000026,0],[-1.531000000000006,-0.7467000000000041],[-0.8589999999999804,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999857,-1.306700000000006],[1.531000000000006,-0.8960000000000008],[3.509333333333331,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666674,0.541333333333327],[-1.680000000000007,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.896000000000015,0.7092999999999989],[1.717000000000013,0],[1.306999999999988,-0.7467000000000041],[0.7089999999999748,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.082999999999998,-0.9707000000000079],[-1.680000000000007,0],[-1.2319999999999993,0.8586999999999989],[-1.754666666666679,-0.8773333333333255],[0.5600000000000023,-1.493300000000005],[1.269000000000005,-1.157300000000006],[1.717000000000013,-0.6347000000000094],[2.388999999999982,0],[1.86699999999999,0.8960000000000008],[1.045000000000016,1.568000000000012],[0,6.794666666666672],[-2.015999999999991,0],[0,-1.829333333333338],[0.429333333333318,0.05599999999999739],[-0.7089999999999748,1.2319999999999993],[-1.082999999999998,0.8960000000000008],[-1.381,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.159999999999997,0.6720000000000041],[21.84,22.400000000000006],[18.75999999999999,22.400000000000006],[34.16,0.6720000000000041],[40.31999999999999,0.6720000000000041],[40.31999999999999,42.392],[33.768,42.392],[33.768,8.456000000000003],[36.232,9.128],[20.49600000000001,30.632000000000005],[19.824000000000012,30.632000000000005],[4.424000000000007,9.128],[6.608000000000004,8.456000000000003],[6.608000000000004,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.053333333333342,0],[-5.226666666666659,-7.242666666666665],[1.026666666666671,0],[-5.133333333333326,7.242666666666672],[-2.053333333333342,0],[0,-13.90666666666667],[2.183999999999997,0],[0,11.312],[-0.8213333333333424,-0.2240000000000038],[5.245333333333321,-7.168000000000006],[0.2239999999999895,0],[5.133333333333326,7.168000000000006],[-0.7280000000000086,0.2240000000000038],[0,-11.312],[2.202666666666659,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.053333333333342,0],[5.226666666666659,7.242666666666672],[-1.026666666666671,0],[5.133333333333326,-7.242666666666665],[2.053333333333342,0],[0,13.90666666666666],[-2.183999999999997,0],[0,-11.312],[0.8213333333333424,0.2240000000000038],[-5.245333333333321,7.168000000000006],[-0.2239999999999895,0],[-5.133333333333326,-7.168000000000006],[0.7280000000000086,-0.2240000000000038],[0,11.312],[-2.202666666666659,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[2.91259765625,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[702.6863719370097,144],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":72,"ix":2}},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[56.54167175292969,-0.000022762338630855083],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":80,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[122.0000003294881,25.00000012138912],"ix":2},"a":{"a":0,"k":[56.54167175292969,-0.00002288818359375],"ix":2},"s":{"a":0,"k":[34.403572049765366,34.403572049765366],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":53,"st":0,"bm":0},{"ddd":0,"ind":14,"ty":3,"nm":"Color & Stroke Change","sr":1,"ks":{"p":{"a":0,"k":[250,250],"ix":2},"a":{"a":0,"k":[50,50],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":53,"st":0,"bm":0},{"ddd":0,"refId":"0","w":500,"h":500,"ind":15,"ty":0,"nm":"hover-lock","sr":1,"ks":{"p":{"a":0,"k":[50,50],"ix":2},"a":{"a":0,"k":[250,250],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":53,"st":0,"bm":0,"parent":14}],"markers":[]} \ No newline at end of file diff --git a/frontend/public/lotties/moving-block.json b/frontend/public/lotties/moving-block.json new file mode 100644 index 000000000..9ab4f37e8 --- /dev/null +++ b/frontend/public/lotties/moving-block.json @@ -0,0 +1 @@ +{"v":"5.7.5","fr":100,"ip":0,"op":102,"w":500,"h":500,"nm":"Comp 1","ddd":0,"metadata":{},"assets":[{"id":"0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"1","w":376,"h":376,"ind":2,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[62,62],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":98,"s":[100],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":1},{"ddd":0,"ind":3,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"2","w":376,"h":376,"ind":4,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[62,62],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"h":1},{"t":2,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":3},{"ddd":0,"ind":5,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"3","w":470,"h":217,"ind":6,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[15,-3],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":98,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":5},{"ddd":0,"ind":7,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"4","w":470,"h":353,"ind":8,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[15,-3],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":98,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":7},{"ddd":0,"ind":9,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"5","w":470,"h":488,"ind":10,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[15,-3],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":98,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":9},{"ddd":0,"ind":11,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"6","w":470,"h":487,"ind":12,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[15,14],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":98,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":11},{"ddd":0,"ind":13,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"7","w":470,"h":352,"ind":14,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[15,149],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":98,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":13},{"ddd":0,"ind":15,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"8","w":470,"h":216,"ind":16,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[15,285],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":98,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":15}]},{"id":"1","layers":[{"ddd":0,"ind":17,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-62,-62],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[156.226,20.808],[-156.226,20.808],[-156.226,-20.808],[156.226,-20.808],[156.226,20.808]],"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[171.876,-52.108],[-171.876,-52.108],[-187.526,-36.458],[-187.526,36.458],[-171.876,52.108],[171.876,52.108],[187.526,36.458],[187.526,-36.458],[171.876,-52.108]],"i":[[0,0],[0,0],[0,-8.643],[0,0],[-8.644,0],[0,0],[0,8.643],[0,0],[8.644,0]],"o":[[0,0],[-8.644,0],[0,0],[0,8.643],[0,0],[8.644,0],[0,0],[0,-8.643],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.004,385.421],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","nm":"Group 2","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[156.226,20.808],[-156.226,20.808],[-156.226,-20.808],[156.226,-20.808],[156.226,20.808]],"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[171.876,-52.108],[-171.876,-52.108],[-187.526,-36.458],[-187.526,36.458],[-171.876,52.108],[171.876,52.108],[187.526,36.458],[187.526,-36.458],[171.876,-52.108]],"i":[[0,0],[0,0],[0,-8.643],[0,0],[-8.644,0],[0,0],[0,8.643],[0,0],[8.644,0]],"o":[[0,0],[-8.644,0],[0,0],[0,8.643],[0,0],[8.644,0],[0,0],[0,-8.643],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","nm":"Group 3","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[156.226,20.808],[-156.226,20.808],[-156.226,-20.808],[156.226,-20.808],[156.226,20.808]],"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[171.876,-52.108],[-171.876,-52.108],[-187.526,-36.458],[-187.526,36.458],[-171.876,52.108],[171.876,52.108],[187.526,36.458],[187.526,-36.458],[171.876,-52.108]],"i":[[0,0],[0,0],[0,-8.643],[0,0],[-8.644,0],[0,0],[0,8.643],[0,0],[8.644,0]],"o":[[0,0],[-8.644,0],[0,0],[0,8.643],[0,0],[8.644,0],[0,0],[0,-8.643],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.004,114.586],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[250.004,250.003],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"2","layers":[{"ddd":0,"ind":18,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-62,-62],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[156.226,20.808],[-156.226,20.808],[-156.226,-20.808],[156.226,-20.808],[156.226,20.808]],"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[171.876,-52.108],[-171.876,-52.108],[-187.526,-36.458],[-187.526,36.458],[-171.876,52.108],[171.876,52.108],[187.526,36.458],[187.526,-36.458],[171.876,-52.108]],"i":[[0,0],[0,0],[0,-8.643],[0,0],[-8.644,0],[0,0],[0,8.643],[0,0],[8.644,0]],"o":[[0,0],[-8.644,0],[0,0],[0,8.643],[0,0],[8.644,0],[0,0],[0,-8.643],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.004,385.421],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","nm":"Group 2","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[156.226,20.808],[-156.226,20.808],[-156.226,-20.808],[156.226,-20.808],[156.226,20.808]],"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[171.876,-52.108],[-171.876,-52.108],[-187.526,-36.458],[-187.526,36.458],[-171.876,52.108],[171.876,52.108],[187.526,36.458],[187.526,-36.458],[171.876,-52.108]],"i":[[0,0],[0,0],[0,-8.643],[0,0],[-8.644,0],[0,0],[0,8.643],[0,0],[8.644,0]],"o":[[0,0],[-8.644,0],[0,0],[0,8.643],[0,0],[8.644,0],[0,0],[0,-8.643],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","nm":"Group 3","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[156.226,20.808],[-156.226,20.808],[-156.226,-20.808],[156.226,-20.808],[156.226,20.808]],"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[171.876,-52.108],[-171.876,-52.108],[-187.526,-36.458],[-187.526,36.458],[-171.876,52.108],[171.876,52.108],[187.526,36.458],[187.526,-36.458],[171.876,-52.108]],"i":[[0,0],[0,0],[0,-8.643],[0,0],[-8.644,0],[0,0],[0,8.643],[0,0],[8.644,0]],"o":[[0,0],[-8.644,0],[0,0],[0,8.643],[0,0],[8.644,0],[0,0],[0,-8.643],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.004,114.586],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[250.004,250.003],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"3","layers":[{"ddd":0,"ind":19,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-15,3],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[171.876,36.458],[-171.876,36.458],[-171.876,-36.458],[171.876,-36.458],[171.876,36.458]],"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":1,"k":[{"t":2,"s":[250.004,114.586],"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"ti":[0,75.5],"to":[0,-75.5]},{"t":35,"s":[250.004,-338.414],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"4","layers":[{"ddd":0,"ind":20,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-15,3],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[171.876,36.458],[-171.876,36.458],[-171.876,-36.458],[171.876,-36.458],[171.876,36.458]],"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":1,"k":[{"t":5,"s":[250.004,250.003],"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"ti":[0,75.5],"to":[0,-75.5]},{"t":48,"s":[250.004,-202.997],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"5","layers":[{"ddd":0,"ind":21,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-15,3],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[171.876,36.458],[-171.876,36.458],[-171.876,-36.458],[171.876,-36.458],[171.876,36.458]],"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":1,"k":[{"t":8,"s":[250.004,385.42],"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"ti":[0,75.5],"to":[0,-75.5]},{"t":62,"s":[250.004,-67.58],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"6","layers":[{"ddd":0,"ind":22,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-15,-14],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[171.876,36.458],[-171.876,36.458],[-171.876,-36.458],[171.876,-36.458],[171.876,36.458]],"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":1,"k":[{"t":25,"s":[250.004,566.586],"i":{"x":[0.2],"y":[1]},"o":{"x":[0.333],"y":[0]},"ti":[0,75.5],"to":[0,-75.5]},{"t":85,"s":[250.004,113.586],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"7","layers":[{"ddd":0,"ind":23,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-15,-149],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[171.876,36.458],[-171.876,36.458],[-171.876,-36.458],[171.876,-36.458],[171.876,36.458]],"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":1,"k":[{"t":35,"s":[250.004,702.003],"i":{"x":[0.2],"y":[1]},"o":{"x":[0.333],"y":[0]},"ti":[0,75.5],"to":[0,-75.5]},{"t":92,"s":[250.004,249.003],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"8","layers":[{"ddd":0,"ind":24,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-15,-285],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[171.876,36.458],[-171.876,36.458],[-171.876,-36.458],[171.876,-36.458],[171.876,36.458]],"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":1,"k":[{"t":47,"s":[250.004,837.42],"i":{"x":[0.2],"y":[1]},"o":{"x":[0.333],"y":[0]},"ti":[0,75.5],"to":[0,-75.5]},{"t":98,"s":[250.004,384.42],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":12345679,"ty":4,"nm":"Group Layer 8","sr":1,"ks":{"p":{"a":0,"k":[365,464.38524590163934,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[51.229508196721305,51.229508196721305,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[220.741,37.184],[225.501,35.896],[228.749,32.36800000000001],[229.981,27.216000000000008],[228.749,22.12],[225.501,18.592],[220.741,17.304],[215.981,18.592],[212.677,22.12],[211.501,27.216000000000008],[212.677,32.36800000000001],[215.981,35.896],[220.741,37.184],[220.741,37.184],[220.741,37.184]],"i":[[0,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.382000000000062,0.8586999999999989],[1.79200000000003,0],[1.418999999999983,-0.8586999999999989],[0.8220000000000027,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.382000000000062,-0.8586999999999989],[0.8220000000000027,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.418999999999983,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[221.357,43.06400000000001],[214.917,41.608],[210.49300000000005,37.408],[211.221,36.232],[211.221,42.392],[205.173,42.392],[205.173,0],[211.501,0],[211.501,18.36800000000001],[210.49300000000005,16.912000000000006],[214.973,12.88],[221.357,11.424000000000007],[229.085,13.49600000000001],[234.51700000000005,19.152],[236.533,27.216000000000008],[234.51700000000005,35.28],[229.141,40.992],[221.357,43.06400000000001],[221.357,43.06400000000001],[221.357,43.06400000000001]],"i":[[0,0],[1.942000000000007,0.9706999999999937],[1.045999999999935,1.829300000000003],[-0.2426666666666506,0.3919999999999959],[0,-2.053333333333327],[2.015999999999963,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.122666666666674],[0.3360000000000127,0.4853333333333296],[-1.865999999999985,0.9707000000000079],[-2.38900000000001,0],[-2.277000000000044,-1.3813000000000102],[-1.30600000000004,-2.389300000000006],[0,-2.986699999999999],[1.343999999999937,-2.389300000000006],[2.27800000000002,-1.4187000000000012],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.351999999999975,0],[-1.903999999999996,-0.9707000000000079],[0.2426666666666506,-0.3919999999999959],[0,2.053333333333327],[-2.015999999999963,0],[0,-14.13066666666667],[2.109333333333325,0],[0,6.122666666666667],[-0.3360000000000127,-0.4853333333333296],[1.120000000000005,-1.717300000000009],[1.867000000000075,-0.9706999999999937],[2.875,0],[2.314999999999941,1.381299999999996],[1.343999999999937,2.389300000000006],[0,2.986699999999999],[-1.30600000000004,2.389300000000006],[-2.27699999999993,1.381299999999996],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[181.87,43.06400000000001],[176.438,42],[172.854,38.976],[171.566,34.384],[172.63,29.960000000000008],[176.046,26.656000000000006],[181.814,24.752],[192.342,23.016000000000005],[192.342,28],[183.046,29.624],[179.35,31.248],[178.174,34.16],[179.462,37.016000000000005],[182.878,38.08],[187.35799999999995,36.96000000000001],[190.38199999999995,33.992],[191.446,29.792],[191.446,22.008],[189.766,18.36800000000001],[185.398,16.912000000000006],[180.974,18.256],[178.23,21.616],[172.966,18.98400000000001],[175.71,15.064000000000007],[180.134,12.376],[185.566,11.424000000000007],[191.894,12.768],[196.206,16.52],[197.774,22.008],[197.774,42.392],[191.726,42.392],[191.726,36.904],[193.014,37.072],[190.27,40.264],[186.518,42.336],[181.87,43.06400000000001],[181.87,43.06400000000001],[181.87,43.06400000000001]],"i":[[0,0],[1.567999999999984,0.7092999999999989],[0.8589999999999236,1.2693000000000012],[0,1.7547],[-0.7089999999999463,1.306699999999992],[-1.530000000000086,0.8960000000000008],[-2.313999999999965,0.3733000000000004],[-3.509333333333302,0.5786666666666633],[0,-1.661333333333332],[3.098666666666645,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8579999999999472,-0.7467000000000041],[-1.381000000000085,0],[-1.268999999999892,0.7466999999999899],[-0.7089999999999463,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829999999999927,0],[1.269999999999982,-0.8960000000000008],[0.5979999999999563,-1.381299999999996],[1.754666666666708,0.8773333333333255],[-1.269000000000005,1.11999999999999],[-1.680000000000064,0.6346999999999952],[-1.903999999999996,0],[-1.828999999999951,-0.8960000000000008],[-1.008000000000038,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999963,0],[0,1.829333333333338],[-0.4293333333333749,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.717999999999961,0],[0,0],[0,0]],"o":[[-2.052999999999997,0],[-1.529999999999973,-0.7467000000000041],[-0.8580000000000609,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999573,-1.306700000000006],[1.530999999999949,-0.8960000000000008],[3.509333333333302,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666645,0.541333333333327],[-1.680000000000064,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.8959999999999582,0.7092999999999989],[1.717999999999961,0],[1.307000000000016,-0.7467000000000041],[0.7100000000000364,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.081999999999994,-0.9707000000000079],[-1.680000000000064,0],[-1.232000000000085,0.8586999999999989],[-1.754666666666708,-0.8773333333333255],[0.5599999999999454,-1.493300000000005],[1.269999999999982,-1.157300000000006],[1.717999999999961,-0.6347000000000094],[2.389999999999986,0],[1.866999999999962,0.8960000000000008],[1.045999999999935,1.568000000000012],[0,6.794666666666672],[-2.015999999999963,0],[0,-1.829333333333338],[0.4293333333333749,0.05599999999999739],[-0.70900000000006,1.2319999999999993],[-1.081999999999994,0.8960000000000008],[-1.380999999999972,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[159.072,42.392],[159.072,0],[165.4,0],[165.4,42.392],[159.072,42.392],[159.072,42.392],[159.072,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-14.13066666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,14.13066666666667],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[138.952,43.06400000000001],[130.888,40.992],[125.456,35.28],[123.496,27.16],[125.456,19.040000000000006],[130.832,13.49600000000001],[138.448,11.424000000000007],[144.552,12.600000000000009],[149.088,15.848],[151.888,20.49600000000001],[152.896,26.096],[152.84,27.608],[152.616,29.064000000000007],[128.48,29.064000000000007],[128.48,24.024],[149.032,24.024],[146.008,26.320000000000007],[145.616,21.448000000000008],[142.816,18.032],[138.448,16.744],[133.968,18.032],[130.944,21.616],[130.104,27.216000000000008],[130.944,32.592],[134.192,36.176],[139.008,37.464],[143.65599999999995,36.232],[146.736,33.040000000000006],[151.888,35.56],[149.088,39.42400000000001],[144.60799999999995,42.11200000000001],[138.952,43.06400000000001],[138.952,43.06400000000001],[138.952,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.307000000000016,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.836999999999989,0],[-1.79200000000003,-0.784000000000006],[-1.231999999999971,-1.381299999999996],[-0.6349999999999909,-1.754700000000014],[0,-1.978700000000003],[0.03699999999992087,-0.5227000000000004],[0.1119999999999663,-0.4480000000000075],[8.04533333333336,0],[0,1.680000000000007],[-6.850666666666712,0],[1.008000000000038,-0.7653333333333308],[0.6349999999999909,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.680000000000064,0],[1.307000000000016,-0.8586999999999989],[0.70900000000006,-1.567999999999998],[-0.1490000000000009,-2.202700000000007],[-0.7469999999999573,-1.530699999999996],[-1.380999999999972,-0.8586999999999989],[-1.79200000000003,0],[-1.268999999999892,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333386,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.755000000000109,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.315000000000055,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999937,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.277000000000044,0],[1.79200000000003,0.7839999999999918],[1.232000000000085,1.344000000000008],[0.6720000000000255,1.7547],[0,0.4852999999999952],[-0.03700000000003456,0.5227000000000004],[-8.04533333333336,0],[0,-1.680000000000007],[6.850666666666712,0],[-1.008000000000038,0.7653333333333308],[0.3729999999999336,-1.829300000000003],[-0.59699999999998,-1.456000000000003],[-1.232000000000085,-0.8586999999999989],[-1.67999999999995,0],[-1.306999999999903,0.8213000000000079],[-0.7089999999999463,1.530699999999996],[-0.1870000000000118,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418999999999983,0.8586999999999989],[1.828999999999951,0],[1.307000000000016,-0.8212999999999937],[1.717333333333386,0.8400000000000034],[-0.59699999999998,1.4187000000000012],[-1.231999999999971,1.11999999999999],[-1.716999999999985,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,7.951999999999998],[111.001,0.6720000000000041],[117.329,0.6720000000000041],[117.329,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,42.392],[111.001,12.096],[117.329,12.096],[117.329,42.392],[111.001,42.392],[111.001,42.392],[111.001,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[101.41,42.72800000000001],[94.01800000000003,40.040000000000006],[91.38599999999997,32.48],[91.38599999999997,17.808000000000007],[86.06600000000003,17.808000000000007],[86.06600000000003,12.096],[86.90599999999995,12.096],[90.21000000000004,10.864],[91.38599999999997,7.504000000000005],[91.38599999999997,5.152000000000001],[97.71400000000006,5.152000000000001],[97.71400000000006,12.096],[104.602,12.096],[104.602,17.808000000000007],[97.71400000000006,17.808000000000007],[97.71400000000006,32.2],[98.21799999999996,34.888000000000005],[99.84199999999998,36.568],[102.754,37.128],[103.76200000000006,37.072],[104.826,36.96000000000001],[104.826,42.392],[103.09,42.616],[101.41,42.72800000000001],[101.41,42.72800000000001],[101.41,42.72800000000001]],"i":[[0,0],[1.754999999999995,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333312,0],[0,1.903999999999996],[-0.2799999999999727,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999935,0],[0,-1.903999999999996],[2.295999999999935,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7469999999999573,-0.4106999999999914],[-1.19500000000005,0],[-0.3730000000000473,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6349999999999909,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333312,0],[0,-1.903999999999996],[0.2799999999999727,0],[1.419000000000096,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999935,0],[0,1.903999999999996],[-2.295999999999935,0],[0,4.797333333333327],[0,1.045299999999997],[0.3360000000000127,0.7092999999999989],[0.7470000000000709,0.3733000000000004],[0.2989999999999782,0],[0.3729999999999336,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[78.71499999999997,42.72800000000001],[71.32299999999998,40.040000000000006],[68.69100000000003,32.48],[68.69100000000003,17.808000000000007],[63.37099999999998,17.808000000000007],[63.37099999999998,12.096],[64.21100000000001,12.096],[67.51499999999999,10.864],[68.69100000000003,7.504000000000005],[68.69100000000003,5.152000000000001],[75.019,5.152000000000001],[75.019,12.096],[81.90700000000004,12.096],[81.90700000000004,17.808000000000007],[75.019,17.808000000000007],[75.019,32.2],[75.52300000000002,34.888000000000005],[77.14699999999999,36.568],[80.05900000000003,37.128],[81.06700000000001,37.072],[82.13099999999997,36.96000000000001],[82.13099999999997,42.392],[80.39499999999998,42.616],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001]],"i":[[0,0],[1.754000000000019,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7470000000000141,-0.4106999999999914],[-1.19500000000005,0],[-0.3740000000000236,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6340000000000146,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173999999999978,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418000000000006,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7460000000000377,0.3733000000000004],[0.297999999999945,0],[0.3730000000000473,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,37.184],[48.94799999999998,35.896],[52.19600000000003,32.36800000000001],[53.428,27.216000000000008],[52.19600000000003,22.12],[48.94799999999998,18.592],[44.18799999999999,17.304],[39.428,18.592],[36.12400000000002,22.12],[34.94799999999998,27.216000000000008],[36.12400000000002,32.36800000000001],[39.428,35.896],[44.18799999999999,37.184],[44.18799999999999,37.184],[44.18799999999999,37.184]],"i":[[0,0],[-1.381999999999948,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.381000000000029,0.8586999999999989],[1.79200000000003,0],[1.418000000000006,-0.8586999999999989],[0.8209999999999695,-1.493300000000005],[0,-1.904000000000011],[-0.7840000000000487,-1.5307000000000102],[-1.382000000000005,-0.8586999999999989],[-1.754999999999995,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.381000000000029,-0.8586999999999989],[0.8209999999999695,-1.5307000000000102],[0,-1.904000000000011],[-0.7840000000000487,-1.493300000000005],[-1.381999999999948,-0.8586999999999989],[-1.754999999999995,0],[-1.382000000000005,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.418000000000006,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,43.06400000000001],[36.18000000000001,40.992],[30.46800000000002,35.336],[28.33999999999997,27.216000000000008],[30.46800000000002,19.096],[36.18000000000001,13.49600000000001],[44.18799999999999,11.424000000000007],[52.19600000000003,13.49600000000001],[57.85199999999998,19.096],[59.98000000000002,27.216000000000008],[57.85199999999998,35.392],[52.139999999999986,41.048],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001]],"i":[[0,0],[2.425999999999988,1.381299999999996],[1.418000000000006,2.389300000000006],[0,3.024000000000001],[-1.41900000000004,2.352000000000004],[-2.389999999999986,1.343999999999994],[-2.949999999999989,0],[-2.352000000000032,-1.3813000000000102],[-1.381999999999948,-2.389300000000006],[0,-3.061300000000003],[1.418000000000006,-2.389299999999992],[2.38900000000001,-1.381299999999996],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.911999999999978,0],[-2.389999999999986,-1.381299999999996],[-1.41900000000004,-2.389299999999992],[0,-3.061300000000003],[1.418000000000006,-2.389300000000006],[2.38900000000001,-1.3813000000000102],[2.98599999999999,0],[2.388999999999953,1.343999999999994],[1.418000000000006,2.352000000000004],[0,3.061299999999989],[-1.418999999999983,2.389300000000006],[-2.389999999999986,1.343999999999994],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.608000000000004,0.6720000000000041],[6.608000000000004,36.512],[24.639999999999986,36.512],[24.639999999999986,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.202666666666687,0],[0,-11.94666666666666],[-6.01066666666668,0],[0,-1.959999999999994],[8.21333333333331,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.202666666666687,0],[0,11.94666666666667],[6.01066666666668,0],[0,1.959999999999994],[-8.21333333333331,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[98.08047485351562,-21.67217254638672],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[246.681,42.392],[246.681,0],[253.009,0],[253.009,18.032],[252.001,17.248],[255.585,12.936000000000007],[261.297,11.424000000000007],[267.23299999999995,12.88],[271.265,16.912000000000006],[272.721,22.792],[272.721,42.392],[266.449,42.392],[266.449,24.528000000000006],[265.553,20.664],[263.201,18.2],[259.729,17.304],[256.25699999999995,18.2],[253.849,20.664],[253.009,24.528000000000006],[253.009,42.392],[246.681,42.392],[246.681,42.392],[246.681,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.010666666666665],[0.3360000000000127,0.2613333333333259],[-1.643000000000029,0.9706999999999937],[-2.166000000000054,0],[-1.717999999999961,-0.9706999999999937],[-0.9710000000000036,-1.717300000000009],[0,-2.202699999999993],[0,-6.533333333333331],[2.090666666666721,0],[0,5.954666666666668],[0.59699999999998,1.045299999999997],[1.007999999999925,0.5600000000000023],[1.305999999999926,0],[1.045000000000073,-0.5973000000000042],[0.59699999999998,-1.082700000000003],[0,-1.493300000000005],[0,-5.954666666666668],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,6.010666666666665],[-0.3360000000000127,-0.2613333333333259],[0.7459999999999809,-1.903999999999996],[1.641999999999967,-1.0080000000000098],[2.240000000000009,0],[1.717000000000098,0.9707000000000079],[0.9700000000000273,1.717299999999994],[0,6.533333333333331],[-2.090666666666721,0],[0,-5.954666666666668],[0,-1.5307000000000102],[-0.5599999999999454,-1.082700000000003],[-1.008000000000038,-0.5973000000000042],[-1.270000000000095,0],[-1.007999999999953,0.5600000000000023],[-0.5600000000000023,1.082700000000003],[0,5.954666666666668],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[237.089,42.72800000000001],[229.697,40.040000000000006],[227.065,32.48],[227.065,17.808000000000007],[221.745,17.808000000000007],[221.745,12.096],[222.585,12.096],[225.889,10.864],[227.065,7.504000000000005],[227.065,5.152000000000001],[233.393,5.152000000000001],[233.393,12.096],[240.281,12.096],[240.281,17.808000000000007],[233.393,17.808000000000007],[233.393,32.2],[233.897,34.888000000000005],[235.521,36.568],[238.433,37.128],[239.441,37.072],[240.505,36.96000000000001],[240.505,42.392],[238.769,42.616],[237.089,42.72800000000001],[237.089,42.72800000000001],[237.089,42.72800000000001]],"i":[[0,0],[1.755000000000052,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7459999999999809,-0.4106999999999914],[-1.194000000000017,0],[-0.3729999999999905,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6350000000000477,-0.07469999999999288],[0.48599999999999,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.753999999999962,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418999999999983,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7470000000000141,0.3733000000000004],[0.2989999999999782,0],[0.3740000000000236,-0.03730000000000189],[0,1.810666666666663],[-0.5220000000000482,0.0747000000000071],[-0.6339999999999577,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,7.951999999999998],[210.259,0.6720000000000041],[216.587,0.6720000000000041],[216.587,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,42.392],[210.259,12.096],[216.587,12.096],[216.587,42.392],[210.259,42.392],[210.259,42.392],[210.259,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[169.688,42.392],[159.272,12.096],[165.992,12.096],[173.944,36.232],[171.592,36.232],[179.712,12.096],[185.48,12.096],[193.544,36.232],[191.192,36.232],[199.2,12.096],[205.92,12.096],[195.448,42.392],[189.736,42.392],[181.56,17.696],[183.632,17.696],[175.456,42.392],[169.688,42.392],[169.688,42.392],[169.688,42.392]],"i":[[0,0],[3.47199999999998,10.09866666666667],[-2.240000000000009,0],[-2.650666666666666,-8.045333333333332],[0.7839999999999918,0],[-2.706666666666649,8.045333333333332],[-1.922666666666657,0],[-2.687999999999988,-8.045333333333332],[0.7839999999999918,0],[-2.669333333333327,8.045333333333332],[-2.240000000000009,0],[3.490666666666641,-10.09866666666667],[1.903999999999996,0],[2.725333333333367,8.232],[-0.6906666666666865,0],[2.72533333333331,-8.232],[1.922666666666657,0],[0,0],[0,0]],"o":[[-3.47199999999998,-10.09866666666666],[2.240000000000009,0],[2.650666666666666,8.045333333333332],[-0.7839999999999918,0],[2.706666666666649,-8.045333333333332],[1.922666666666657,0],[2.687999999999988,8.045333333333332],[-0.7839999999999918,0],[2.669333333333327,-8.045333333333332],[2.240000000000009,0],[-3.490666666666641,10.09866666666666],[-1.903999999999996,0],[-2.725333333333367,-8.232],[0.6906666666666865,0],[-2.72533333333331,8.232],[-1.922666666666657,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[155.86146545410156,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[132.444,43.06400000000001],[124.38,40.992],[118.948,35.28],[116.988,27.16],[118.948,19.040000000000006],[124.324,13.49600000000001],[131.94,11.424000000000007],[138.044,12.600000000000009],[142.58,15.848],[145.38,20.49600000000001],[146.388,26.096],[146.332,27.608],[146.108,29.064000000000007],[121.972,29.064000000000007],[121.972,24.024],[142.524,24.024],[139.5,26.320000000000007],[139.108,21.448000000000008],[136.308,18.032],[131.94,16.744],[127.46,18.032],[124.436,21.616],[123.596,27.216000000000008],[124.436,32.592],[127.684,36.176],[132.5,37.464],[137.148,36.232],[140.228,33.040000000000006],[145.38,35.56],[142.58,39.42400000000001],[138.1,42.11200000000001],[132.444,43.06400000000001],[132.444,43.06400000000001],[132.444,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.305999999999983,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.838000000000022,0],[-1.79200000000003,-0.784000000000006],[-1.232000000000028,-1.381299999999996],[-0.6350000000000477,-1.754700000000014],[0,-1.978700000000003],[0.03699999999997772,-0.5227000000000004],[0.1120000000000232,-0.4480000000000075],[8.045333333333303,0],[0,1.680000000000007],[-6.850666666666655,0],[1.007999999999981,-0.7653333333333308],[0.6340000000000146,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.67999999999995,0],[1.305999999999983,-0.8586999999999989],[0.7090000000000032,-1.567999999999998],[-0.1499999999999773,-2.202700000000007],[-0.7470000000000141,-1.530699999999996],[-1.382000000000005,-0.8586999999999989],[-1.791999999999973,0],[-1.269999999999982,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333329,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.754000000000019,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.314999999999998,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999994,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.276999999999987,0],[1.791999999999973,0.7839999999999918],[1.231999999999971,1.344000000000008],[0.6719999999999686,1.7547],[0,0.4852999999999952],[-0.03800000000001091,0.5227000000000004],[-8.045333333333303,0],[0,-1.680000000000007],[6.850666666666655,0],[-1.007999999999981,0.7653333333333308],[0.3730000000000473,-1.829300000000003],[-0.5979999999999563,-1.456000000000003],[-1.232000000000028,-0.8586999999999989],[-1.680000000000007,0],[-1.307000000000016,0.8213000000000079],[-0.7100000000000364,1.530699999999996],[-0.186999999999955,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418000000000006,0.8586999999999989],[1.829000000000008,0],[1.305999999999983,-0.8212999999999937],[1.717333333333329,0.8400000000000034],[-0.5980000000000132,1.4187000000000012],[-1.232000000000028,1.11999999999999],[-1.718000000000018,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[95.32,37.184],[100.024,35.896],[103.328,32.36800000000001],[104.56,27.216000000000008],[103.328,22.12],[100.024,18.592],[95.32,17.304],[90.56,18.592],[87.256,22.12],[86.08000000000001,27.216000000000008],[87.256,32.36800000000001],[90.50399999999999,35.896],[95.32,37.184],[95.32,37.184],[95.32,37.184]],"i":[[0,0],[-1.381,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.820999999999998,1.493299999999991],[1.419000000000011,0.8586999999999989],[1.754999999999995,0],[1.418999999999983,-0.8586999999999989],[0.7839999999999918,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.381,-0.8586999999999989],[-1.792000000000002,0],[0,0],[0,0]],"o":[[1.754999999999995,0],[1.419000000000011,-0.8586999999999989],[0.820999999999998,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.381,-0.8586999999999989],[-1.754999999999995,0],[-1.419000000000011,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.7839999999999918,1.493299999999991],[1.419000000000011,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[94.70400000000001,43.06400000000001],[86.864,40.992],[81.43199999999999,35.28],[79.47200000000001,27.216000000000008],[81.488,19.152],[86.91999999999999,13.49600000000001],[94.648,11.424000000000007],[101.088,12.88],[105.512,16.912000000000006],[104.56,18.36800000000001],[104.56,0],[110.832,0],[110.832,42.392],[104.84,42.392],[104.84,36.232],[105.568,37.408],[101.088,41.608],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001]],"i":[[0,0],[2.314999999999998,1.381299999999996],[1.344000000000023,2.389300000000006],[0,2.986699999999999],[-1.343999999999994,2.389300000000006],[-2.276999999999987,1.381299999999996],[-2.875,0],[-1.8669999999999902,-0.9706999999999937],[-1.082999999999998,-1.717300000000009],[0.3173333333333233,-0.4853333333333296],[0,6.122666666666674],[-2.090666666666664,0],[0,-14.13066666666667],[1.99733333333333,0],[0,2.053333333333327],[-0.242666666666679,-0.3919999999999959],[1.941000000000003,-0.9707000000000079],[2.314999999999998,0],[0,0],[0,0]],"o":[[-2.912000000000006,0],[-2.277000000000015,-1.4187000000000012],[-1.306999999999988,-2.389300000000006],[0,-2.986699999999999],[1.343999999999994,-2.389300000000006],[2.277000000000015,-1.3813000000000102],[2.426999999999992,0],[1.867000000000019,0.9707000000000079],[-0.3173333333333233,0.4853333333333296],[0,-6.122666666666674],[2.090666666666664,0],[0,14.13066666666667],[-1.99733333333333,0],[0,-2.053333333333327],[0.242666666666679,0.3919999999999959],[-1.045000000000016,1.829300000000003],[-1.941000000000003,0.9706999999999937],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[57.40100000000001,43.06400000000001],[51.968999999999994,42],[48.38499999999999,38.976],[47.09700000000001,34.384],[48.161,29.960000000000008],[51.577,26.656000000000006],[57.345,24.752],[67.87299999999999,23.016000000000005],[67.87299999999999,28],[58.577,29.624],[54.881,31.248],[53.70500000000001,34.16],[54.992999999999995,37.016000000000005],[58.40899999999999,38.08],[62.88900000000001,36.96000000000001],[65.91300000000001,33.992],[66.977,29.792],[66.977,22.008],[65.297,18.36800000000001],[60.929,16.912000000000006],[56.505,18.256],[53.761,21.616],[48.496999999999986,18.98400000000001],[51.240999999999985,15.064000000000007],[55.66499999999999,12.376],[61.09700000000001,11.424000000000007],[67.42500000000001,12.768],[71.737,16.52],[73.305,22.008],[73.305,42.392],[67.257,42.392],[67.257,36.904],[68.54499999999999,37.072],[65.80099999999999,40.264],[62.04900000000001,42.336],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001]],"i":[[0,0],[1.568000000000012,0.7092999999999989],[0.8590000000000089,1.2693000000000012],[0,1.7547],[-0.7090000000000032,1.306699999999992],[-1.531000000000006,0.8960000000000008],[-2.314999999999998,0.3733000000000004],[-3.509333333333331,0.5786666666666633],[0,-1.661333333333332],[3.098666666666674,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8590000000000089,-0.7467000000000041],[-1.381,0],[-1.269000000000005,0.7466999999999899],[-0.7090000000000032,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829000000000008,0],[1.269000000000005,-0.8960000000000008],[0.5970000000000084,-1.381299999999996],[1.754666666666679,0.8773333333333255],[-1.268999999999977,1.11999999999999],[-1.680000000000007,0.6346999999999952],[-1.903999999999996,0],[-1.829000000000008,-0.8960000000000008],[-1.0080000000000098,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999991,0],[0,1.829333333333338],[-0.429333333333318,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.716999999999985,0],[0,0],[0,0]],"o":[[-2.053000000000026,0],[-1.531000000000006,-0.7467000000000041],[-0.8589999999999804,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999857,-1.306700000000006],[1.531000000000006,-0.8960000000000008],[3.509333333333331,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666674,0.541333333333327],[-1.680000000000007,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.896000000000015,0.7092999999999989],[1.717000000000013,0],[1.306999999999988,-0.7467000000000041],[0.7089999999999748,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.082999999999998,-0.9707000000000079],[-1.680000000000007,0],[-1.2319999999999993,0.8586999999999989],[-1.754666666666679,-0.8773333333333255],[0.5600000000000023,-1.493300000000005],[1.269000000000005,-1.157300000000006],[1.717000000000013,-0.6347000000000094],[2.388999999999982,0],[1.86699999999999,0.8960000000000008],[1.045000000000016,1.568000000000012],[0,6.794666666666672],[-2.015999999999991,0],[0,-1.829333333333338],[0.429333333333318,0.05599999999999739],[-0.7089999999999748,1.2319999999999993],[-1.082999999999998,0.8960000000000008],[-1.381,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.159999999999997,0.6720000000000041],[21.84,22.400000000000006],[18.75999999999999,22.400000000000006],[34.16,0.6720000000000041],[40.31999999999999,0.6720000000000041],[40.31999999999999,42.392],[33.768,42.392],[33.768,8.456000000000003],[36.232,9.128],[20.49600000000001,30.632000000000005],[19.824000000000012,30.632000000000005],[4.424000000000007,9.128],[6.608000000000004,8.456000000000003],[6.608000000000004,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.053333333333342,0],[-5.226666666666659,-7.242666666666665],[1.026666666666671,0],[-5.133333333333326,7.242666666666672],[-2.053333333333342,0],[0,-13.90666666666667],[2.183999999999997,0],[0,11.312],[-0.8213333333333424,-0.2240000000000038],[5.245333333333321,-7.168000000000006],[0.2239999999999895,0],[5.133333333333326,7.168000000000006],[-0.7280000000000086,0.2240000000000038],[0,-11.312],[2.202666666666659,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.053333333333342,0],[5.226666666666659,7.242666666666672],[-1.026666666666671,0],[5.133333333333326,-7.242666666666665],[2.053333333333342,0],[0,13.90666666666666],[-2.183999999999997,0],[0,-11.312],[0.8213333333333424,0.2240000000000038],[-5.245333333333321,7.168000000000006],[-0.2239999999999895,0],[-5.133333333333326,-7.168000000000006],[0.7280000000000086,-0.2240000000000038],[0,11.312],[-2.202666666666659,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[2.91259765625,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[702.6863719370097,144],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":72,"ix":2}},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[56.54167175292969,-0.000022762338630855083],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":80,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[122.0000003294881,25.00000012138912],"ix":2},"a":{"a":0,"k":[56.54167175292969,-0.00002288818359375],"ix":2},"s":{"a":0,"k":[34.403572049765366,34.403572049765366],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"ind":25,"ty":3,"nm":"Color & Stroke Change","sr":1,"ks":{"p":{"a":0,"k":[250,250],"ix":2},"a":{"a":0,"k":[50,50],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"0","w":500,"h":500,"ind":26,"ty":0,"nm":"hover-headline","sr":1,"ks":{"p":{"a":0,"k":[50,50],"ix":2},"a":{"a":0,"k":[250,250],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":25}],"markers":[]} \ No newline at end of file diff --git a/frontend/public/lotties/note.json b/frontend/public/lotties/note.json new file mode 100644 index 000000000..783cab705 --- /dev/null +++ b/frontend/public/lotties/note.json @@ -0,0 +1,808 @@ +{ + "v": "5.12.2", + "fr": 29.9700012207031, + "ip": 0, + "op": 45.0000018328876, + "w": 48, + "h": 48, + "nm": "note", + "ddd": 1, + "assets": [], + "layers": [ + { + "ddd": 1, + "ind": 1, + "ty": 4, + "nm": "note-outline-bot_s1g1_s2g2_s3g1_s4g1 Outlines", + "parent": 2, + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "rx": { "a": 0, "k": 0, "ix": 8 }, + "ry": { "a": 0, "k": 0, "ix": 9 }, + "rz": { + "a": 1, + "k": [ + { "i": { "x": [0], "y": [1] }, "o": { "x": [0.333], "y": [0] }, "t": 0, "s": [0] }, + { "i": { "x": [0], "y": [1] }, "o": { "x": [0.333], "y": [0] }, "t": 22, "s": [5] }, + { "t": 44.0000017921567, "s": [0] } + ], + "ix": 10 + }, + "or": { "a": 0, "k": [0, 0, 0], "ix": 7 }, + "p": { "a": 0, "k": [19.448, 27.122, 0], "ix": 2 }, + "a": { "a": 0, "k": [13.405, 11.539, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.022, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [ + { + "i": [ + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0] + ], + "v": [ + [5, 18.078], + [21.809, 18.078] + ], + "c": false + } + ] + }, + { + "i": { "x": 0.022, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 22, + "s": [ + { + "i": [ + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0] + ], + "v": [ + [4.311, 17.73], + [21.654, 16.837] + ], + "c": false + } + ] + }, + { + "t": 44.0000017921567, + "s": [ + { + "i": [ + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0] + ], + "v": [ + [5, 18.078], + [21.809, 18.078] + ], + "c": false + } + ] + } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ind": 1, + "ty": "sh", + "ix": 2, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.022, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [ + { + "i": [ + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0] + ], + "v": [ + [5, 11.281], + [21.809, 11.281] + ], + "c": false + } + ] + }, + { + "i": { "x": 0.022, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 22, + "s": [ + { + "i": [ + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0] + ], + "v": [ + [5, 11.281], + [21.735, 9.907] + ], + "c": false + } + ] + }, + { + "t": 44.0000017921567, + "s": [ + { + "i": [ + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0] + ], + "v": [ + [5, 11.281], + [21.809, 11.281] + ], + "c": false + } + ] + } + ], + "ix": 2 + }, + "nm": "Path 2", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ind": 2, + "ty": "sh", + "ix": 3, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.022, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [ + { + "i": [ + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0] + ], + "v": [ + [5, 5], + [13.756, 5] + ], + "c": false + } + ] + }, + { + "i": { "x": 0.022, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 22, + "s": [ + { + "i": [ + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0] + ], + "v": [ + [5.497, 4.939], + [14.633, 4.166] + ], + "c": false + } + ] + }, + { + "t": 44.0000017921567, + "s": [ + { + "i": [ + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0] + ], + "v": [ + [5, 5], + [13.756, 5] + ], + "c": false + } + ] + } + ], + "ix": 2 + }, + "nm": "Path 3", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "mm", + "mm": 1, + "nm": "Merge Paths 1", + "mn": "ADBE Vector Filter - Merge", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [1, 1, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { "a": 0, "k": 2.5, "ix": 5 }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 5, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 1 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 3 }, + "m": 1, + "ix": 2, + "nm": "Trim Paths 1", + "mn": "ADBE Vector Filter - Trim", + "hd": false + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 1 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 3 }, + "m": 1, + "ix": 3, + "nm": "Trim Paths 2", + "mn": "ADBE Vector Filter - Trim", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ty": "tm", + "s": { + "a": 1, + "k": [ + { + "i": { "x": [0.616], "y": [1] }, + "o": { "x": [0.41], "y": [0] }, + "t": 0, + "s": [0] + }, + { + "i": { "x": [0.005], "y": [1] }, + "o": { "x": [0.369], "y": [0] }, + "t": 22, + "s": [100] + }, + { "t": 44.0000017921567, "s": [0] } + ], + "ix": 1 + }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { + "a": 1, + "k": [ + { + "i": { "x": [0], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 22, + "s": [0] + }, + { "t": 44.0000017921567, "s": [360] } + ], + "ix": 3 + }, + "m": 1, + "ix": 1, + "nm": "Trim Paths 1", + "mn": "ADBE Vector Filter - Trim", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 2", + "np": 1, + "cix": 2, + "bm": 0, + "ix": 4, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 45.0000018328876, + "st": 0, + "ct": 1, + "bm": 0 + }, + { + "ddd": 1, + "ind": 2, + "ty": 4, + "nm": "note-outline-bot_s1g1_s2g1_s3g1_s4g1_background Outlines", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "rx": { "a": 0, "k": 0, "ix": 8 }, + "ry": { "a": 0, "k": 0, "ix": 9 }, + "rz": { "a": 0, "k": 0, "ix": 10 }, + "or": { "a": 0, "k": [0, 0, 0], "ix": 7 }, + "p": { "a": 0, "k": [24.778, 23.858, 0], "ix": 2 }, + "a": { "a": 0, "k": [19.825, 23.313, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [ + { + "i": [ + [1.423, 1.657], + [0, 0], + [0, 0], + [0, -2.354], + [0, 0], + [-1.324, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0], + [-1.482, -1.727], + [0, 0], + [0, 1.429], + [0, 0], + [2.093, 0] + ], + "v": [ + [4.581, 2.516], + [1.876, -0.634], + [-1.99, -5.137], + [-6.005, -3.212], + [-6.005, 4.277], + [-3.606, 6.865], + [2.822, 6.865] + ], + "c": true + } + ] + }, + { + "i": { "x": 0, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 22, + "s": [ + { + "i": [ + [0.56, 0.711], + [0.835, 1.361], + [0, 0], + [-0.031, 0.137], + [0, 0], + [-0.118, -0.836], + [0, 0] + ], + "o": [ + [0, 0], + [-0.835, -1.361], + [-0.299, -0.286], + [0, 0], + [1.062, 0.814], + [0, 0], + [-0.101, 0.125] + ], + "v": [ + [6.281, -2.819], + [5.393, -4.55], + [3.663, -5.916], + [3.337, -6.047], + [4.119, -5.661], + [6.298, -2.792], + [6.5, -1.722] + ], + "c": true + } + ] + }, + { + "t": 44.0000017921567, + "s": [ + { + "i": [ + [1.423, 1.657], + [0, 0], + [0, 0], + [0, -2.354], + [0, 0], + [-1.324, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0], + [-1.482, -1.727], + [0, 0], + [0, 1.429], + [0, 0], + [2.093, 0] + ], + "v": [ + [4.581, 2.516], + [1.876, -0.634], + [-1.99, -5.137], + [-6.005, -3.212], + [-6.005, 4.277], + [-3.606, 6.865], + [2.822, 6.865] + ], + "c": true + } + ] + } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [1, 1, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { "a": 0, "k": 2.5, "ix": 5 }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [1, 1, 1], "ix": 4 }, + "o": { "a": 0, "k": 0, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [28.646, 11.865], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [ + { + "i": [ + [0, 0], + [0, -0.781], + [0, 0], + [2.763, 0], + [0, 0], + [0, 2.828], + [0, 0], + [0, 0], + [-2.763, 0], + [0, 0], + [-0.625, -0.733] + ], + "o": [ + [0.507, 0.595], + [0, 0], + [0, 2.828], + [0, 0], + [-2.763, 0], + [0, 0], + [0, 0], + [0, -2.828], + [0, 0], + [0.963, 0], + [0, 0] + ], + "v": [ + [14.012, -8.411], + [14.797, -6.279], + [14.797, 13.05], + [9.794, 18.171], + [-9.794, 18.171], + [-14.796, 13.05], + [-14.796, 0.99], + [-14.796, -13.051], + [-9.794, -18.171], + [4.174, -18.171], + [6.676, -17.016] + ], + "c": true + } + ] + }, + { + "i": { "x": 0, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 22, + "s": [ + { + "i": [ + [0, 0], + [0.171, -6.028], + [0, 0], + [2.763, 0], + [0, 0], + [0, 2.828], + [-1.453, 6.385], + [0, 0], + [-2.763, 0], + [0, 0], + [-1.739, -0.297] + ], + "o": [ + [0.051, 2.426], + [-0.171, 6.028], + [-1.046, 2.979], + [0, 0], + [-2.763, 0], + [0, 0], + [1.266, -5.562], + [0, -2.828], + [0, 0], + [0.963, 0], + [0, 0] + ], + "v": [ + [15.322, -13.817], + [14.796, -0.028], + [12.547, 13.021], + [6.669, 18.201], + [-12.044, 18.141], + [-17.046, 13.021], + [-14.421, 1.025], + [-12.923, -12.566], + [-7.92, -17.686], + [3.673, -17.672], + [12.612, -17.453] + ], + "c": true + } + ] + }, + { + "t": 44.0000017921567, + "s": [ + { + "i": [ + [0, 0], + [0, -0.781], + [0, 0], + [2.763, 0], + [0, 0], + [0, 2.828], + [0, 0], + [0, 0], + [-2.763, 0], + [0, 0], + [-0.625, -0.733] + ], + "o": [ + [0.507, 0.595], + [0, 0], + [0, 2.828], + [0, 0], + [-2.763, 0], + [0, 0], + [0, 0], + [0, -2.828], + [0, 0], + [0.963, 0], + [0, 0] + ], + "v": [ + [14.012, -8.411], + [14.797, -6.279], + [14.797, 13.05], + [9.794, 18.171], + [-9.794, 18.171], + [-14.796, 13.05], + [-14.796, 0.99], + [-14.796, -13.051], + [-9.794, -18.171], + [4.174, -18.171], + [6.676, -17.016] + ], + "c": true + } + ] + } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [1, 1, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { "a": 0, "k": 2.5, "ix": 5 }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [1, 1, 1], "ix": 4 }, + "o": { "a": 0, "k": 0, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [19.797, 23.455], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 2", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 2, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 45.0000018328876, + "st": 0, + "ct": 1, + "bm": 0 + } + ], + "markers": [], + "props": {} +} diff --git a/frontend/public/lotties/notification-bell.json b/frontend/public/lotties/notification-bell.json new file mode 100644 index 000000000..56d4c9950 --- /dev/null +++ b/frontend/public/lotties/notification-bell.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":0,"op":90,"w":500,"h":500,"nm":"system-regular-46-notification-bell","ddd":0,"assets":[{"id":"comp_1","nm":"hover-bell","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.231],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.313],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":7.041,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":19,"s":[-53]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":29,"s":[-20]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":39,"s":[-62]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":49,"s":[-20]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":58,"s":[-62]},{"i":{"x":[0.283],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":69.713,"s":[14]},{"t":79,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.231,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[249.998,67.334,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.313,"y":1},"o":{"x":0.333,"y":0},"t":7.041,"s":[269.998,67.334,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":19,"s":[111.998,67.334,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.313,"y":1},"o":{"x":0.333,"y":0},"t":29,"s":[121.998,67.334,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":39,"s":[111.998,67.334,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":49,"s":[121.998,67.334,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":58,"s":[111.998,67.334,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.283,"y":1},"o":{"x":0.333,"y":0},"t":69.713,"s":[261.998,67.334,0],"to":[0,0,0],"ti":[0,0,0]},{"t":79,"s":[249.998,67.334,0]}],"ix":2,"l":2},"a":{"a":0,"k":[249.998,67.334,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,6.468],[0,0],[60.406,0],[0,0],[0,-60.406],[0,0],[2.893,-5.786],[0,0],[0,0]],"o":[[-2.893,-5.786],[0,0],[0,-60.406],[0,0],[-60.406,0],[0,0],[0,6.468],[0,0],[0,0],[0,0]],"v":[[113.773,55.671],[109.375,37.038],[109.375,-20.834],[0.001,-130.21],[-0.001,-130.21],[-109.375,-20.834],[-109.375,37.038],[-113.773,55.671],[-151.042,130.21],[151.042,130.21]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-46-notification-bell').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[249.998,229.166],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-20.832],[0,20.832]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-46-notification-bell').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[249.998,78.125],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":90,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":11,"s":[27]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":24,"s":[-26]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":32,"s":[29]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":42,"s":[-28]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":52,"s":[29]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":61,"s":[-28]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":73,"s":[29]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":83,"s":[-6]},{"t":90,"s":[0]}],"ix":10},"p":{"a":0,"k":[249.998,182.041,0],"ix":2,"l":2},"a":{"a":0,"k":[0,-219,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-25.889,0],[0,25.889],[0,0]],"o":[[0,0],[0,25.889],[25.888,0],[0,0],[0,0]],"v":[[-46.751,-119.666],[-46.875,-5.21],[0.001,41.666],[46.875,-5.21],[46.998,-119.666]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-46-notification-bell').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":1,"s":[23]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":11,"s":[33]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":18,"s":[22.538]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":24,"s":[21]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[22.538]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":32,"s":[33]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":37,"s":[22.538]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":42,"s":[21]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[22.538]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":52,"s":[33]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":57,"s":[22.538]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":61,"s":[21]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":67,"s":[22.538]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":73,"s":[33]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":78,"s":[22.538]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":83,"s":[21]},{"t":90,"s":[22.538]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":11,"s":[79]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":18,"s":[77.077]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":24,"s":[68]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[77.077]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":32,"s":[79]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":37,"s":[77.077]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":42,"s":[68]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[77.077]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":52,"s":[79]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":57,"s":[77.077]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":61,"s":[68]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":67,"s":[77.077]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":73,"s":[79]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":78,"s":[77.077]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":83,"s":[75]},{"t":90,"s":[77.077]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":1,"op":90,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.038,250.002,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0.43],[0,0],[-2.48,0],[0,-2.48],[0,0],[-0.19,-0.38],[0,0]],"o":[[0,0],[0.19,-0.38],[0,0],[0,-2.48],[2.48,0],[0,0],[0,0.42],[0,0],[0,0]],"v":[[-6.042,4.5],[-4.792,2.01],[-4.502,0.78],[-4.502,-2],[-0.002,-6.5],[4.498,-2],[4.498,0.78],[4.788,2.01],[6.038,4.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0.19],[0,0],[2.96,0.37],[0,0],[0.41,0],[0,-0.41],[0,0],[0,-3.05],[0,0],[0.08,-0.17],[0,0],[-0.13,-0.22],[-0.26,0],[0,0],[-0.14,0.23],[0.12,0.23]],"o":[[-0.08,-0.18],[0,0],[0,-3.05],[0,0],[0,-0.41],[-0.41,0],[0,0],[-2.96,0.37],[0,0],[0,0.19],[0,0],[-0.12,0.23],[0.14,0.22],[0,0],[0.26,0],[0.14,-0.22],[0,0]],"v":[[6.128,1.34],[5.998,0.78],[5.998,-2],[0.748,-7.95],[0.748,-9.25],[-0.002,-10],[-0.752,-9.25],[-0.752,-7.95],[-6.002,-2],[-6.002,0.78],[-6.132,1.34],[-7.922,4.92],[-7.892,5.65],[-7.252,6],[7.248,6],[7.888,5.64],[7.918,4.91]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-46-notification-bell').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.038,250.002,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.83,0],[0,0.83],[0,0],[0,0]],"o":[[0,0.83],[-0.83,0],[0,0],[0,0],[0,0]],"v":[[1.498,7],[-0.002,8.5],[-1.502,7],[-1.501,5.544],[1.499,5.544]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-1.65,0],[0,1.65],[0,0]],"o":[[0,0],[0,1.65],[1.65,0],[0,0],[0,0]],"v":[[-3.001,5.088],[-3.002,7],[-0.002,10],[2.998,7],[2.999,5.088]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-46-notification-bell').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.038,250.002,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0.43],[0,0],[-2.48,0],[0,-2.48],[0,0],[-0.19,-0.38],[0,0]],"o":[[0,0],[0.19,-0.38],[0,0],[0,-2.48],[2.48,0],[0,0],[0,0.42],[0,0],[0,0]],"v":[[-6.042,4.5],[-4.792,2.01],[-4.502,0.78],[-4.502,-2],[-0.002,-6.5],[4.498,-2],[4.498,0.78],[4.788,2.01],[6.038,4.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0.19],[0,0],[2.96,0.37],[0,0],[0.41,0],[0,-0.41],[0,0],[0,-3.05],[0,0],[0.08,-0.17],[0,0],[-0.13,-0.22],[-0.26,0],[0,0],[-0.14,0.23],[0.12,0.23]],"o":[[-0.08,-0.18],[0,0],[0,-3.05],[0,0],[0,-0.41],[-0.41,0],[0,0],[-2.96,0.37],[0,0],[0,0.19],[0,0],[-0.12,0.23],[0.14,0.22],[0,0],[0.26,0],[0.14,-0.22],[0,0]],"v":[[6.128,1.34],[5.998,0.78],[5.998,-2],[0.748,-7.95],[0.748,-9.25],[-0.002,-10],[-0.752,-9.25],[-0.752,-7.95],[-6.002,-2],[-6.002,0.78],[-6.132,1.34],[-7.922,4.92],[-7.892,5.65],[-7.252,6],[7.248,6],[7.888,5.64],[7.918,4.91]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-46-notification-bell').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":90,"op":300,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.038,250.002,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.83,0],[0,0.83],[0,0],[0,0]],"o":[[0,0.83],[-0.83,0],[0,0],[0,0],[0,0]],"v":[[1.498,7],[-0.002,8.5],[-1.502,7],[-1.501,5.544],[1.499,5.544]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-1.65,0],[0,1.65],[0,0]],"o":[[0,0],[0,1.65],[1.65,0],[0,0],[0,0]],"v":[[-3.001,5.088],[-3.002,7],[-0.002,10],[2.998,7],[2.999,5.088]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-46-notification-bell').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":90,"op":300,"st":0,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"control","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"primary","np":3,"mn":"ADBE Color Control","ix":1,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]}],"ip":0,"op":291,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"hover-bell","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":100,"st":0,"bm":0}],"markers":[{"tm":0,"cm":"default:hover-bell","dr":90}],"props":{}} \ No newline at end of file diff --git a/frontend/public/lotties/secret-scan.json b/frontend/public/lotties/secret-scan.json new file mode 100644 index 000000000..36740c799 --- /dev/null +++ b/frontend/public/lotties/secret-scan.json @@ -0,0 +1 @@ +{"v":"5.7.5","fr":100,"ip":0,"op":152,"w":48,"h":48,"nm":"Comp 1","ddd":0,"metadata":{},"assets":[{"id":"0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0},{"ddd":0,"refId":"1","w":376,"h":32,"ind":2,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[62,234],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":150,"s":[100],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0,"parent":1},{"ddd":0,"ind":3,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0},{"ddd":0,"refId":"2","w":376,"h":376,"ind":4,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[62,62],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":150,"s":[100],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0,"parent":3},{"ddd":0,"ind":5,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0},{"ddd":0,"refId":"3","w":376,"h":32,"ind":6,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[62,234],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"h":1},{"t":2,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0,"parent":5},{"ddd":0,"ind":7,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0},{"ddd":0,"refId":"4","w":376,"h":376,"ind":8,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[62,62],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"h":1},{"t":2,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0,"parent":7},{"ddd":0,"ind":9,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0},{"ddd":0,"refId":"5","w":504,"h":503,"ind":10,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[-2,-1],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":150,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0,"parent":9},{"ddd":0,"ind":11,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0},{"ddd":0,"refId":"6","w":504,"h":503,"ind":12,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[-2,-1],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":150,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0,"parent":11}]},{"id":"1","layers":[{"ddd":0,"ind":13,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-62,-234],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[171.875,15.65],[-171.875,15.65],[-187.525,0],[-171.875,-15.65],[171.875,-15.65],[187.525,0],[171.875,15.65]],"i":[[0,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-8.643],[8.644,0]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[8.644,0],[0,8.643],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[250.0030059814453,250.0030059814453],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":153,"st":0,"bm":0}]},{"id":"2","layers":[{"ddd":0,"ind":14,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-62,-62],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[46.875,62.525],[31.225,46.875],[31.225,-26.041],[26.041,-31.225],[-46.875,-31.225],[-62.525,-46.875],[-46.875,-62.525],[26.041,-62.525],[62.525,-26.041],[62.525,46.875],[46.875,62.525]],"i":[[0,0],[0,8.643],[0,0],[2.858,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-20.117],[0,0],[8.644,0]],"o":[[-8.644,0],[0,0],[0,-2.858],[0,0],[-8.644,0],[0,-8.643],[0,0],[20.117,0],[0,0],[0,8.643],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[374.998,125.001],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Group 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[46.875,62.524],[-26.042,62.524],[-62.525,26.041],[-62.525,-46.875],[-46.875,-62.524],[-31.224,-46.875],[-31.224,26.041],[-26.042,31.225],[46.875,31.225],[62.525,46.875],[46.875,62.524]],"i":[[0,0],[0,0],[0,20.117],[0,0],[-8.644,0],[0,-8.643],[0,0],[-2.857,0],[0,0],[0,-8.643],[8.644,0]],"o":[[0,0],[-20.117,0],[0,0],[0,-8.643],[8.644,0],[0,0],[0,2.858],[0,0],[8.644,0],[0,8.643],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[124.998,374.999],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Group 3","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-46.875,62.524],[-62.525,46.875],[-62.525,-26.042],[-26.042,-62.524],[46.875,-62.524],[62.525,-46.875],[46.875,-31.225],[-26.042,-31.225],[-31.224,-26.042],[-31.224,46.875],[-46.875,62.524]],"i":[[0,0],[0,8.643],[0,0],[-20.117,0],[0,0],[0,-8.643],[8.644,0],[0,0],[0,-2.858],[0,0],[8.644,0]],"o":[[-8.644,0],[0,0],[0,-20.117],[0,0],[8.644,0],[0,8.643],[0,0],[-2.857,0],[0,0],[0,8.643],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[124.998,124.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Group 4","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[26.042,62.525],[-46.875,62.525],[-62.525,46.875],[-46.875,31.225],[26.042,31.225],[31.224,26.041],[31.224,-46.875],[46.875,-62.525],[62.525,-46.875],[62.525,26.041],[26.042,62.525]],"i":[[0,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,2.858],[0,0],[-8.644,0],[0,-8.643],[0,0],[20.117,0]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[2.857,0],[0,0],[0,-8.643],[8.644,0],[0,0],[0,20.117],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[375.004,375.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.001,250.001],"ix":2},"a":{"a":0,"k":[250.001,250.001],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":153,"st":0,"bm":0}]},{"id":"3","layers":[{"ddd":0,"ind":15,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-62,-234],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[171.875,15.65],[-171.875,15.65],[-187.525,0],[-171.875,-15.65],[171.875,-15.65],[187.525,0],[171.875,15.65]],"i":[[0,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-8.643],[8.644,0]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[8.644,0],[0,8.643],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[250.0030059814453,250.0030059814453],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":153,"st":0,"bm":0}]},{"id":"4","layers":[{"ddd":0,"ind":16,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-62,-62],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[46.875,62.525],[31.225,46.875],[31.225,-26.041],[26.041,-31.225],[-46.875,-31.225],[-62.525,-46.875],[-46.875,-62.525],[26.041,-62.525],[62.525,-26.041],[62.525,46.875],[46.875,62.525]],"i":[[0,0],[0,8.643],[0,0],[2.858,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-20.117],[0,0],[8.644,0]],"o":[[-8.644,0],[0,0],[0,-2.858],[0,0],[-8.644,0],[0,-8.643],[0,0],[20.117,0],[0,0],[0,8.643],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[374.998,125.001],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Group 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[46.875,62.524],[-26.042,62.524],[-62.525,26.041],[-62.525,-46.875],[-46.875,-62.524],[-31.224,-46.875],[-31.224,26.041],[-26.042,31.225],[46.875,31.225],[62.525,46.875],[46.875,62.524]],"i":[[0,0],[0,0],[0,20.117],[0,0],[-8.644,0],[0,-8.643],[0,0],[-2.857,0],[0,0],[0,-8.643],[8.644,0]],"o":[[0,0],[-20.117,0],[0,0],[0,-8.643],[8.644,0],[0,0],[0,2.858],[0,0],[8.644,0],[0,8.643],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[124.998,374.999],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Group 3","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-46.875,62.524],[-62.525,46.875],[-62.525,-26.042],[-26.042,-62.524],[46.875,-62.524],[62.525,-46.875],[46.875,-31.225],[-26.042,-31.225],[-31.224,-26.042],[-31.224,46.875],[-46.875,62.524]],"i":[[0,0],[0,8.643],[0,0],[-20.117,0],[0,0],[0,-8.643],[8.644,0],[0,0],[0,-2.858],[0,0],[8.644,0]],"o":[[-8.644,0],[0,0],[0,-20.117],[0,0],[8.644,0],[0,8.643],[0,0],[-2.857,0],[0,0],[0,8.643],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[124.998,124.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Group 4","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[26.042,62.525],[-46.875,62.525],[-62.525,46.875],[-46.875,31.225],[26.042,31.225],[31.224,26.041],[31.224,-46.875],[46.875,-62.525],[62.525,-46.875],[62.525,26.041],[26.042,62.525]],"i":[[0,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,2.858],[0,0],[-8.644,0],[0,-8.643],[0,0],[20.117,0]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[2.857,0],[0,0],[0,-8.643],[8.644,0],[0,0],[0,20.117],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[375.004,375.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.001,250.001],"ix":2},"a":{"a":0,"k":[250.001,250.001],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":153,"st":0,"bm":0}]},{"id":"5","layers":[{"ddd":0,"ind":17,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[2,1],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","nm":"Rectangle 1","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"t":0,"s":[343,0],"i":{"x":[0.667],"y":[0.042]},"o":{"x":[0.333],"y":[0]}},{"t":12,"s":[352.076,52],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0.346]}},{"t":37,"s":[406,0],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":68,"s":[406,119],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":100,"s":[406,0],"i":{"x":[0.667],"y":[0.639]},"o":{"x":[0.333],"y":[0]}},{"t":133,"s":[354.333,30],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0.824]}},{"t":150,"s":[343,0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[-0.504,-0.503],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":1,"k":[{"t":0,"s":[250.064,250.003],"i":{"x":[0.365],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":37,"s":[250.064,56.003],"i":{"x":[0.431],"y":[1]},"o":{"x":[0.575],"y":[0]}},{"t":100,"s":[250.064,443.003],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.803],"y":[0]}},{"t":150,"s":[250.064,250.003],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":153,"st":0,"bm":0}]},{"id":"6","layers":[{"ddd":0,"ind":18,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[2,1],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","nm":".primary.design","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[-46.875,-46.875],[26.041,-46.875],[46.875,-26.041],[46.875,46.875]],"i":[[0,0],[0,0],[0,-11.506],[0,0]],"o":[[0,0],[11.506,0],[0,0],[0,0]]}],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]}},{"t":37,"s":[{"c":false,"v":[[-13.917,-79.832],[58.999,-79.832],[79.832,-58.999],[79.832,13.917]],"i":[[0,0],[0,0],[0,-11.506],[0,0]],"o":[[0,0],[11.506,0],[0,0],[0,0]]}],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]}},{"t":100,"s":[{"c":false,"v":[[-13.917,-79.832],[58.999,-79.832],[79.832,-58.999],[79.832,13.917]],"i":[[0,0],[0,0],[0,-11.506],[0,0]],"o":[[0,0],[11.506,0],[0,0],[0,0]]}],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]}},{"t":150,"s":[{"c":false,"v":[[-46.875,-46.875],[26.041,-46.875],[46.875,-26.041],[46.875,46.875]],"i":[[0,0],[0,0],[0,-11.506],[0,0]],"o":[[0,0],[11.506,0],[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[-203.126,296.872],[-276.042,296.872],[-296.875,276.039],[-296.875,203.123]],"i":[[0,0],[0,0],[0,11.506],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,0]]}],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]}},{"t":37,"s":[{"c":false,"v":[[-236.084,329.83],[-309,329.83],[-329.833,308.997],[-329.833,236.081]],"i":[[0,0],[0,0],[0,11.506],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,0]]}],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]}},{"t":100,"s":[{"c":false,"v":[[-236.084,329.83],[-309,329.83],[-329.833,308.997],[-329.833,236.081]],"i":[[0,0],[0,0],[0,11.506],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,0]]}],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]}},{"t":150,"s":[{"c":false,"v":[[-203.126,296.872],[-276.042,296.872],[-296.875,276.039],[-296.875,203.123]],"i":[[0,0],[0,0],[0,11.506],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 3","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[-296.875,46.872],[-296.875,-26.044],[-276.042,-46.878],[-203.126,-46.878]],"i":[[0,0],[0,0],[-11.506,0],[0,0]],"o":[[0,0],[0,-11.506],[0,0],[0,0]]}],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]}},{"t":37,"s":[{"c":false,"v":[[-329.833,13.914],[-329.833,-59.002],[-309,-79.835],[-236.084,-79.835]],"i":[[0,0],[0,0],[-11.506,0],[0,0]],"o":[[0,0],[0,-11.506],[0,0],[0,0]]}],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]}},{"t":100,"s":[{"c":false,"v":[[-329.833,13.914],[-329.833,-59.002],[-309,-79.835],[-236.084,-79.835]],"i":[[0,0],[0,0],[-11.506,0],[0,0]],"o":[[0,0],[0,-11.506],[0,0],[0,0]]}],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]}},{"t":150,"s":[{"c":false,"v":[[-296.875,46.872],[-296.875,-26.044],[-276.042,-46.878],[-203.126,-46.878]],"i":[[0,0],[0,0],[-11.506,0],[0,0]],"o":[[0,0],[0,-11.506],[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 4","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[46.881,203.127],[46.881,276.043],[26.048,296.877],[-46.868,296.877]],"i":[[0,0],[0,0],[11.506,0],[0,0]],"o":[[0,0],[0,11.506],[0,0],[0,0]]}],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]}},{"t":37,"s":[{"c":false,"v":[[79.839,236.085],[79.839,309.001],[59.006,329.834],[-13.91,329.834]],"i":[[0,0],[0,0],[11.506,0],[0,0]],"o":[[0,0],[0,11.506],[0,0],[0,0]]}],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]}},{"t":100,"s":[{"c":false,"v":[[79.839,236.085],[79.839,309.001],[59.006,329.834],[-13.91,329.834]],"i":[[0,0],[0,0],[11.506,0],[0,0]],"o":[[0,0],[0,11.506],[0,0],[0,0]]}],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]}},{"t":150,"s":[{"c":false,"v":[[46.881,203.127],[46.881,276.043],[26.048,296.877],[-46.868,296.877]],"i":[[0,0],[0,0],[11.506,0],[0,0]],"o":[[0,0],[0,11.506],[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.0009765625,250.00051879882812],"ix":2},"a":{"a":0,"k":[-124.99699974060059,124.99950790405273],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":153,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":12345679,"ty":4,"nm":"Group Layer 8","sr":1,"ks":{"p":{"a":0,"k":[35.04,44.580983606557375,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[4.918032786885246,4.918032786885246,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[220.741,37.184],[225.501,35.896],[228.749,32.36800000000001],[229.981,27.216000000000008],[228.749,22.12],[225.501,18.592],[220.741,17.304],[215.981,18.592],[212.677,22.12],[211.501,27.216000000000008],[212.677,32.36800000000001],[215.981,35.896],[220.741,37.184],[220.741,37.184],[220.741,37.184]],"i":[[0,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.382000000000062,0.8586999999999989],[1.79200000000003,0],[1.418999999999983,-0.8586999999999989],[0.8220000000000027,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.382000000000062,-0.8586999999999989],[0.8220000000000027,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.418999999999983,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[221.357,43.06400000000001],[214.917,41.608],[210.49300000000005,37.408],[211.221,36.232],[211.221,42.392],[205.173,42.392],[205.173,0],[211.501,0],[211.501,18.36800000000001],[210.49300000000005,16.912000000000006],[214.973,12.88],[221.357,11.424000000000007],[229.085,13.49600000000001],[234.51700000000005,19.152],[236.533,27.216000000000008],[234.51700000000005,35.28],[229.141,40.992],[221.357,43.06400000000001],[221.357,43.06400000000001],[221.357,43.06400000000001]],"i":[[0,0],[1.942000000000007,0.9706999999999937],[1.045999999999935,1.829300000000003],[-0.2426666666666506,0.3919999999999959],[0,-2.053333333333327],[2.015999999999963,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.122666666666674],[0.3360000000000127,0.4853333333333296],[-1.865999999999985,0.9707000000000079],[-2.38900000000001,0],[-2.277000000000044,-1.3813000000000102],[-1.30600000000004,-2.389300000000006],[0,-2.986699999999999],[1.343999999999937,-2.389300000000006],[2.27800000000002,-1.4187000000000012],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.351999999999975,0],[-1.903999999999996,-0.9707000000000079],[0.2426666666666506,-0.3919999999999959],[0,2.053333333333327],[-2.015999999999963,0],[0,-14.13066666666667],[2.109333333333325,0],[0,6.122666666666667],[-0.3360000000000127,-0.4853333333333296],[1.120000000000005,-1.717300000000009],[1.867000000000075,-0.9706999999999937],[2.875,0],[2.314999999999941,1.381299999999996],[1.343999999999937,2.389300000000006],[0,2.986699999999999],[-1.30600000000004,2.389300000000006],[-2.27699999999993,1.381299999999996],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[181.87,43.06400000000001],[176.438,42],[172.854,38.976],[171.566,34.384],[172.63,29.960000000000008],[176.046,26.656000000000006],[181.814,24.752],[192.342,23.016000000000005],[192.342,28],[183.046,29.624],[179.35,31.248],[178.174,34.16],[179.462,37.016000000000005],[182.878,38.08],[187.35799999999995,36.96000000000001],[190.38199999999995,33.992],[191.446,29.792],[191.446,22.008],[189.766,18.36800000000001],[185.398,16.912000000000006],[180.974,18.256],[178.23,21.616],[172.966,18.98400000000001],[175.71,15.064000000000007],[180.134,12.376],[185.566,11.424000000000007],[191.894,12.768],[196.206,16.52],[197.774,22.008],[197.774,42.392],[191.726,42.392],[191.726,36.904],[193.014,37.072],[190.27,40.264],[186.518,42.336],[181.87,43.06400000000001],[181.87,43.06400000000001],[181.87,43.06400000000001]],"i":[[0,0],[1.567999999999984,0.7092999999999989],[0.8589999999999236,1.2693000000000012],[0,1.7547],[-0.7089999999999463,1.306699999999992],[-1.530000000000086,0.8960000000000008],[-2.313999999999965,0.3733000000000004],[-3.509333333333302,0.5786666666666633],[0,-1.661333333333332],[3.098666666666645,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8579999999999472,-0.7467000000000041],[-1.381000000000085,0],[-1.268999999999892,0.7466999999999899],[-0.7089999999999463,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829999999999927,0],[1.269999999999982,-0.8960000000000008],[0.5979999999999563,-1.381299999999996],[1.754666666666708,0.8773333333333255],[-1.269000000000005,1.11999999999999],[-1.680000000000064,0.6346999999999952],[-1.903999999999996,0],[-1.828999999999951,-0.8960000000000008],[-1.008000000000038,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999963,0],[0,1.829333333333338],[-0.4293333333333749,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.717999999999961,0],[0,0],[0,0]],"o":[[-2.052999999999997,0],[-1.529999999999973,-0.7467000000000041],[-0.8580000000000609,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999573,-1.306700000000006],[1.530999999999949,-0.8960000000000008],[3.509333333333302,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666645,0.541333333333327],[-1.680000000000064,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.8959999999999582,0.7092999999999989],[1.717999999999961,0],[1.307000000000016,-0.7467000000000041],[0.7100000000000364,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.081999999999994,-0.9707000000000079],[-1.680000000000064,0],[-1.232000000000085,0.8586999999999989],[-1.754666666666708,-0.8773333333333255],[0.5599999999999454,-1.493300000000005],[1.269999999999982,-1.157300000000006],[1.717999999999961,-0.6347000000000094],[2.389999999999986,0],[1.866999999999962,0.8960000000000008],[1.045999999999935,1.568000000000012],[0,6.794666666666672],[-2.015999999999963,0],[0,-1.829333333333338],[0.4293333333333749,0.05599999999999739],[-0.70900000000006,1.2319999999999993],[-1.081999999999994,0.8960000000000008],[-1.380999999999972,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[159.072,42.392],[159.072,0],[165.4,0],[165.4,42.392],[159.072,42.392],[159.072,42.392],[159.072,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-14.13066666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,14.13066666666667],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[138.952,43.06400000000001],[130.888,40.992],[125.456,35.28],[123.496,27.16],[125.456,19.040000000000006],[130.832,13.49600000000001],[138.448,11.424000000000007],[144.552,12.600000000000009],[149.088,15.848],[151.888,20.49600000000001],[152.896,26.096],[152.84,27.608],[152.616,29.064000000000007],[128.48,29.064000000000007],[128.48,24.024],[149.032,24.024],[146.008,26.320000000000007],[145.616,21.448000000000008],[142.816,18.032],[138.448,16.744],[133.968,18.032],[130.944,21.616],[130.104,27.216000000000008],[130.944,32.592],[134.192,36.176],[139.008,37.464],[143.65599999999995,36.232],[146.736,33.040000000000006],[151.888,35.56],[149.088,39.42400000000001],[144.60799999999995,42.11200000000001],[138.952,43.06400000000001],[138.952,43.06400000000001],[138.952,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.307000000000016,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.836999999999989,0],[-1.79200000000003,-0.784000000000006],[-1.231999999999971,-1.381299999999996],[-0.6349999999999909,-1.754700000000014],[0,-1.978700000000003],[0.03699999999992087,-0.5227000000000004],[0.1119999999999663,-0.4480000000000075],[8.04533333333336,0],[0,1.680000000000007],[-6.850666666666712,0],[1.008000000000038,-0.7653333333333308],[0.6349999999999909,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.680000000000064,0],[1.307000000000016,-0.8586999999999989],[0.70900000000006,-1.567999999999998],[-0.1490000000000009,-2.202700000000007],[-0.7469999999999573,-1.530699999999996],[-1.380999999999972,-0.8586999999999989],[-1.79200000000003,0],[-1.268999999999892,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333386,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.755000000000109,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.315000000000055,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999937,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.277000000000044,0],[1.79200000000003,0.7839999999999918],[1.232000000000085,1.344000000000008],[0.6720000000000255,1.7547],[0,0.4852999999999952],[-0.03700000000003456,0.5227000000000004],[-8.04533333333336,0],[0,-1.680000000000007],[6.850666666666712,0],[-1.008000000000038,0.7653333333333308],[0.3729999999999336,-1.829300000000003],[-0.59699999999998,-1.456000000000003],[-1.232000000000085,-0.8586999999999989],[-1.67999999999995,0],[-1.306999999999903,0.8213000000000079],[-0.7089999999999463,1.530699999999996],[-0.1870000000000118,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418999999999983,0.8586999999999989],[1.828999999999951,0],[1.307000000000016,-0.8212999999999937],[1.717333333333386,0.8400000000000034],[-0.59699999999998,1.4187000000000012],[-1.231999999999971,1.11999999999999],[-1.716999999999985,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,7.951999999999998],[111.001,0.6720000000000041],[117.329,0.6720000000000041],[117.329,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,42.392],[111.001,12.096],[117.329,12.096],[117.329,42.392],[111.001,42.392],[111.001,42.392],[111.001,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[101.41,42.72800000000001],[94.01800000000003,40.040000000000006],[91.38599999999997,32.48],[91.38599999999997,17.808000000000007],[86.06600000000003,17.808000000000007],[86.06600000000003,12.096],[86.90599999999995,12.096],[90.21000000000004,10.864],[91.38599999999997,7.504000000000005],[91.38599999999997,5.152000000000001],[97.71400000000006,5.152000000000001],[97.71400000000006,12.096],[104.602,12.096],[104.602,17.808000000000007],[97.71400000000006,17.808000000000007],[97.71400000000006,32.2],[98.21799999999996,34.888000000000005],[99.84199999999998,36.568],[102.754,37.128],[103.76200000000006,37.072],[104.826,36.96000000000001],[104.826,42.392],[103.09,42.616],[101.41,42.72800000000001],[101.41,42.72800000000001],[101.41,42.72800000000001]],"i":[[0,0],[1.754999999999995,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333312,0],[0,1.903999999999996],[-0.2799999999999727,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999935,0],[0,-1.903999999999996],[2.295999999999935,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7469999999999573,-0.4106999999999914],[-1.19500000000005,0],[-0.3730000000000473,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6349999999999909,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333312,0],[0,-1.903999999999996],[0.2799999999999727,0],[1.419000000000096,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999935,0],[0,1.903999999999996],[-2.295999999999935,0],[0,4.797333333333327],[0,1.045299999999997],[0.3360000000000127,0.7092999999999989],[0.7470000000000709,0.3733000000000004],[0.2989999999999782,0],[0.3729999999999336,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[78.71499999999997,42.72800000000001],[71.32299999999998,40.040000000000006],[68.69100000000003,32.48],[68.69100000000003,17.808000000000007],[63.37099999999998,17.808000000000007],[63.37099999999998,12.096],[64.21100000000001,12.096],[67.51499999999999,10.864],[68.69100000000003,7.504000000000005],[68.69100000000003,5.152000000000001],[75.019,5.152000000000001],[75.019,12.096],[81.90700000000004,12.096],[81.90700000000004,17.808000000000007],[75.019,17.808000000000007],[75.019,32.2],[75.52300000000002,34.888000000000005],[77.14699999999999,36.568],[80.05900000000003,37.128],[81.06700000000001,37.072],[82.13099999999997,36.96000000000001],[82.13099999999997,42.392],[80.39499999999998,42.616],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001]],"i":[[0,0],[1.754000000000019,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7470000000000141,-0.4106999999999914],[-1.19500000000005,0],[-0.3740000000000236,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6340000000000146,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173999999999978,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418000000000006,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7460000000000377,0.3733000000000004],[0.297999999999945,0],[0.3730000000000473,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,37.184],[48.94799999999998,35.896],[52.19600000000003,32.36800000000001],[53.428,27.216000000000008],[52.19600000000003,22.12],[48.94799999999998,18.592],[44.18799999999999,17.304],[39.428,18.592],[36.12400000000002,22.12],[34.94799999999998,27.216000000000008],[36.12400000000002,32.36800000000001],[39.428,35.896],[44.18799999999999,37.184],[44.18799999999999,37.184],[44.18799999999999,37.184]],"i":[[0,0],[-1.381999999999948,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.381000000000029,0.8586999999999989],[1.79200000000003,0],[1.418000000000006,-0.8586999999999989],[0.8209999999999695,-1.493300000000005],[0,-1.904000000000011],[-0.7840000000000487,-1.5307000000000102],[-1.382000000000005,-0.8586999999999989],[-1.754999999999995,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.381000000000029,-0.8586999999999989],[0.8209999999999695,-1.5307000000000102],[0,-1.904000000000011],[-0.7840000000000487,-1.493300000000005],[-1.381999999999948,-0.8586999999999989],[-1.754999999999995,0],[-1.382000000000005,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.418000000000006,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,43.06400000000001],[36.18000000000001,40.992],[30.46800000000002,35.336],[28.33999999999997,27.216000000000008],[30.46800000000002,19.096],[36.18000000000001,13.49600000000001],[44.18799999999999,11.424000000000007],[52.19600000000003,13.49600000000001],[57.85199999999998,19.096],[59.98000000000002,27.216000000000008],[57.85199999999998,35.392],[52.139999999999986,41.048],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001]],"i":[[0,0],[2.425999999999988,1.381299999999996],[1.418000000000006,2.389300000000006],[0,3.024000000000001],[-1.41900000000004,2.352000000000004],[-2.389999999999986,1.343999999999994],[-2.949999999999989,0],[-2.352000000000032,-1.3813000000000102],[-1.381999999999948,-2.389300000000006],[0,-3.061300000000003],[1.418000000000006,-2.389299999999992],[2.38900000000001,-1.381299999999996],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.911999999999978,0],[-2.389999999999986,-1.381299999999996],[-1.41900000000004,-2.389299999999992],[0,-3.061300000000003],[1.418000000000006,-2.389300000000006],[2.38900000000001,-1.3813000000000102],[2.98599999999999,0],[2.388999999999953,1.343999999999994],[1.418000000000006,2.352000000000004],[0,3.061299999999989],[-1.418999999999983,2.389300000000006],[-2.389999999999986,1.343999999999994],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.608000000000004,0.6720000000000041],[6.608000000000004,36.512],[24.639999999999986,36.512],[24.639999999999986,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.202666666666687,0],[0,-11.94666666666666],[-6.01066666666668,0],[0,-1.959999999999994],[8.21333333333331,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.202666666666687,0],[0,11.94666666666667],[6.01066666666668,0],[0,1.959999999999994],[-8.21333333333331,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[98.08047485351562,-21.67217254638672],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[246.681,42.392],[246.681,0],[253.009,0],[253.009,18.032],[252.001,17.248],[255.585,12.936000000000007],[261.297,11.424000000000007],[267.23299999999995,12.88],[271.265,16.912000000000006],[272.721,22.792],[272.721,42.392],[266.449,42.392],[266.449,24.528000000000006],[265.553,20.664],[263.201,18.2],[259.729,17.304],[256.25699999999995,18.2],[253.849,20.664],[253.009,24.528000000000006],[253.009,42.392],[246.681,42.392],[246.681,42.392],[246.681,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.010666666666665],[0.3360000000000127,0.2613333333333259],[-1.643000000000029,0.9706999999999937],[-2.166000000000054,0],[-1.717999999999961,-0.9706999999999937],[-0.9710000000000036,-1.717300000000009],[0,-2.202699999999993],[0,-6.533333333333331],[2.090666666666721,0],[0,5.954666666666668],[0.59699999999998,1.045299999999997],[1.007999999999925,0.5600000000000023],[1.305999999999926,0],[1.045000000000073,-0.5973000000000042],[0.59699999999998,-1.082700000000003],[0,-1.493300000000005],[0,-5.954666666666668],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,6.010666666666665],[-0.3360000000000127,-0.2613333333333259],[0.7459999999999809,-1.903999999999996],[1.641999999999967,-1.0080000000000098],[2.240000000000009,0],[1.717000000000098,0.9707000000000079],[0.9700000000000273,1.717299999999994],[0,6.533333333333331],[-2.090666666666721,0],[0,-5.954666666666668],[0,-1.5307000000000102],[-0.5599999999999454,-1.082700000000003],[-1.008000000000038,-0.5973000000000042],[-1.270000000000095,0],[-1.007999999999953,0.5600000000000023],[-0.5600000000000023,1.082700000000003],[0,5.954666666666668],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[237.089,42.72800000000001],[229.697,40.040000000000006],[227.065,32.48],[227.065,17.808000000000007],[221.745,17.808000000000007],[221.745,12.096],[222.585,12.096],[225.889,10.864],[227.065,7.504000000000005],[227.065,5.152000000000001],[233.393,5.152000000000001],[233.393,12.096],[240.281,12.096],[240.281,17.808000000000007],[233.393,17.808000000000007],[233.393,32.2],[233.897,34.888000000000005],[235.521,36.568],[238.433,37.128],[239.441,37.072],[240.505,36.96000000000001],[240.505,42.392],[238.769,42.616],[237.089,42.72800000000001],[237.089,42.72800000000001],[237.089,42.72800000000001]],"i":[[0,0],[1.755000000000052,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7459999999999809,-0.4106999999999914],[-1.194000000000017,0],[-0.3729999999999905,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6350000000000477,-0.07469999999999288],[0.48599999999999,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.753999999999962,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418999999999983,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7470000000000141,0.3733000000000004],[0.2989999999999782,0],[0.3740000000000236,-0.03730000000000189],[0,1.810666666666663],[-0.5220000000000482,0.0747000000000071],[-0.6339999999999577,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,7.951999999999998],[210.259,0.6720000000000041],[216.587,0.6720000000000041],[216.587,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,42.392],[210.259,12.096],[216.587,12.096],[216.587,42.392],[210.259,42.392],[210.259,42.392],[210.259,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[169.688,42.392],[159.272,12.096],[165.992,12.096],[173.944,36.232],[171.592,36.232],[179.712,12.096],[185.48,12.096],[193.544,36.232],[191.192,36.232],[199.2,12.096],[205.92,12.096],[195.448,42.392],[189.736,42.392],[181.56,17.696],[183.632,17.696],[175.456,42.392],[169.688,42.392],[169.688,42.392],[169.688,42.392]],"i":[[0,0],[3.47199999999998,10.09866666666667],[-2.240000000000009,0],[-2.650666666666666,-8.045333333333332],[0.7839999999999918,0],[-2.706666666666649,8.045333333333332],[-1.922666666666657,0],[-2.687999999999988,-8.045333333333332],[0.7839999999999918,0],[-2.669333333333327,8.045333333333332],[-2.240000000000009,0],[3.490666666666641,-10.09866666666667],[1.903999999999996,0],[2.725333333333367,8.232],[-0.6906666666666865,0],[2.72533333333331,-8.232],[1.922666666666657,0],[0,0],[0,0]],"o":[[-3.47199999999998,-10.09866666666666],[2.240000000000009,0],[2.650666666666666,8.045333333333332],[-0.7839999999999918,0],[2.706666666666649,-8.045333333333332],[1.922666666666657,0],[2.687999999999988,8.045333333333332],[-0.7839999999999918,0],[2.669333333333327,-8.045333333333332],[2.240000000000009,0],[-3.490666666666641,10.09866666666666],[-1.903999999999996,0],[-2.725333333333367,-8.232],[0.6906666666666865,0],[-2.72533333333331,8.232],[-1.922666666666657,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[155.86146545410156,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[132.444,43.06400000000001],[124.38,40.992],[118.948,35.28],[116.988,27.16],[118.948,19.040000000000006],[124.324,13.49600000000001],[131.94,11.424000000000007],[138.044,12.600000000000009],[142.58,15.848],[145.38,20.49600000000001],[146.388,26.096],[146.332,27.608],[146.108,29.064000000000007],[121.972,29.064000000000007],[121.972,24.024],[142.524,24.024],[139.5,26.320000000000007],[139.108,21.448000000000008],[136.308,18.032],[131.94,16.744],[127.46,18.032],[124.436,21.616],[123.596,27.216000000000008],[124.436,32.592],[127.684,36.176],[132.5,37.464],[137.148,36.232],[140.228,33.040000000000006],[145.38,35.56],[142.58,39.42400000000001],[138.1,42.11200000000001],[132.444,43.06400000000001],[132.444,43.06400000000001],[132.444,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.305999999999983,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.838000000000022,0],[-1.79200000000003,-0.784000000000006],[-1.232000000000028,-1.381299999999996],[-0.6350000000000477,-1.754700000000014],[0,-1.978700000000003],[0.03699999999997772,-0.5227000000000004],[0.1120000000000232,-0.4480000000000075],[8.045333333333303,0],[0,1.680000000000007],[-6.850666666666655,0],[1.007999999999981,-0.7653333333333308],[0.6340000000000146,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.67999999999995,0],[1.305999999999983,-0.8586999999999989],[0.7090000000000032,-1.567999999999998],[-0.1499999999999773,-2.202700000000007],[-0.7470000000000141,-1.530699999999996],[-1.382000000000005,-0.8586999999999989],[-1.791999999999973,0],[-1.269999999999982,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333329,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.754000000000019,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.314999999999998,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999994,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.276999999999987,0],[1.791999999999973,0.7839999999999918],[1.231999999999971,1.344000000000008],[0.6719999999999686,1.7547],[0,0.4852999999999952],[-0.03800000000001091,0.5227000000000004],[-8.045333333333303,0],[0,-1.680000000000007],[6.850666666666655,0],[-1.007999999999981,0.7653333333333308],[0.3730000000000473,-1.829300000000003],[-0.5979999999999563,-1.456000000000003],[-1.232000000000028,-0.8586999999999989],[-1.680000000000007,0],[-1.307000000000016,0.8213000000000079],[-0.7100000000000364,1.530699999999996],[-0.186999999999955,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418000000000006,0.8586999999999989],[1.829000000000008,0],[1.305999999999983,-0.8212999999999937],[1.717333333333329,0.8400000000000034],[-0.5980000000000132,1.4187000000000012],[-1.232000000000028,1.11999999999999],[-1.718000000000018,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[95.32,37.184],[100.024,35.896],[103.328,32.36800000000001],[104.56,27.216000000000008],[103.328,22.12],[100.024,18.592],[95.32,17.304],[90.56,18.592],[87.256,22.12],[86.08000000000001,27.216000000000008],[87.256,32.36800000000001],[90.50399999999999,35.896],[95.32,37.184],[95.32,37.184],[95.32,37.184]],"i":[[0,0],[-1.381,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.820999999999998,1.493299999999991],[1.419000000000011,0.8586999999999989],[1.754999999999995,0],[1.418999999999983,-0.8586999999999989],[0.7839999999999918,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.381,-0.8586999999999989],[-1.792000000000002,0],[0,0],[0,0]],"o":[[1.754999999999995,0],[1.419000000000011,-0.8586999999999989],[0.820999999999998,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.381,-0.8586999999999989],[-1.754999999999995,0],[-1.419000000000011,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.7839999999999918,1.493299999999991],[1.419000000000011,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[94.70400000000001,43.06400000000001],[86.864,40.992],[81.43199999999999,35.28],[79.47200000000001,27.216000000000008],[81.488,19.152],[86.91999999999999,13.49600000000001],[94.648,11.424000000000007],[101.088,12.88],[105.512,16.912000000000006],[104.56,18.36800000000001],[104.56,0],[110.832,0],[110.832,42.392],[104.84,42.392],[104.84,36.232],[105.568,37.408],[101.088,41.608],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001]],"i":[[0,0],[2.314999999999998,1.381299999999996],[1.344000000000023,2.389300000000006],[0,2.986699999999999],[-1.343999999999994,2.389300000000006],[-2.276999999999987,1.381299999999996],[-2.875,0],[-1.8669999999999902,-0.9706999999999937],[-1.082999999999998,-1.717300000000009],[0.3173333333333233,-0.4853333333333296],[0,6.122666666666674],[-2.090666666666664,0],[0,-14.13066666666667],[1.99733333333333,0],[0,2.053333333333327],[-0.242666666666679,-0.3919999999999959],[1.941000000000003,-0.9707000000000079],[2.314999999999998,0],[0,0],[0,0]],"o":[[-2.912000000000006,0],[-2.277000000000015,-1.4187000000000012],[-1.306999999999988,-2.389300000000006],[0,-2.986699999999999],[1.343999999999994,-2.389300000000006],[2.277000000000015,-1.3813000000000102],[2.426999999999992,0],[1.867000000000019,0.9707000000000079],[-0.3173333333333233,0.4853333333333296],[0,-6.122666666666674],[2.090666666666664,0],[0,14.13066666666667],[-1.99733333333333,0],[0,-2.053333333333327],[0.242666666666679,0.3919999999999959],[-1.045000000000016,1.829300000000003],[-1.941000000000003,0.9706999999999937],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[57.40100000000001,43.06400000000001],[51.968999999999994,42],[48.38499999999999,38.976],[47.09700000000001,34.384],[48.161,29.960000000000008],[51.577,26.656000000000006],[57.345,24.752],[67.87299999999999,23.016000000000005],[67.87299999999999,28],[58.577,29.624],[54.881,31.248],[53.70500000000001,34.16],[54.992999999999995,37.016000000000005],[58.40899999999999,38.08],[62.88900000000001,36.96000000000001],[65.91300000000001,33.992],[66.977,29.792],[66.977,22.008],[65.297,18.36800000000001],[60.929,16.912000000000006],[56.505,18.256],[53.761,21.616],[48.496999999999986,18.98400000000001],[51.240999999999985,15.064000000000007],[55.66499999999999,12.376],[61.09700000000001,11.424000000000007],[67.42500000000001,12.768],[71.737,16.52],[73.305,22.008],[73.305,42.392],[67.257,42.392],[67.257,36.904],[68.54499999999999,37.072],[65.80099999999999,40.264],[62.04900000000001,42.336],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001]],"i":[[0,0],[1.568000000000012,0.7092999999999989],[0.8590000000000089,1.2693000000000012],[0,1.7547],[-0.7090000000000032,1.306699999999992],[-1.531000000000006,0.8960000000000008],[-2.314999999999998,0.3733000000000004],[-3.509333333333331,0.5786666666666633],[0,-1.661333333333332],[3.098666666666674,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8590000000000089,-0.7467000000000041],[-1.381,0],[-1.269000000000005,0.7466999999999899],[-0.7090000000000032,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829000000000008,0],[1.269000000000005,-0.8960000000000008],[0.5970000000000084,-1.381299999999996],[1.754666666666679,0.8773333333333255],[-1.268999999999977,1.11999999999999],[-1.680000000000007,0.6346999999999952],[-1.903999999999996,0],[-1.829000000000008,-0.8960000000000008],[-1.0080000000000098,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999991,0],[0,1.829333333333338],[-0.429333333333318,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.716999999999985,0],[0,0],[0,0]],"o":[[-2.053000000000026,0],[-1.531000000000006,-0.7467000000000041],[-0.8589999999999804,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999857,-1.306700000000006],[1.531000000000006,-0.8960000000000008],[3.509333333333331,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666674,0.541333333333327],[-1.680000000000007,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.896000000000015,0.7092999999999989],[1.717000000000013,0],[1.306999999999988,-0.7467000000000041],[0.7089999999999748,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.082999999999998,-0.9707000000000079],[-1.680000000000007,0],[-1.2319999999999993,0.8586999999999989],[-1.754666666666679,-0.8773333333333255],[0.5600000000000023,-1.493300000000005],[1.269000000000005,-1.157300000000006],[1.717000000000013,-0.6347000000000094],[2.388999999999982,0],[1.86699999999999,0.8960000000000008],[1.045000000000016,1.568000000000012],[0,6.794666666666672],[-2.015999999999991,0],[0,-1.829333333333338],[0.429333333333318,0.05599999999999739],[-0.7089999999999748,1.2319999999999993],[-1.082999999999998,0.8960000000000008],[-1.381,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.159999999999997,0.6720000000000041],[21.84,22.400000000000006],[18.75999999999999,22.400000000000006],[34.16,0.6720000000000041],[40.31999999999999,0.6720000000000041],[40.31999999999999,42.392],[33.768,42.392],[33.768,8.456000000000003],[36.232,9.128],[20.49600000000001,30.632000000000005],[19.824000000000012,30.632000000000005],[4.424000000000007,9.128],[6.608000000000004,8.456000000000003],[6.608000000000004,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.053333333333342,0],[-5.226666666666659,-7.242666666666665],[1.026666666666671,0],[-5.133333333333326,7.242666666666672],[-2.053333333333342,0],[0,-13.90666666666667],[2.183999999999997,0],[0,11.312],[-0.8213333333333424,-0.2240000000000038],[5.245333333333321,-7.168000000000006],[0.2239999999999895,0],[5.133333333333326,7.168000000000006],[-0.7280000000000086,0.2240000000000038],[0,-11.312],[2.202666666666659,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.053333333333342,0],[5.226666666666659,7.242666666666672],[-1.026666666666671,0],[5.133333333333326,-7.242666666666665],[2.053333333333342,0],[0,13.90666666666666],[-2.183999999999997,0],[0,-11.312],[0.8213333333333424,0.2240000000000038],[-5.245333333333321,7.168000000000006],[-0.2239999999999895,0],[-5.133333333333326,-7.168000000000006],[0.7280000000000086,-0.2240000000000038],[0,11.312],[-2.202666666666659,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[2.91259765625,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[702.6863719370097,144],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":72,"ix":2}},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[56.54167175292969,-0.000022762338630855083],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":80,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[122.0000003294881,25.00000012138912],"ix":2},"a":{"a":0,"k":[56.54167175292969,-0.00002288818359375],"ix":2},"s":{"a":0,"k":[34.403572049765366,34.403572049765366],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":153,"st":0,"bm":0},{"ddd":0,"ind":19,"ty":3,"nm":"Color & Stroke Change","sr":1,"ks":{"p":{"a":0,"k":[24,24],"ix":2},"a":{"a":0,"k":[50,50],"ix":2},"s":{"a":0,"k":[9.600000215640163,9.600000215640163],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0},{"ddd":0,"refId":"0","w":500,"h":500,"ind":20,"ty":0,"nm":"hover-scan","sr":1,"ks":{"p":{"a":0,"k":[50,50],"ix":2},"a":{"a":0,"k":[250,250],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":153,"st":0,"bm":0,"parent":19}],"markers":[]} \ No newline at end of file diff --git a/frontend/public/lotties/server.json b/frontend/public/lotties/server.json new file mode 100644 index 000000000..537e6bfe0 --- /dev/null +++ b/frontend/public/lotties/server.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":0,"op":60,"w":430,"h":430,"nm":"wired-outline-57-server","ddd":0,"assets":[{"id":"comp_1","nm":"hover-pinch","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Rectangle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[215,285.471,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-11.046],[0,0],[-11.046,0],[0,0],[0,11.046],[0,0],[11.046,0]],"o":[[-11.046,0],[0,0],[0,11.046],[0,0],[11.046,0],[0,0],[0,-11.046],[0,0]],"v":[[-165,-45.685],[-185,-25.685],[-185,25.685],[-165,45.685],[165,45.685],[185,25.685],[185,-25.685],[165,-45.685]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-57-server').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-57-server').layer('control').effect('stroke')('Menu'));"},"lc":1,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1800,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Vector 2","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":0,"s":[-25.74,-14.872,0],"to":[0,0,0],"ti":[0,0,0]},{"t":30,"s":[-25.74,-14.872,0]}],"ix":2,"l":2},"a":{"a":0,"k":[106.014,-14.875,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[106.014,-14.873],[286.263,-14.779]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.578],"y":[1]},"o":{"x":[0.182],"y":[0]},"t":0,"s":[100]},{"i":{"x":[0.703],"y":[1]},"o":{"x":[0.344],"y":[0]},"t":9,"s":[37]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":44,"s":[44]},{"t":56,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-57-server').layer('control').effect('secondary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":24,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-57-server').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"d":[{"n":"d","nm":"dash","v":{"a":0,"k":0,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":30,"ix":2}},{"n":"o","nm":"offset","v":{"a":0,"k":0,"ix":7}}],"nm":".secondary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"secondary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[106.014,-14.873],[286.263,-14.779]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.573],"y":[1]},"o":{"x":[0.187],"y":[0]},"t":0,"s":[100]},{"i":{"x":[0.704],"y":[1]},"o":{"x":[0.337],"y":[0]},"t":17,"s":[6]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":34,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":48,"s":[60]},{"t":60,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-57-server').layer('control').effect('secondary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":24,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-57-server').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"d":[{"n":"d","nm":"dash","v":{"a":0,"k":0,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":30,"ix":2}},{"n":"o","nm":"offset","v":{"a":0,"k":0,"ix":7}}],"nm":".secondary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"secondary"},{"ty":"tr","p":{"a":0,"k":[0,30],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1800,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Vector","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-131.754,0.002,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,9.625],[9.595,0.102],[0.064,0],[0.117,-9.683],[-9.683,0],[-0.059,0.001]],"o":[[0,-9.619],[-0.064,-0.001],[-9.567,0],[0,9.683],[0.059,0],[9.602,-0.094]],"v":[[17.5,0],[0.192,-17.499],[0,-17.5],[-17.5,0],[0,17.5],[0.176,17.499]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[0,9.625],[9.595,0.102],[0.064,0],[0.117,-9.683],[-9.683,0],[-0.059,0.001]],"o":[[0,-9.619],[-0.064,-0.001],[-9.567,0],[0,9.683],[0.059,0],[9.602,-0.094]],"v":[[72.75,-0.017],[55.442,-17.516],[0,-17.5],[-17.5,0],[0,17.5],[55.426,17.482]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":30,"s":[{"i":[[0,9.625],[9.595,0.102],[0.064,0],[0.117,-9.683],[-9.683,0],[-0.059,0.001]],"o":[[0,-9.619],[-0.064,-0.001],[-9.567,0],[0,9.683],[0.059,0],[9.602,-0.094]],"v":[[72.875,-0.027],[55.567,-17.526],[55.375,-17.527],[37.875,-0.027],[55.375,17.473],[55.551,17.473]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[0,9.625],[9.595,0.102],[0.064,0],[0.117,-9.683],[-9.683,0],[-0.059,0.001]],"o":[[0,-9.619],[-0.064,-0.001],[-9.567,0],[0,9.683],[0.059,0],[9.602,-0.094]],"v":[[72.75,-0.017],[55.442,-17.516],[0,-17.5],[-17.5,0],[0,17.5],[55.426,17.482]],"c":true}]},{"t":60,"s":[{"i":[[0,9.625],[9.595,0.102],[0.064,0],[0.117,-9.683],[-9.683,0],[-0.059,0.001]],"o":[[0,-9.619],[-0.064,-0.001],[-9.567,0],[0,9.683],[0.059,0],[9.602,-0.094]],"v":[[17.5,0],[0.192,-17.499],[0,-17.5],[-17.5,0],[0,17.5],[0.176,17.499]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-57-server').layer('control').effect('secondary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-57-server').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".secondary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"secondary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1800,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Vector","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[215.001,176.112,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[8.478,0],[0,0],[2.827,-7.987],[0,0]],"o":[[0,0],[-2.823,-7.994],[0,0],[-8.473,0],[0,0],[0,0]],"v":[[183.952,77.268],[134.083,-63.929],[115.224,-77.268],[-115.115,-77.268],[-133.969,-63.942],[-183.952,77.268]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('wired-outline-57-server').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-57-server').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1800,"st":0,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"control","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"stroke","np":3,"mn":"Pseudo/@@NH5Ou6jMSumHdvYySdCPdw","ix":1,"en":1,"ef":[{"ty":7,"nm":"Menu","mn":"Pseudo/@@NH5Ou6jMSumHdvYySdCPdw-0001","ix":1,"v":{"a":0,"k":3,"ix":1}}]},{"ty":5,"nm":"primary","np":3,"mn":"ADBE Color Control","ix":2,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]},{"ty":5,"nm":"secondary","np":3,"mn":"ADBE Color Control","ix":3,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]}],"ip":0,"op":131,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"hover-pinch","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[215,215,0],"ix":2,"l":2},"a":{"a":0,"k":[215,215,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":430,"h":430,"ip":0,"op":70,"st":0,"bm":0}],"markers":[{"tm":0,"cm":"default:hover-pinch","dr":60}],"props":{}} \ No newline at end of file diff --git a/frontend/public/lotties/settings-cog.json b/frontend/public/lotties/settings-cog.json new file mode 100644 index 000000000..fdf1333c7 --- /dev/null +++ b/frontend/public/lotties/settings-cog.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":0,"op":60,"w":500,"h":500,"nm":"system-regular-63-settings-cog","ddd":0,"assets":[{"id":"comp_1","nm":"hover-cog-1","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.38],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":60,"s":[180]}],"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[65.415,17.27],[65.425,17.27]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-5.279,-1.58],[-4.338,0],[-4.502,1.145],[-2.995,1.872],[-0.218,0.122],[-4.41,7.876],[-0.134,0.213],[-1.581,5.285],[0,4.342],[1.146,4.497],[1.868,2.989],[0.123,0.22],[7.877,4.411],[0.214,0.133],[5.283,1.581],[4.342,0],[4.498,-1.145],[2.992,-1.871],[0.22,-0.123],[4.411,-7.878],[0.134,-0.213],[1.581,-5.283],[0,-4.341],[-1.144,-4.497],[-1.871,-2.993],[-0.123,-0.219],[-7.879,-4.413],[-0.213,-0.132]],"o":[[4.502,1.145],[4.338,0],[5.28,-1.581],[0.212,-0.132],[7.878,-4.413],[0.123,-0.22],[1.868,-2.99],[1.146,-4.495],[0,-4.342],[-1.581,-5.285],[-0.134,-0.214],[-4.411,-7.877],[-0.219,-0.123],[-2.991,-1.87],[-4.497,-1.145],[-4.341,0],[-5.282,1.581],[-0.214,0.133],[-7.876,4.41],[-0.123,0.219],[-1.87,2.992],[-1.145,4.498],[0,4.341],[1.581,5.284],[0.133,0.213],[4.41,7.876],[0.218,0.122],[2.996,1.872]],"v":[[-13.321,50.103],[-0.001,51.829],[13.318,50.104],[25.456,45.04],[26.102,44.658],[44.885,25.874],[45.271,25.224],[50.331,13.088],[52.057,-0.229],[50.331,-13.547],[45.271,-25.682],[44.885,-26.332],[26.103,-45.116],[25.453,-45.5],[13.316,-50.562],[-0.001,-52.287],[-13.319,-50.562],[-25.455,-45.5],[-26.105,-45.115],[-44.888,-26.332],[-45.272,-25.684],[-50.334,-13.548],[-52.059,-0.229],[-50.335,13.087],[-45.271,25.226],[-44.888,25.874],[-26.104,44.658],[-25.458,45.04]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[7.066,0],[7.15,1.852],[0.174,0.051],[7.142,4.396],[7.169,12.665],[1.507,5.085],[0.045,0.174],[0,7.071],[-1.853,7.147],[-0.051,0.173],[-4.395,7.138],[-12.665,7.167],[-5.085,1.507],[-0.174,0.045],[-7.071,0],[-7.146,-1.853],[-0.172,-0.051],[-7.138,-4.395],[-7.169,-12.666],[-1.508,-5.086],[-0.045,-0.174],[0,-7.073],[1.854,-7.145],[0.051,-0.171],[4.395,-7.136],[12.666,-7.17],[5.083,-1.507],[0.176,-0.045]],"o":[[-7.065,0],[-0.175,-0.046],[-5.082,-1.506],[-12.667,-7.17],[-4.395,-7.139],[-0.051,-0.172],[-1.853,-7.146],[0,-7.071],[0.045,-0.175],[1.508,-5.085],[7.17,-12.666],[7.138,-4.396],[0.172,-0.051],[7.147,-1.853],[7.072,0],[0.174,0.045],[5.086,1.508],[12.664,7.168],[4.394,7.134],[0.051,0.172],[1.854,7.146],[0,7.074],[-0.045,0.173],[-1.508,5.086],[-7.168,12.664],[-7.142,4.396],[-0.174,0.051],[-7.149,1.852]],"v":[[-0.001,83.129],[-21.426,80.338],[-21.949,80.193],[-41.716,71.788],[-72.018,41.488],[-80.423,21.717],[-80.567,21.197],[-83.359,-0.229],[-80.567,-21.656],[-80.423,-22.178],[-72.018,-41.946],[-41.719,-72.244],[-21.949,-80.651],[-21.43,-80.795],[-0.001,-83.587],[21.427,-80.795],[21.946,-80.651],[41.717,-72.244],[72.015,-41.945],[80.42,-22.178],[80.563,-21.659],[83.357,-0.229],[80.563,21.2],[80.42,21.717],[72.015,41.487],[41.716,71.787],[21.946,80.193],[21.423,80.338]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[-0.007,0.004],[0,0]],"o":[[0,0],[0.006,-0.003]],"v":[[142.26,-118.378],[142.24,-118.367]],"c":true},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ind":4,"ty":"sh","ix":5,"ks":{"a":0,"k":{"i":[[0,0],[-0.007,-0.004]],"o":[[0.007,0.004],[0,0]],"v":[[-142.23,-118.359],[-142.211,-118.348]],"c":true},"ix":2},"nm":"Path 5","mn":"ADBE Vector Shape - Group","hd":false},{"ind":5,"ty":"sh","ix":6,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[173.794,64.156],[173.804,64.156]],"c":true},"ix":2},"nm":"Path 6","mn":"ADBE Vector Shape - Group","hd":false},{"ind":6,"ty":"sh","ix":7,"ks":{"a":0,"k":{"i":[[0.007,0.003],[0,0]],"o":[[0,0],[-0.006,-0.003]],"v":[[142.209,117.89],[142.228,117.901]],"c":true},"ix":2},"nm":"Path 7","mn":"ADBE Vector Shape - Group","hd":false},{"ind":7,"ty":"sh","ix":8,"ks":{"a":0,"k":{"i":[[0,0],[0.006,-0.003]],"o":[[-0.007,0.003],[0,0]],"v":[[-142.24,117.907],[-142.259,117.918]],"c":true},"ix":2},"nm":"Path 8","mn":"ADBE Vector Shape - Group","hd":false},{"ind":8,"ty":"sh","ix":9,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-6.556,2.028],[-14.196,13.044],[-5.925,-3.438],[0,0],[0,0],[0,0],[-1.534,6.696],[0,9.907],[1.949,8.508],[-5.948,3.436],[0,0],[0,0],[0,0],[5.044,4.635],[17.51,5.416],[0,6.862],[0,0],[0,0],[0,0],[6.556,-2.027],[14.196,-13.046],[5.926,3.438],[0,0],[0,0],[0,0],[1.535,-6.697],[0,-9.912],[-1.949,-8.505],[5.949,-3.437],[0,0],[0,0],[0,0],[-5.046,-4.636],[-17.514,-5.417],[0,-6.862]],"o":[[0,0],[0,0],[0,-6.862],[17.513,-5.417],[5.045,-4.635],[0,0],[0,0],[0,0],[-5.948,-3.437],[1.949,-8.51],[0,-9.908],[-1.534,-6.696],[0,0],[0,0],[0,0],[-5.927,3.438],[-14.198,-13.046],[-6.556,-2.027],[0,0],[0,0],[0,0],[0,6.862],[-17.511,5.416],[-5.046,4.636],[0,0],[0,0],[0,0],[5.949,3.437],[-1.948,8.503],[0,9.911],[1.534,6.697],[0,0],[0,0],[0,0],[5.927,-3.439],[14.194,13.044],[6.556,2.028],[0,0]],"v":[[-31.226,176.828],[31.224,176.828],[31.224,143.104],[42.249,128.152],[90.035,100.33],[108.479,98.318],[137.809,115.337],[168.973,61.378],[139.879,44.572],[132.452,27.526],[135.39,-0.229],[132.452,-27.984],[139.879,-45.03],[168.973,-61.837],[137.809,-115.795],[108.479,-98.776],[90.035,-100.788],[42.25,-128.611],[31.224,-143.562],[31.224,-177.286],[-31.226,-177.286],[-31.226,-143.562],[-42.252,-128.611],[-90.036,-100.788],[-108.48,-98.776],[-137.812,-115.795],[-168.976,-61.836],[-139.881,-45.031],[-132.455,-27.982],[-135.392,-0.229],[-132.454,27.525],[-139.881,44.572],[-168.976,61.378],[-137.812,115.337],[-108.48,98.318],[-90.036,100.33],[-42.251,128.152],[-31.226,143.104]],"c":true},"ix":2},"nm":"Path 9","mn":"ADBE Vector Shape - Group","hd":false},{"ind":9,"ty":"sh","ix":10,"ks":{"a":0,"k":{"i":[[14.373,0],[0,0],[0,14.373],[0,0],[12.287,9.814],[0,0],[7.26,12.444],[0,0],[0.067,0.125],[-12.372,6.865],[0,0],[0,8.014],[-1.062,7.435],[0,0],[-6.843,12.634],[-0.072,0.124],[0,0],[-6.784,1.764],[-6.019,-3.511],[0,0],[-14.219,5.629],[0,0],[-14.373,0],[0,0],[0,-14.373],[0,0],[-12.288,-9.814],[0,0],[-7.261,-12.448],[0,0],[-0.068,-0.125],[12.369,-6.866],[0,0],[0,-8.012],[1.062,-7.437],[0,0],[6.844,-12.634],[0.071,-0.124],[0,0],[12.468,7.269],[0,0],[14.221,-5.63],[0,0]],"o":[[0,0],[-14.373,0],[0,0],[-14.221,-5.63],[0,0],[-12.438,7.253],[0,0],[-0.071,-0.124],[-6.844,-12.634],[0,0],[-1.062,-7.436],[0,-8.014],[0,0],[-12.371,-6.865],[0.068,-0.125],[0,0],[3.559,-6.101],[6.745,-1.756],[0,0],[12.287,-9.814],[0,0],[0,-14.373],[0,0],[14.373,0],[0,0],[14.218,5.629],[0,0],[12.436,-7.256],[0,0],[0.071,0.124],[6.842,12.633],[0,0],[1.062,7.437],[0,8.011],[0,0],[12.372,6.865],[-0.067,0.125],[0,0],[-7.293,12.502],[0,0],[-12.288,9.814],[0,0],[0,14.373]],"v":[[36.457,208.128],[-36.459,208.128],[-62.526,182.062],[-62.526,154.174],[-102.371,130.96],[-126.521,144.973],[-162.269,135.573],[-198.761,72.389],[-198.97,72.015],[-188.935,36.76],[-165.094,22.989],[-166.692,-0.229],[-165.094,-23.448],[-188.936,-37.219],[-198.971,-72.473],[-198.761,-72.848],[-162.303,-135.973],[-146.283,-148.136],[-126.489,-145.413],[-102.371,-131.418],[-62.526,-154.633],[-62.526,-182.52],[-36.459,-208.586],[36.457,-208.586],[62.524,-182.52],[62.524,-154.633],[102.369,-131.418],[126.519,-145.432],[162.266,-136.032],[198.759,-72.848],[198.969,-72.473],[188.935,-37.221],[165.092,-23.448],[166.69,-0.229],[165.092,22.989],[188.934,36.761],[198.968,72.015],[198.759,72.389],[162.3,135.514],[126.49,144.957],[102.369,130.96],[62.524,154.174],[62.524,182.062]],"c":true},"ix":2},"nm":"Path 10","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.914,0.91,0.91,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-63-settings-cog').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":11,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":425,"st":60,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"control","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"primary","np":3,"mn":"ADBE Color Control","ix":1,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[0.914,0.91,0.91],"ix":1}}]}],"ip":0,"op":271,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"hover-cog-1","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":70,"st":0,"bm":0}],"markers":[{"tm":0,"cm":"default:hover-cog-1","dr":60}],"props":{}} \ No newline at end of file diff --git a/frontend/public/lotties/sliding-carousel.json b/frontend/public/lotties/sliding-carousel.json new file mode 100644 index 000000000..311b185cd --- /dev/null +++ b/frontend/public/lotties/sliding-carousel.json @@ -0,0 +1 @@ +{"v":"5.7.5","fr":100,"ip":0,"op":102,"w":500,"h":500,"nm":"Comp 1","ddd":0,"metadata":{},"assets":[{"id":"0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"1","w":418,"h":334,"ind":2,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[41,83],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":92,"s":[100],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":1},{"ddd":0,"ind":3,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"2","w":418,"h":334,"ind":4,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[41,83],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"h":1},{"t":2,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":3},{"ddd":0,"ind":5,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"3","w":425,"h":494,"ind":6,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[-3,5],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":48,"s":[100],"h":1},{"t":92,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":5},{"ddd":0,"ind":7,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"4","w":265,"h":427,"ind":8,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[235,35],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":22,"s":[100],"h":1},{"t":48,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":7},{"ddd":0,"ind":9,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"5","w":423,"h":494,"ind":10,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[78,5],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":48,"s":[100],"h":1},{"t":92,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":9},{"ddd":0,"ind":11,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"6","w":423,"h":494,"ind":12,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[78,5],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":48,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":11},{"ddd":0,"ind":13,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"7","w":255,"h":425,"ind":14,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[-3,39],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":22,"s":[100],"h":1},{"t":43,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":13},{"ddd":0,"ind":15,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"8","w":425,"h":494,"ind":16,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[-3,5],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":20,"s":[100],"h":1},{"t":48,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":15},{"ddd":0,"ind":17,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"9","w":425,"h":494,"ind":18,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[-3,5],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":20,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":17},{"ddd":0,"ind":19,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"10","w":505,"h":344,"ind":20,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[-3,78],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":92,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":19}]},{"id":"1","layers":[{"ddd":0,"ind":21,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-41,-83],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","nm":".primary.design","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[177.06,88.542],[171.88,93.732],[125.028,93.732],[125.028,-93.718],[171.88,-93.718],[177.06,-88.538],[177.06,88.542]],"i":[[0,0],[2.856,0],[0,0],[0,0],[0,0],[0,-2.856],[0,0]],"o":[[0,2.862],[0,0],[0,0],[0,0],[2.856,0],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[93.728,130.212],[88.544,135.395],[-88.538,135.395],[-93.722,130.212],[-93.722,109.421],[-93.72,109.382],[-93.722,109.343],[-93.722,-109.329],[-93.72,-109.368],[-93.722,-109.407],[-93.722,-130.206],[-88.538,-135.39],[88.544,-135.39],[93.728,-130.206],[93.728,130.212]],"i":[[0,0],[2.858,0],[0,0],[0,2.858],[0,0],[0,0.013],[0,0.013],[0,0],[0,0.013],[0,0.013],[0,0],[-2.858,0],[0,0],[0,-2.858],[0,0]],"o":[[0,2.858],[0,0],[-2.858,0],[0,0],[0,-0.013],[0,-0.013],[0,0],[0,-0.013],[0,-0.013],[0,0],[0,-2.858],[0,0],[2.858,0],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 3","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-171.87,93.732],[-177.06,88.542],[-177.06,-88.538],[-171.87,-93.718],[-125.022,-93.718],[-125.022,93.732],[-171.87,93.732]],"i":[[0,0],[0,2.862],[0,0],[-2.861,0],[0,0],[0,0],[0,0]],"o":[[-2.861,0],[0,0],[0,-2.856],[0,0],[0,0],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 4","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[171.88,-125.018],[125.028,-125.018],[125.028,-130.206],[88.544,-166.69],[-88.538,-166.69],[-125.022,-130.206],[-125.022,-125.018],[-171.87,-125.018],[-208.36,-88.538],[-208.36,88.542],[-171.87,125.032],[-125.022,125.032],[-125.022,130.212],[-88.538,166.695],[88.544,166.695],[125.028,130.212],[125.028,125.032],[171.88,125.032],[208.36,88.542],[208.36,-88.538],[171.88,-125.018]],"i":[[0,0],[0,0],[0,0],[20.117,0],[0,0],[0,-20.117],[0,0],[0,0],[0,-20.115],[0,0],[-20.12,0],[0,0],[0,0],[-20.117,0],[0,0],[0,20.117],[0,0],[0,0],[0,20.121],[0,0],[20.115,0]],"o":[[0,0],[0,0],[0,-20.117],[0,0],[-20.117,0],[0,0],[0,0],[-20.12,0],[0,0],[0,20.121],[0,0],[0,0],[0,20.117],[0,0],[20.117,0],[0,0],[0,0],[20.115,0],[0,0],[0,-20.115],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250,250.00250244140625],"ix":2},"a":{"a":0,"k":[0,0.00250244140625],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"2","layers":[{"ddd":0,"ind":22,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-41,-83],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","nm":".primary.design","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[177.06,88.542],[171.88,93.732],[125.028,93.732],[125.028,-93.718],[171.88,-93.718],[177.06,-88.538],[177.06,88.542]],"i":[[0,0],[2.856,0],[0,0],[0,0],[0,0],[0,-2.856],[0,0]],"o":[[0,2.862],[0,0],[0,0],[0,0],[2.856,0],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[93.728,130.212],[88.544,135.395],[-88.538,135.395],[-93.722,130.212],[-93.722,109.421],[-93.72,109.382],[-93.722,109.343],[-93.722,-109.329],[-93.72,-109.368],[-93.722,-109.407],[-93.722,-130.206],[-88.538,-135.39],[88.544,-135.39],[93.728,-130.206],[93.728,130.212]],"i":[[0,0],[2.858,0],[0,0],[0,2.858],[0,0],[0,0.013],[0,0.013],[0,0],[0,0.013],[0,0.013],[0,0],[-2.858,0],[0,0],[0,-2.858],[0,0]],"o":[[0,2.858],[0,0],[-2.858,0],[0,0],[0,-0.013],[0,-0.013],[0,0],[0,-0.013],[0,-0.013],[0,0],[0,-2.858],[0,0],[2.858,0],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 3","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-171.87,93.732],[-177.06,88.542],[-177.06,-88.538],[-171.87,-93.718],[-125.022,-93.718],[-125.022,93.732],[-171.87,93.732]],"i":[[0,0],[0,2.862],[0,0],[-2.861,0],[0,0],[0,0],[0,0]],"o":[[-2.861,0],[0,0],[0,-2.856],[0,0],[0,0],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 4","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[171.88,-125.018],[125.028,-125.018],[125.028,-130.206],[88.544,-166.69],[-88.538,-166.69],[-125.022,-130.206],[-125.022,-125.018],[-171.87,-125.018],[-208.36,-88.538],[-208.36,88.542],[-171.87,125.032],[-125.022,125.032],[-125.022,130.212],[-88.538,166.695],[88.544,166.695],[125.028,130.212],[125.028,125.032],[171.88,125.032],[208.36,88.542],[208.36,-88.538],[171.88,-125.018]],"i":[[0,0],[0,0],[0,0],[20.117,0],[0,0],[0,-20.117],[0,0],[0,0],[0,-20.115],[0,0],[-20.12,0],[0,0],[0,0],[-20.117,0],[0,0],[0,20.117],[0,0],[0,0],[0,20.121],[0,0],[20.115,0]],"o":[[0,0],[0,0],[0,-20.117],[0,0],[-20.117,0],[0,0],[0,0],[-20.12,0],[0,0],[0,20.121],[0,0],[0,0],[0,20.117],[0,0],[20.117,0],[0,0],[0,0],[20.115,0],[0,0],[0,-20.115],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250,250.00250244140625],"ix":2},"a":{"a":0,"k":[0,0.00250244140625],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"3","layers":[{"ddd":0,"ind":23,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[3,-5],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":2,"s":[{"c":true,"v":[[81.291,-108.915],[24,-108.915],[-33.291,-108.915],[-48.023,-102.813],[-54.125,-88.081],[-54.125,89.003],[-33.291,109.836],[81.291,109.836],[102.125,89.003],[102.125,-88.081],[81.291,-108.915]],"i":[[0,0],[0,0],[0,0],[3.77,-3.77],[0,-5.753],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.506,0]],"o":[[0,0],[0,0],[-5.753,0],[-3.77,3.77],[0,0],[0,11.505],[0,0],[11.506,0],[0,0],[0,-11.506],[0,0]]}],"h":1},{"t":4,"s":[{"c":true,"v":[[-34.003,-109.325],[18.497,-109.489],[79.459,-109.409],[100.997,-87.576],[100.997,88.342],[80.898,109.181],[18.497,109.193],[-34.503,109.76],[-55.503,92.484],[-55.503,-91.976],[-34.003,-109.325]],"i":[[0,0],[-33,0],[0,0],[0,-11.506],[0,0],[10.599,-0.506],[31.5,0],[0,0],[0,9.674],[0,0],[-10.5,0]],"o":[[0,0],[30.33,0],[14.039,-0.421],[0,0],[0,11.505],[0,0],[-32,0],[-10,-0.837],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.833],"y":[0.859]},"o":{"x":[0.333],"y":[0]}},{"t":8,"s":[{"c":true,"v":[[-32.567,-109.991],[23.211,-112.208],[83.578,-115.094],[105.014,-93.835],[105.196,92.407],[84.952,115.024],[22.77,112.067],[-33.068,110.27],[-53.988,92.985],[-53.988,-92.637],[-32.567,-109.991]],"i":[[0,0],[-33.475,1.617],[0,0],[0,-11.506],[0,0],[11.602,0.847],[31.916,1.156],[0,0],[0,9.674],[0,0],[-10.707,0.102]],"o":[[0,0],[30.272,-1.173],[14.232,-0.984],[0,0],[0,11.505],[0,0],[-32.538,-0.824],[-10.206,-0.926],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.141]}},{"t":13,"s":[{"c":true,"v":[[-28.317,-111.962],[31.476,-120.26],[79.82,-131.925],[97.307,-112.367],[97.997,104.443],[80.768,132.323],[29.803,120.575],[-28.819,111.778],[-49.503,94.468],[-49.503,-94.593],[-28.317,-111.962]],"i":[[0,0],[-29.19,6.404],[0,0],[0,-11.506],[0,0],[12.597,4.854],[27.72,4.58],[0,0],[0,9.673],[0,0],[-11.32,0.404]],"o":[[0,0],[24.953,-4.645],[12.385,-2.651],[0,0],[0,11.505],[0,0],[-28.6,-3.263],[-10.817,-1.188],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":18,"s":[{"c":true,"v":[[7.243,-114.557],[57.814,-130.99],[80.187,-154.399],[88.793,-137.117],[89.982,120.584],[80.382,155.507],[54.93,131.999],[6.876,113.837],[-10.503,96.493],[-10.503,-97.168],[7.243,-114.557]],"i":[[0,0],[-17.317,12.809],[0,0],[0,-11.506],[0,0],[11.116,10.214],[16.283,9.16],[0,0],[0,9.673],[0,0],[-9.013,0.807]],"o":[[0,0],[12.683,-9.289],[7.31,-4.881],[0,0],[0,11.505],[0,0],[-17.3,-6.527],[-8.646,-1.539],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":25,"s":[{"c":true,"v":[[0.489,-118.579],[59.13,-154.283],[32.725,-168.179],[22.399,-155.812],[24.967,153.672],[41.432,165.042],[58.93,144.014],[0.256,119.124],[-13.503,101.711],[-13.503,-101.15],[0.489,-118.579]],"i":[[0,0],[-1.633,25.617],[0,0],[0,-11.506],[0,0],[-6.935,3.293],[1.067,18.321],[0,0],[0,9.674],[0,0],[-7.525,1.615]],"o":[[0,0],[-9.023,-16.317],[-6.728,1.014],[0,0],[0,11.505],[0,0],[-2.6,-13.053],[-7.292,-2.239],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":33,"s":[{"c":true,"v":[[25.735,-123.553],[61.197,-154.527],[-29.141,-161.911],[-41.9,-139.278],[-36.9,138.74],[-24.654,160.375],[60.897,155.077],[25.635,123.459],[15.497,105.977],[15.497,-106.084],[25.735,-123.553]],"i":[[0,0],[8.3,17.524],[0,0],[0,-11.506],[0,0],[-6.249,-3.706],[-24.4,30.92],[0,0],[0,9.674],[0,0],[-6.038,2.422]],"o":[[0,0],[-28.7,-24.476],[-7.161,4.779],[0,0],[0,11.505],[0,0],[12.1,-19.58],[-5.938,-2.94],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":37,"s":[{"c":true,"v":[[42.419,-127.031],[57.997,-160.082],[-64.541,-153.082],[-85.875,-130.248],[-85.875,130.169],[-65.541,152.003],[54.497,161.003],[42.419,126.963],[34.997,109.43],[34.997,-109.531],[42.419,-127.031]],"i":[[0,0],[15.827,9.377],[0,0],[0,-11.506],[0,0],[-10.461,-4.506],[-34,22.494],[0,0],[0,9.674],[0,0],[-4.922,3.028]],"o":[[0,0],[-37,-21.921],[-12.461,6.079],[0,0],[0,11.505],[0,0],[24.637,-16.3],[-4.922,-3.466],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":40,"s":[{"c":true,"v":[[58.47,-130.461],[59.054,-159.208],[-67.97,-152.791],[-89.232,-130.243],[-89.232,130.175],[-68.827,151.866],[56.422,159.163],[58.47,130.403],[56.711,112.398],[56.711,-112.485],[58.47,-130.461]],"i":[[0,0],[23.373,8.037],[0,0],[0,-11.506],[0,0],[-10.611,-3.862],[-41.925,18.346],[0,0],[0,9.935],[0,0],[-1.776,2.595]],"o":[[0,0],[-44.2,-18.355],[-12.325,5.211],[0,0],[0,11.505],[0,0],[31.73,-13.971],[-1.776,-2.971],[0,0],[0,-9.936],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":52,"s":[{"c":true,"v":[[98.595,-139.037],[30.196,-155.562],[-76.541,-152.062],[-97.625,-130.229],[-97.625,130.189],[-77.041,151.523],[29.733,156.023],[98.595,139.003],[110.997,119.819],[110.997,-119.87],[98.595,-139.037]],"i":[[0,0],[42.238,4.688],[0,0],[0,-11.506],[0,0],[-10.984,-2.253],[-61.736,7.974],[0,0],[0,10.589],[0,0],[6.089,1.514]],"o":[[0,0],[-62.199,-9.441],[-11.984,3.04],[0,0],[0,11.505],[0,0],[49.46,-8.15],[6.089,-1.733],[0,0],[0,-10.591],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":57,"s":[{"c":true,"v":[[93.957,-150.003],[16.803,-153.622],[-81.693,-151.624],[-102.669,-130.22],[-102.669,130.198],[-81.978,151.316],[17.827,153.885],[93.957,149.984],[110.624,129.29],[110.624,-129.319],[93.957,-150.003]],"i":[[0,0],[38.858,0.619],[0,0],[0,-11.506],[0,0],[-11.208,-1.286],[-49.166,1.112],[0,0],[0,11.425],[0,0],[8.754,0.899]],"o":[[0,0],[-47.257,-0.753],[-11.779,1.735],[0,0],[0,11.505],[0,0],[44.834,-0.888],[8.754,-1.029],[0,0],[0,-11.427],[0,0]]}],"i":{"x":[0.25],"y":[1]},"o":{"x":[0.167],"y":[0.167]}},{"t":79,"s":[{"c":true,"v":[[88.541,-151.042],[-1.003,-151.042],[-88.541,-151.042],[-109.375,-130.209],[-109.375,130.209],[-88.541,151.042],[1.997,151.042],[88.541,151.042],[109.375,130.209],[109.375,-130.209],[88.541,-151.042]],"i":[[0,0],[29.848,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[-30.18,0],[0,0],[0,11.505],[0,0],[11.506,0]],"o":[[0,0],[-29.179,0],[-11.506,0],[0,0],[0,11.505],[0,0],[28.848,0],[11.506,0],[0,0],[0,-11.506],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":2,"s":[24],"h":1},{"t":5,"s":[24],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":12,"s":[17],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":13,"s":[15],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":15,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"e":{"a":1,"k":[{"t":2,"s":[74],"h":1},{"t":5,"s":[74],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":12,"s":[83],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":13,"s":[85],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":15,"s":[100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":1,"k":[{"t":2,"s":[122.00000000000001],"h":1},{"t":5,"s":[302],"h":1}],"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":1,"k":[{"t":2,"s":[158.997,250.003],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.77],"y":[0]},"ti":[-15.167,0],"to":[-11.333,0]},{"t":28,"s":[90.997,250.003],"i":{"x":[0.159],"y":[1]},"o":{"x":[0.407],"y":[0]},"ti":[-13.722,0],"to":[7.313,0]},{"t":92,"s":[249.997,250.003],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[-100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"4","layers":[{"ddd":0,"ind":24,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-235,-35],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":23,"s":[{"c":false,"v":[[71.259,-144.472],[71.259,140.945]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":28,"s":[{"c":false,"v":[[63.259,-151.942],[64.259,148.476]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":42,"s":[{"c":false,"v":[[63.259,-151.942],[61.259,148.476]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":47,"s":[{"c":false,"v":[[48.259,-151.881],[51.259,147.537]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":1,"k":[{"t":2,"s":[250.003,250.003],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"ti":[-19.333,0],"to":[19.333,0]},{"t":29,"s":[366.003,250.003],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":51,"s":[366.003,250.003],"i":{"x":[0.23],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"ti":[4.167,0],"to":[-4.167,0]},{"t":78,"s":[341.003,250.003],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"5","layers":[{"ddd":0,"ind":25,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-78,-5],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":2,"s":[{"c":true,"v":[[88.541,-151.042],[-1.003,-151.042],[-88.541,-151.042],[-109.375,-130.209],[-109.375,130.209],[-88.541,151.042],[1.997,151.042],[88.541,151.042],[109.375,130.209],[109.375,-130.209],[88.541,-151.042]],"i":[[0,0],[29.848,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[-30.18,0],[0,0],[0,11.505],[0,0],[11.506,0]],"o":[[0,0],[-29.179,0],[-11.506,0],[0,0],[0,11.505],[0,0],[28.848,0],[11.506,0],[0,0],[0,-11.506],[0,0]]}],"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]}},{"t":11,"s":[{"c":true,"v":[[106.723,-142.676],[20.996,-159.271],[-79.97,-151.771],[-100.982,-130.223],[-100.982,130.195],[-80.327,151.385],[22.05,158.6],[106.723,142.233],[121.534,122.578],[121.534,-123.033],[106.723,-142.676]],"i":[[0,0],[38.698,3.349],[0,0],[0,-11.506],[0,0],[-11.133,-1.609],[-52.72,5.696],[0,0],[0,10.851],[0,0],[7.637,1.081]],"o":[[0,0],[-52.765,-6.743],[-11.848,2.171],[0,0],[0,11.505],[0,0],[43.571,-5.821],[7.637,-1.238],[0,0],[0,-10.852],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":15,"s":[{"c":true,"v":[[98.595,-139.037],[30.196,-155.562],[-76.541,-152.062],[-97.625,-130.229],[-97.625,130.189],[-77.041,151.523],[29.733,156.023],[98.595,139.003],[110.997,119.819],[110.997,-119.87],[98.595,-139.037]],"i":[[0,0],[42.238,4.688],[0,0],[0,-11.506],[0,0],[-10.984,-2.253],[-61.736,7.974],[0,0],[0,10.589],[0,0],[6.089,1.514]],"o":[[0,0],[-62.199,-9.441],[-11.984,3.04],[0,0],[0,11.505],[0,0],[49.46,-8.15],[6.089,-1.733],[0,0],[0,-10.591],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":25,"s":[{"c":true,"v":[[58.47,-130.461],[59.054,-159.208],[-67.97,-152.791],[-89.232,-130.243],[-89.232,130.175],[-68.827,151.866],[56.422,159.163],[58.47,130.403],[56.711,112.398],[56.711,-112.485],[58.47,-130.461]],"i":[[0,0],[23.373,8.037],[0,0],[0,-11.506],[0,0],[-10.611,-3.862],[-41.925,18.346],[0,0],[0,9.935],[0,0],[-1.776,2.595]],"o":[[0,0],[-44.2,-18.355],[-12.325,5.211],[0,0],[0,11.505],[0,0],[31.73,-13.971],[-1.776,-2.971],[0,0],[0,-9.936],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":29,"s":[{"c":true,"v":[[42.419,-127.031],[57.997,-160.082],[-64.541,-153.082],[-85.875,-130.248],[-85.875,130.169],[-65.541,152.003],[54.497,161.003],[42.419,126.963],[34.997,109.43],[34.997,-109.531],[42.419,-127.031]],"i":[[0,0],[15.827,9.377],[0,0],[0,-11.506],[0,0],[-10.461,-4.506],[-34,22.494],[0,0],[0,9.674],[0,0],[-4.922,3.028]],"o":[[0,0],[-37,-21.921],[-12.461,6.079],[0,0],[0,11.505],[0,0],[24.637,-16.3],[-4.922,-3.466],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":35,"s":[{"c":true,"v":[[25.735,-123.553],[61.197,-154.527],[-29.141,-161.911],[-41.9,-139.278],[-36.9,138.74],[-24.654,160.375],[60.897,155.077],[25.635,123.459],[15.497,105.977],[15.497,-106.084],[25.735,-123.553]],"i":[[0,0],[8.3,17.524],[0,0],[0,-11.506],[0,0],[-6.249,-3.706],[-24.4,30.92],[0,0],[0,9.674],[0,0],[-6.038,2.422]],"o":[[0,0],[-28.7,-24.476],[-7.161,4.779],[0,0],[0,11.505],[0,0],[12.1,-19.58],[-5.938,-2.94],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":43,"s":[{"c":true,"v":[[3.489,-118.917],[62.13,-154.62],[35.725,-168.517],[25.399,-156.15],[27.967,153.335],[44.432,164.704],[61.93,143.676],[3.256,118.787],[-10.503,101.373],[-10.503,-101.487],[3.489,-118.917]],"i":[[0,0],[-1.633,25.617],[0,0],[0,-11.506],[0,0],[-6.935,3.293],[1.067,18.321],[0,0],[0,9.674],[0,0],[-7.525,1.615]],"o":[[0,0],[-9.023,-16.317],[-6.728,1.014],[0,0],[0,11.505],[0,0],[-2.6,-13.053],[-7.292,-2.239],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":51,"s":[{"c":true,"v":[[-18.757,-114.28],[31.814,-130.714],[54.187,-154.122],[62.793,-136.84],[63.982,120.861],[54.382,155.784],[28.93,132.276],[-19.124,114.114],[-36.503,96.77],[-36.503,-96.891],[-18.757,-114.28]],"i":[[0,0],[-17.317,12.809],[0,0],[0,-11.506],[0,0],[11.116,10.214],[16.283,9.16],[0,0],[0,9.673],[0,0],[-9.013,0.807]],"o":[[0,0],[12.683,-9.289],[7.31,-4.881],[0,0],[0,11.505],[0,0],[-17.3,-6.527],[-8.646,-1.539],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":55,"s":[{"c":true,"v":[[-28.317,-111.962],[31.476,-120.26],[79.82,-131.925],[97.307,-112.367],[97.997,104.443],[80.768,132.323],[29.803,120.575],[-28.819,111.778],[-49.503,94.468],[-49.503,-94.593],[-28.317,-111.962]],"i":[[0,0],[-29.19,6.404],[0,0],[0,-11.506],[0,0],[12.597,4.854],[27.72,4.58],[0,0],[0,9.673],[0,0],[-11.32,0.404]],"o":[[0,0],[24.953,-4.645],[12.385,-2.651],[0,0],[0,11.505],[0,0],[-28.6,-3.263],[-10.817,-1.188],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.833],"y":[0.859]},"o":{"x":[0.167],"y":[0.167]}},{"t":61,"s":[{"c":true,"v":[[-32.567,-109.991],[23.211,-112.208],[83.578,-115.094],[105.014,-93.835],[105.196,92.407],[84.952,115.024],[22.77,112.067],[-33.068,110.27],[-53.988,92.985],[-53.988,-92.637],[-32.567,-109.991]],"i":[[0,0],[-33.475,1.617],[0,0],[0,-11.506],[0,0],[11.602,0.847],[31.916,1.156],[0,0],[0,9.674],[0,0],[-10.707,0.102]],"o":[[0,0],[30.272,-1.173],[14.232,-0.984],[0,0],[0,11.505],[0,0],[-32.538,-0.824],[-10.206,-0.926],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.141]}},{"t":65,"s":[{"c":true,"v":[[-34.003,-109.325],[18.497,-109.489],[79.459,-109.409],[100.997,-87.576],[100.997,88.342],[80.898,109.181],[18.497,109.193],[-34.503,109.76],[-55.503,92.484],[-55.503,-91.976],[-34.003,-109.325]],"i":[[0,0],[-33,0],[0,0],[0,-11.506],[0,0],[10.599,-0.506],[31.5,0],[0,0],[0,9.674],[0,0],[-10.5,0]],"o":[[0,0],[30.33,0],[14.039,-0.421],[0,0],[0,11.505],[0,0],[-32,0],[-10,-0.837],[0,0],[0,-9.675],[0,0]]}],"h":1},{"t":67,"s":[{"c":true,"v":[[81.291,-108.915],[24,-108.915],[-33.291,-108.915],[-48.023,-102.813],[-54.125,-88.081],[-54.125,89.003],[-33.291,109.836],[81.291,109.836],[102.125,89.003],[102.125,-88.081],[81.291,-108.915]],"i":[[0,0],[0,0],[0,0],[3.77,-3.77],[0,-5.753],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.506,0]],"o":[[0,0],[0,0],[-5.753,0],[-3.77,3.77],[0,0],[0,11.505],[0,0],[11.506,0],[0,0],[0,-11.506],[0,0]]}],"h":1}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":55,"s":[0],"h":1},{"t":57,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":65,"s":[5],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":67,"s":[5],"i":{"x":[0.569],"y":[0.704]},"o":{"x":[0.203],"y":[0.107]}},{"t":75,"s":[8.155],"i":{"x":[0.703],"y":[1]},"o":{"x":[0.317],"y":[0.743]}},{"t":92,"s":[10],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"e":{"a":1,"k":[{"t":55,"s":[100],"h":1},{"t":57,"s":[68],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":65,"s":[62],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":67,"s":[62],"i":{"x":[0.569],"y":[0.83]},"o":{"x":[0.203],"y":[0.062]}},{"t":75,"s":[58.707],"i":{"x":[0.703],"y":[1]},"o":{"x":[0.317],"y":[-2.808]}},{"t":92,"s":[59],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":1,"k":[{"t":65,"s":[0],"h":1},{"t":67,"s":[-182],"h":1}],"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":1,"k":[{"t":2,"s":[250.003,250.003],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"ti":[-19.333,0],"to":[19.333,0]},{"t":29,"s":[366.003,250.003],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":51,"s":[366.003,250.003],"i":{"x":[0.23],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"ti":[4.167,0],"to":[-4.167,0]},{"t":78,"s":[341.003,250.003],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"6","layers":[{"ddd":0,"ind":26,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-78,-5],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":2,"s":[{"c":true,"v":[[88.541,-151.042],[-1.003,-151.042],[-88.541,-151.042],[-109.375,-130.209],[-109.375,130.209],[-88.541,151.042],[1.997,151.042],[88.541,151.042],[109.375,130.209],[109.375,-130.209],[88.541,-151.042]],"i":[[0,0],[29.848,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[-30.18,0],[0,0],[0,11.505],[0,0],[11.506,0]],"o":[[0,0],[-29.179,0],[-11.506,0],[0,0],[0,11.505],[0,0],[28.848,0],[11.506,0],[0,0],[0,-11.506],[0,0]]}],"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]}},{"t":11,"s":[{"c":true,"v":[[106.723,-142.676],[20.996,-159.271],[-79.97,-151.771],[-100.982,-130.223],[-100.982,130.195],[-80.327,151.385],[22.05,158.6],[106.723,142.233],[121.534,122.578],[121.534,-123.033],[106.723,-142.676]],"i":[[0,0],[38.698,3.349],[0,0],[0,-11.506],[0,0],[-11.133,-1.609],[-52.72,5.696],[0,0],[0,10.851],[0,0],[7.637,1.081]],"o":[[0,0],[-52.765,-6.743],[-11.848,2.171],[0,0],[0,11.505],[0,0],[43.571,-5.821],[7.637,-1.238],[0,0],[0,-10.852],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":15,"s":[{"c":true,"v":[[98.595,-139.037],[30.196,-155.562],[-76.541,-152.062],[-97.625,-130.229],[-97.625,130.189],[-77.041,151.523],[29.733,156.023],[98.595,139.003],[110.997,119.819],[110.997,-119.87],[98.595,-139.037]],"i":[[0,0],[42.238,4.688],[0,0],[0,-11.506],[0,0],[-10.984,-2.253],[-61.736,7.974],[0,0],[0,10.589],[0,0],[6.089,1.514]],"o":[[0,0],[-62.199,-9.441],[-11.984,3.04],[0,0],[0,11.505],[0,0],[49.46,-8.15],[6.089,-1.733],[0,0],[0,-10.591],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":25,"s":[{"c":true,"v":[[58.47,-130.461],[59.054,-159.208],[-67.97,-152.791],[-89.232,-130.243],[-89.232,130.175],[-68.827,151.866],[56.422,159.163],[58.47,130.403],[56.711,112.398],[56.711,-112.485],[58.47,-130.461]],"i":[[0,0],[23.373,8.037],[0,0],[0,-11.506],[0,0],[-10.611,-3.862],[-41.925,18.346],[0,0],[0,9.935],[0,0],[-1.776,2.595]],"o":[[0,0],[-44.2,-18.355],[-12.325,5.211],[0,0],[0,11.505],[0,0],[31.73,-13.971],[-1.776,-2.971],[0,0],[0,-9.936],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":29,"s":[{"c":true,"v":[[42.419,-127.031],[57.997,-160.082],[-64.541,-153.082],[-85.875,-130.248],[-85.875,130.169],[-65.541,152.003],[54.497,161.003],[42.419,126.963],[34.997,109.43],[34.997,-109.531],[42.419,-127.031]],"i":[[0,0],[15.827,9.377],[0,0],[0,-11.506],[0,0],[-10.461,-4.506],[-34,22.494],[0,0],[0,9.674],[0,0],[-4.922,3.028]],"o":[[0,0],[-37,-21.921],[-12.461,6.079],[0,0],[0,11.505],[0,0],[24.637,-16.3],[-4.922,-3.466],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":35,"s":[{"c":true,"v":[[25.735,-123.553],[61.197,-154.527],[-29.141,-161.911],[-41.9,-139.278],[-36.9,138.74],[-24.654,160.375],[60.897,155.077],[25.635,123.459],[15.497,105.977],[15.497,-106.084],[25.735,-123.553]],"i":[[0,0],[8.3,17.524],[0,0],[0,-11.506],[0,0],[-6.249,-3.706],[-24.4,30.92],[0,0],[0,9.674],[0,0],[-6.038,2.422]],"o":[[0,0],[-28.7,-24.476],[-7.161,4.779],[0,0],[0,11.505],[0,0],[12.1,-19.58],[-5.938,-2.94],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":43,"s":[{"c":true,"v":[[3.489,-118.917],[62.13,-154.62],[35.725,-168.517],[25.399,-156.15],[27.967,153.335],[44.432,164.704],[61.93,143.676],[3.256,118.787],[-10.503,101.373],[-10.503,-101.487],[3.489,-118.917]],"i":[[0,0],[-1.633,25.617],[0,0],[0,-11.506],[0,0],[-6.935,3.293],[1.067,18.321],[0,0],[0,9.674],[0,0],[-7.525,1.615]],"o":[[0,0],[-9.023,-16.317],[-6.728,1.014],[0,0],[0,11.505],[0,0],[-2.6,-13.053],[-7.292,-2.239],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":51,"s":[{"c":true,"v":[[-18.757,-114.28],[31.814,-130.714],[54.187,-154.122],[62.793,-136.84],[63.982,120.861],[54.382,155.784],[28.93,132.276],[-19.124,114.114],[-36.503,96.77],[-36.503,-96.891],[-18.757,-114.28]],"i":[[0,0],[-17.317,12.809],[0,0],[0,-11.506],[0,0],[11.116,10.214],[16.283,9.16],[0,0],[0,9.673],[0,0],[-9.013,0.807]],"o":[[0,0],[12.683,-9.289],[7.31,-4.881],[0,0],[0,11.505],[0,0],[-17.3,-6.527],[-8.646,-1.539],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":55,"s":[{"c":true,"v":[[-28.317,-111.962],[31.476,-120.26],[79.82,-131.925],[97.307,-112.367],[97.997,104.443],[80.768,132.323],[29.803,120.575],[-28.819,111.778],[-49.503,94.468],[-49.503,-94.593],[-28.317,-111.962]],"i":[[0,0],[-29.19,6.404],[0,0],[0,-11.506],[0,0],[12.597,4.854],[27.72,4.58],[0,0],[0,9.673],[0,0],[-11.32,0.404]],"o":[[0,0],[24.953,-4.645],[12.385,-2.651],[0,0],[0,11.505],[0,0],[-28.6,-3.263],[-10.817,-1.188],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.833],"y":[0.859]},"o":{"x":[0.167],"y":[0.167]}},{"t":61,"s":[{"c":true,"v":[[-32.567,-109.991],[23.211,-112.208],[83.578,-115.094],[105.014,-93.835],[105.196,92.407],[84.952,115.024],[22.77,112.067],[-33.068,110.27],[-53.988,92.985],[-53.988,-92.637],[-32.567,-109.991]],"i":[[0,0],[-33.475,1.617],[0,0],[0,-11.506],[0,0],[11.602,0.847],[31.916,1.156],[0,0],[0,9.674],[0,0],[-10.707,0.102]],"o":[[0,0],[30.272,-1.173],[14.232,-0.984],[0,0],[0,11.505],[0,0],[-32.538,-0.824],[-10.206,-0.926],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.141]}},{"t":65,"s":[{"c":true,"v":[[-34.003,-109.325],[18.497,-109.489],[79.459,-109.409],[100.997,-87.576],[100.997,88.342],[80.898,109.181],[18.497,109.193],[-34.503,109.76],[-55.503,92.484],[-55.503,-91.976],[-34.003,-109.325]],"i":[[0,0],[-33,0],[0,0],[0,-11.506],[0,0],[10.599,-0.506],[31.5,0],[0,0],[0,9.674],[0,0],[-10.5,0]],"o":[[0,0],[30.33,0],[14.039,-0.421],[0,0],[0,11.505],[0,0],[-32,0],[-10,-0.837],[0,0],[0,-9.675],[0,0]]}],"h":1},{"t":67,"s":[{"c":true,"v":[[81.291,-108.915],[24,-108.915],[-33.291,-108.915],[-48.023,-102.813],[-54.125,-88.081],[-54.125,89.003],[-33.291,109.836],[81.291,109.836],[102.125,89.003],[102.125,-88.081],[81.291,-108.915]],"i":[[0,0],[0,0],[0,0],[3.77,-3.77],[0,-5.753],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.506,0]],"o":[[0,0],[0,0],[-5.753,0],[-3.77,3.77],[0,0],[0,11.505],[0,0],[11.506,0],[0,0],[0,-11.506],[0,0]]}],"h":1}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":20,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":22,"s":[2],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":25,"s":[2],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":35,"s":[5],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":42,"s":[8],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":43,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"e":{"a":1,"k":[{"t":20,"s":[100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":22,"s":[69],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":25,"s":[69],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":35,"s":[65],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":42,"s":[59],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":43,"s":[100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":1,"k":[{"t":2,"s":[250.003,250.003],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"ti":[-19.333,0],"to":[19.333,0]},{"t":29,"s":[366.003,250.003],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":51,"s":[366.003,250.003],"i":{"x":[0.23],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"ti":[4.167,0],"to":[-4.167,0]},{"t":78,"s":[341.003,250.003],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"7","layers":[{"ddd":0,"ind":27,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[3,-39],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":22,"s":[{"c":false,"v":[[60.705,-147.133],[60.712,150.416]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":35,"s":[{"c":false,"v":[[61.705,-148.133],[62.712,149.416]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":40,"s":[{"c":false,"v":[[70.455,-148.133],[71.462,142.666]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":42,"s":[{"c":false,"v":[[70.705,-148.133],[71.712,140.416]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":1,"k":[{"t":2,"s":[158.997,250.003],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.77],"y":[0]},"ti":[-15.167,0],"to":[-11.333,0]},{"t":28,"s":[90.997,250.003],"i":{"x":[0.159],"y":[1]},"o":{"x":[0.407],"y":[0]},"ti":[-13.722,0],"to":[7.313,0]},{"t":92,"s":[249.997,250.003],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[-100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"8","layers":[{"ddd":0,"ind":28,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[3,-5],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":2,"s":[{"c":true,"v":[[81.291,-108.915],[24,-108.915],[-33.291,-108.915],[-48.023,-102.813],[-54.125,-88.081],[-54.125,89.003],[-33.291,109.836],[81.291,109.836],[102.125,89.003],[102.125,-88.081],[81.291,-108.915]],"i":[[0,0],[0,0],[0,0],[3.77,-3.77],[0,-5.753],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.506,0]],"o":[[0,0],[0,0],[-5.753,0],[-3.77,3.77],[0,0],[0,11.505],[0,0],[11.506,0],[0,0],[0,-11.506],[0,0]]}],"h":1},{"t":4,"s":[{"c":true,"v":[[-34.003,-109.325],[18.497,-109.489],[79.459,-109.409],[100.997,-87.576],[100.997,88.342],[80.898,109.181],[18.497,109.193],[-34.503,109.76],[-55.503,92.484],[-55.503,-91.976],[-34.003,-109.325]],"i":[[0,0],[-33,0],[0,0],[0,-11.506],[0,0],[10.599,-0.506],[31.5,0],[0,0],[0,9.674],[0,0],[-10.5,0]],"o":[[0,0],[30.33,0],[14.039,-0.421],[0,0],[0,11.505],[0,0],[-32,0],[-10,-0.837],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.833],"y":[0.859]},"o":{"x":[0.333],"y":[0]}},{"t":8,"s":[{"c":true,"v":[[-32.567,-109.991],[23.211,-112.208],[83.578,-115.094],[105.014,-93.835],[105.196,92.407],[84.952,115.024],[22.77,112.067],[-33.068,110.27],[-53.988,92.985],[-53.988,-92.637],[-32.567,-109.991]],"i":[[0,0],[-33.475,1.617],[0,0],[0,-11.506],[0,0],[11.602,0.847],[31.916,1.156],[0,0],[0,9.674],[0,0],[-10.707,0.102]],"o":[[0,0],[30.272,-1.173],[14.232,-0.984],[0,0],[0,11.505],[0,0],[-32.538,-0.824],[-10.206,-0.926],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.141]}},{"t":13,"s":[{"c":true,"v":[[-28.317,-111.962],[31.476,-120.26],[79.82,-131.925],[97.307,-112.367],[97.997,104.443],[80.768,132.323],[29.803,120.575],[-28.819,111.778],[-49.503,94.468],[-49.503,-94.593],[-28.317,-111.962]],"i":[[0,0],[-29.19,6.404],[0,0],[0,-11.506],[0,0],[12.597,4.854],[27.72,4.58],[0,0],[0,9.673],[0,0],[-11.32,0.404]],"o":[[0,0],[24.953,-4.645],[12.385,-2.651],[0,0],[0,11.505],[0,0],[-28.6,-3.263],[-10.817,-1.188],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":18,"s":[{"c":true,"v":[[7.243,-114.557],[57.814,-130.99],[80.187,-154.399],[88.793,-137.117],[89.982,120.584],[80.382,155.507],[54.93,131.999],[6.876,113.837],[-10.503,96.493],[-10.503,-97.168],[7.243,-114.557]],"i":[[0,0],[-17.317,12.809],[0,0],[0,-11.506],[0,0],[11.116,10.214],[16.283,9.16],[0,0],[0,9.673],[0,0],[-9.013,0.807]],"o":[[0,0],[12.683,-9.289],[7.31,-4.881],[0,0],[0,11.505],[0,0],[-17.3,-6.527],[-8.646,-1.539],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":25,"s":[{"c":true,"v":[[0.489,-118.579],[59.13,-154.283],[32.725,-168.179],[22.399,-155.812],[24.967,153.672],[41.432,165.042],[58.93,144.014],[0.256,119.124],[-13.503,101.711],[-13.503,-101.15],[0.489,-118.579]],"i":[[0,0],[-1.633,25.617],[0,0],[0,-11.506],[0,0],[-6.935,3.293],[1.067,18.321],[0,0],[0,9.674],[0,0],[-7.525,1.615]],"o":[[0,0],[-9.023,-16.317],[-6.728,1.014],[0,0],[0,11.505],[0,0],[-2.6,-13.053],[-7.292,-2.239],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":33,"s":[{"c":true,"v":[[25.735,-123.553],[61.197,-154.527],[-29.141,-161.911],[-41.9,-139.278],[-36.9,138.74],[-24.654,160.375],[60.897,155.077],[25.635,123.459],[15.497,105.977],[15.497,-106.084],[25.735,-123.553]],"i":[[0,0],[8.3,17.524],[0,0],[0,-11.506],[0,0],[-6.249,-3.706],[-24.4,30.92],[0,0],[0,9.674],[0,0],[-6.038,2.422]],"o":[[0,0],[-28.7,-24.476],[-7.161,4.779],[0,0],[0,11.505],[0,0],[12.1,-19.58],[-5.938,-2.94],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":37,"s":[{"c":true,"v":[[42.419,-127.031],[57.997,-160.082],[-64.541,-153.082],[-85.875,-130.248],[-85.875,130.169],[-65.541,152.003],[54.497,161.003],[42.419,126.963],[34.997,109.43],[34.997,-109.531],[42.419,-127.031]],"i":[[0,0],[15.827,9.377],[0,0],[0,-11.506],[0,0],[-10.461,-4.506],[-34,22.494],[0,0],[0,9.674],[0,0],[-4.922,3.028]],"o":[[0,0],[-37,-21.921],[-12.461,6.079],[0,0],[0,11.505],[0,0],[24.637,-16.3],[-4.922,-3.466],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":40,"s":[{"c":true,"v":[[58.47,-130.461],[59.054,-159.208],[-67.97,-152.791],[-89.232,-130.243],[-89.232,130.175],[-68.827,151.866],[56.422,159.163],[58.47,130.403],[56.711,112.398],[56.711,-112.485],[58.47,-130.461]],"i":[[0,0],[23.373,8.037],[0,0],[0,-11.506],[0,0],[-10.611,-3.862],[-41.925,18.346],[0,0],[0,9.935],[0,0],[-1.776,2.595]],"o":[[0,0],[-44.2,-18.355],[-12.325,5.211],[0,0],[0,11.505],[0,0],[31.73,-13.971],[-1.776,-2.971],[0,0],[0,-9.936],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":52,"s":[{"c":true,"v":[[98.595,-139.037],[30.196,-155.562],[-76.541,-152.062],[-97.625,-130.229],[-97.625,130.189],[-77.041,151.523],[29.733,156.023],[98.595,139.003],[110.997,119.819],[110.997,-119.87],[98.595,-139.037]],"i":[[0,0],[42.238,4.688],[0,0],[0,-11.506],[0,0],[-10.984,-2.253],[-61.736,7.974],[0,0],[0,10.589],[0,0],[6.089,1.514]],"o":[[0,0],[-62.199,-9.441],[-11.984,3.04],[0,0],[0,11.505],[0,0],[49.46,-8.15],[6.089,-1.733],[0,0],[0,-10.591],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":57,"s":[{"c":true,"v":[[93.957,-150.003],[16.803,-153.622],[-81.693,-151.624],[-102.669,-130.22],[-102.669,130.198],[-81.978,151.316],[17.827,153.885],[93.957,149.984],[110.624,129.29],[110.624,-129.319],[93.957,-150.003]],"i":[[0,0],[38.858,0.619],[0,0],[0,-11.506],[0,0],[-11.208,-1.286],[-49.166,1.112],[0,0],[0,11.425],[0,0],[8.754,0.899]],"o":[[0,0],[-47.257,-0.753],[-11.779,1.735],[0,0],[0,11.505],[0,0],[44.834,-0.888],[8.754,-1.029],[0,0],[0,-11.427],[0,0]]}],"i":{"x":[0.25],"y":[1]},"o":{"x":[0.167],"y":[0.167]}},{"t":79,"s":[{"c":true,"v":[[88.541,-151.042],[-1.003,-151.042],[-88.541,-151.042],[-109.375,-130.209],[-109.375,130.209],[-88.541,151.042],[1.997,151.042],[88.541,151.042],[109.375,130.209],[109.375,-130.209],[88.541,-151.042]],"i":[[0,0],[29.848,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[-30.18,0],[0,0],[0,11.505],[0,0],[11.506,0]],"o":[[0,0],[-29.179,0],[-11.506,0],[0,0],[0,11.505],[0,0],[28.848,0],[11.506,0],[0,0],[0,-11.506],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":22,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":23,"s":[22],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":25,"s":[25],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":37,"s":[20],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":42,"s":[18],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":43,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"e":{"a":1,"k":[{"t":22,"s":[100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":23,"s":[75],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":25,"s":[75],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":37,"s":[84],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":42,"s":[86],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":43,"s":[100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":0,"k":302,"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":1,"k":[{"t":2,"s":[158.997,250.003],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.77],"y":[0]},"ti":[-15.167,0],"to":[-11.333,0]},{"t":28,"s":[90.997,250.003],"i":{"x":[0.159],"y":[1]},"o":{"x":[0.407],"y":[0]},"ti":[-13.722,0],"to":[7.313,0]},{"t":92,"s":[249.997,250.003],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[-100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"9","layers":[{"ddd":0,"ind":29,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[3,-5],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":2,"s":[{"c":true,"v":[[81.291,-108.915],[24,-108.915],[-33.291,-108.915],[-48.023,-102.813],[-54.125,-88.081],[-54.125,89.003],[-33.291,109.836],[81.291,109.836],[102.125,89.003],[102.125,-88.081],[81.291,-108.915]],"i":[[0,0],[0,0],[0,0],[3.77,-3.77],[0,-5.753],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.506,0]],"o":[[0,0],[0,0],[-5.753,0],[-3.77,3.77],[0,0],[0,11.505],[0,0],[11.506,0],[0,0],[0,-11.506],[0,0]]}],"h":1},{"t":4,"s":[{"c":true,"v":[[-34.003,-109.325],[18.497,-109.489],[79.459,-109.409],[100.997,-87.576],[100.997,88.342],[80.898,109.181],[18.497,109.193],[-34.503,109.76],[-55.503,92.484],[-55.503,-91.976],[-34.003,-109.325]],"i":[[0,0],[-33,0],[0,0],[0,-11.506],[0,0],[10.599,-0.506],[31.5,0],[0,0],[0,9.674],[0,0],[-10.5,0]],"o":[[0,0],[30.33,0],[14.039,-0.421],[0,0],[0,11.505],[0,0],[-32,0],[-10,-0.837],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.833],"y":[0.859]},"o":{"x":[0.333],"y":[0]}},{"t":8,"s":[{"c":true,"v":[[-32.567,-109.991],[23.211,-112.208],[83.578,-115.094],[105.014,-93.835],[105.196,92.407],[84.952,115.024],[22.77,112.067],[-33.068,110.27],[-53.988,92.985],[-53.988,-92.637],[-32.567,-109.991]],"i":[[0,0],[-33.475,1.617],[0,0],[0,-11.506],[0,0],[11.602,0.847],[31.916,1.156],[0,0],[0,9.674],[0,0],[-10.707,0.102]],"o":[[0,0],[30.272,-1.173],[14.232,-0.984],[0,0],[0,11.505],[0,0],[-32.538,-0.824],[-10.206,-0.926],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.141]}},{"t":13,"s":[{"c":true,"v":[[-28.317,-111.962],[31.476,-120.26],[79.82,-131.925],[97.307,-112.367],[97.997,104.443],[80.768,132.323],[29.803,120.575],[-28.819,111.778],[-49.503,94.468],[-49.503,-94.593],[-28.317,-111.962]],"i":[[0,0],[-29.19,6.404],[0,0],[0,-11.506],[0,0],[12.597,4.854],[27.72,4.58],[0,0],[0,9.673],[0,0],[-11.32,0.404]],"o":[[0,0],[24.953,-4.645],[12.385,-2.651],[0,0],[0,11.505],[0,0],[-28.6,-3.263],[-10.817,-1.188],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":18,"s":[{"c":true,"v":[[7.243,-114.557],[57.814,-130.99],[80.187,-154.399],[88.793,-137.117],[89.982,120.584],[80.382,155.507],[54.93,131.999],[6.876,113.837],[-10.503,96.493],[-10.503,-97.168],[7.243,-114.557]],"i":[[0,0],[-17.317,12.809],[0,0],[0,-11.506],[0,0],[11.116,10.214],[16.283,9.16],[0,0],[0,9.673],[0,0],[-9.013,0.807]],"o":[[0,0],[12.683,-9.289],[7.31,-4.881],[0,0],[0,11.505],[0,0],[-17.3,-6.527],[-8.646,-1.539],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":25,"s":[{"c":true,"v":[[0.489,-118.579],[59.13,-154.283],[32.725,-168.179],[22.399,-155.812],[24.967,153.672],[41.432,165.042],[58.93,144.014],[0.256,119.124],[-13.503,101.711],[-13.503,-101.15],[0.489,-118.579]],"i":[[0,0],[-1.633,25.617],[0,0],[0,-11.506],[0,0],[-6.935,3.293],[1.067,18.321],[0,0],[0,9.674],[0,0],[-7.525,1.615]],"o":[[0,0],[-9.023,-16.317],[-6.728,1.014],[0,0],[0,11.505],[0,0],[-2.6,-13.053],[-7.292,-2.239],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":33,"s":[{"c":true,"v":[[25.735,-123.553],[61.197,-154.527],[-29.141,-161.911],[-41.9,-139.278],[-36.9,138.74],[-24.654,160.375],[60.897,155.077],[25.635,123.459],[15.497,105.977],[15.497,-106.084],[25.735,-123.553]],"i":[[0,0],[8.3,17.524],[0,0],[0,-11.506],[0,0],[-6.249,-3.706],[-24.4,30.92],[0,0],[0,9.674],[0,0],[-6.038,2.422]],"o":[[0,0],[-28.7,-24.476],[-7.161,4.779],[0,0],[0,11.505],[0,0],[12.1,-19.58],[-5.938,-2.94],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":37,"s":[{"c":true,"v":[[42.419,-127.031],[57.997,-160.082],[-64.541,-153.082],[-85.875,-130.248],[-85.875,130.169],[-65.541,152.003],[54.497,161.003],[42.419,126.963],[34.997,109.43],[34.997,-109.531],[42.419,-127.031]],"i":[[0,0],[15.827,9.377],[0,0],[0,-11.506],[0,0],[-10.461,-4.506],[-34,22.494],[0,0],[0,9.674],[0,0],[-4.922,3.028]],"o":[[0,0],[-37,-21.921],[-12.461,6.079],[0,0],[0,11.505],[0,0],[24.637,-16.3],[-4.922,-3.466],[0,0],[0,-9.675],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":40,"s":[{"c":true,"v":[[58.47,-130.461],[59.054,-159.208],[-67.97,-152.791],[-89.232,-130.243],[-89.232,130.175],[-68.827,151.866],[56.422,159.163],[58.47,130.403],[56.711,112.398],[56.711,-112.485],[58.47,-130.461]],"i":[[0,0],[23.373,8.037],[0,0],[0,-11.506],[0,0],[-10.611,-3.862],[-41.925,18.346],[0,0],[0,9.935],[0,0],[-1.776,2.595]],"o":[[0,0],[-44.2,-18.355],[-12.325,5.211],[0,0],[0,11.505],[0,0],[31.73,-13.971],[-1.776,-2.971],[0,0],[0,-9.936],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":52,"s":[{"c":true,"v":[[98.595,-139.037],[30.196,-155.562],[-76.541,-152.062],[-97.625,-130.229],[-97.625,130.189],[-77.041,151.523],[29.733,156.023],[98.595,139.003],[110.997,119.819],[110.997,-119.87],[98.595,-139.037]],"i":[[0,0],[42.238,4.688],[0,0],[0,-11.506],[0,0],[-10.984,-2.253],[-61.736,7.974],[0,0],[0,10.589],[0,0],[6.089,1.514]],"o":[[0,0],[-62.199,-9.441],[-11.984,3.04],[0,0],[0,11.505],[0,0],[49.46,-8.15],[6.089,-1.733],[0,0],[0,-10.591],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":57,"s":[{"c":true,"v":[[93.957,-150.003],[16.803,-153.622],[-81.693,-151.624],[-102.669,-130.22],[-102.669,130.198],[-81.978,151.316],[17.827,153.885],[93.957,149.984],[110.624,129.29],[110.624,-129.319],[93.957,-150.003]],"i":[[0,0],[38.858,0.619],[0,0],[0,-11.506],[0,0],[-11.208,-1.286],[-49.166,1.112],[0,0],[0,11.425],[0,0],[8.754,0.899]],"o":[[0,0],[-47.257,-0.753],[-11.779,1.735],[0,0],[0,11.505],[0,0],[44.834,-0.888],[8.754,-1.029],[0,0],[0,-11.427],[0,0]]}],"i":{"x":[0.25],"y":[1]},"o":{"x":[0.167],"y":[0.167]}},{"t":79,"s":[{"c":true,"v":[[88.541,-151.042],[-1.003,-151.042],[-88.541,-151.042],[-109.375,-130.209],[-109.375,130.209],[-88.541,151.042],[1.997,151.042],[88.541,151.042],[109.375,130.209],[109.375,-130.209],[88.541,-151.042]],"i":[[0,0],[29.848,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[-30.18,0],[0,0],[0,11.505],[0,0],[11.506,0]],"o":[[0,0],[-29.179,0],[-11.506,0],[0,0],[0,11.505],[0,0],[28.848,0],[11.506,0],[0,0],[0,-11.506],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":2,"s":[24],"h":1},{"t":5,"s":[24],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":12,"s":[17],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":13,"s":[15],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":15,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"e":{"a":1,"k":[{"t":2,"s":[74],"h":1},{"t":5,"s":[74],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":12,"s":[83],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":13,"s":[85],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":15,"s":[100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":1,"k":[{"t":2,"s":[122.00000000000001],"h":1},{"t":5,"s":[302],"h":1}],"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":1,"k":[{"t":2,"s":[158.997,250.003],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.77],"y":[0]},"ti":[-15.167,0],"to":[-11.333,0]},{"t":28,"s":[90.997,250.003],"i":{"x":[0.159],"y":[1]},"o":{"x":[0.407],"y":[0]},"ti":[-13.722,0],"to":[7.313,0]},{"t":92,"s":[249.997,250.003],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[-100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"10","layers":[{"ddd":0,"ind":30,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[3,-78],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[57.291,-109.376],[-57.291,-109.376],[-78.125,-88.542],[-78.125,88.542],[-57.291,109.376],[57.291,109.376],[78.125,88.542],[78.125,-88.542],[57.291,-109.376]],"i":[[0,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.506,0]],"o":[[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.506,0],[0,0],[0,-11.506],[0,0]]}}},{"ty":"tm","s":{"a":1,"k":[{"t":2,"s":[26],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":12,"s":[34],"h":1},{"t":13,"s":[50],"h":1},{"t":18,"s":[65],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":25,"s":[77],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":30,"s":[83],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":32,"s":[100],"h":1},{"t":33,"s":[16],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":37,"s":[27.5],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":40,"s":[29],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":45,"s":[36],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":47,"s":[50],"h":1},{"t":52,"s":[63],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":58,"s":[68],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":67,"s":[70],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]}},{"t":92,"s":[73],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"e":{"a":1,"k":[{"t":2,"s":[74],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":12,"s":[65],"h":1},{"t":13,"s":[50],"h":1},{"t":18,"s":[35],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":25,"s":[21],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":30,"s":[14],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":32,"s":[0],"h":1},{"t":33,"s":[81],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":37,"s":[73.5],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":40,"s":[70],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":45,"s":[63],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":47,"s":[50],"h":1},{"t":52,"s":[35],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":58,"s":[32],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":67,"s":[27],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]}},{"t":92,"s":[26],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":1,"k":[{"t":2,"s":[121.00000000000001],"h":1},{"t":18,"s":[301],"h":1},{"t":33,"s":[121.00000000000001],"h":1},{"t":52,"s":[301],"h":1}],"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":1,"k":[{"t":2,"s":[364.587,250.003],"i":{"x":[0.25],"y":[1]},"o":{"x":[0.333],"y":[0]},"ti":[38.167,0],"to":[-38.167,0]},{"t":67,"s":[135.587,250.003],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":12345679,"ty":4,"nm":"Group Layer 8","sr":1,"ks":{"p":{"a":0,"k":[365,464.38524590163934,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[51.229508196721305,51.229508196721305,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[220.741,37.184],[225.501,35.896],[228.749,32.36800000000001],[229.981,27.216000000000008],[228.749,22.12],[225.501,18.592],[220.741,17.304],[215.981,18.592],[212.677,22.12],[211.501,27.216000000000008],[212.677,32.36800000000001],[215.981,35.896],[220.741,37.184],[220.741,37.184],[220.741,37.184]],"i":[[0,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.382000000000062,0.8586999999999989],[1.79200000000003,0],[1.418999999999983,-0.8586999999999989],[0.8220000000000027,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.382000000000062,-0.8586999999999989],[0.8220000000000027,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.418999999999983,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[221.357,43.06400000000001],[214.917,41.608],[210.49300000000005,37.408],[211.221,36.232],[211.221,42.392],[205.173,42.392],[205.173,0],[211.501,0],[211.501,18.36800000000001],[210.49300000000005,16.912000000000006],[214.973,12.88],[221.357,11.424000000000007],[229.085,13.49600000000001],[234.51700000000005,19.152],[236.533,27.216000000000008],[234.51700000000005,35.28],[229.141,40.992],[221.357,43.06400000000001],[221.357,43.06400000000001],[221.357,43.06400000000001]],"i":[[0,0],[1.942000000000007,0.9706999999999937],[1.045999999999935,1.829300000000003],[-0.2426666666666506,0.3919999999999959],[0,-2.053333333333327],[2.015999999999963,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.122666666666674],[0.3360000000000127,0.4853333333333296],[-1.865999999999985,0.9707000000000079],[-2.38900000000001,0],[-2.277000000000044,-1.3813000000000102],[-1.30600000000004,-2.389300000000006],[0,-2.986699999999999],[1.343999999999937,-2.389300000000006],[2.27800000000002,-1.4187000000000012],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.351999999999975,0],[-1.903999999999996,-0.9707000000000079],[0.2426666666666506,-0.3919999999999959],[0,2.053333333333327],[-2.015999999999963,0],[0,-14.13066666666667],[2.109333333333325,0],[0,6.122666666666667],[-0.3360000000000127,-0.4853333333333296],[1.120000000000005,-1.717300000000009],[1.867000000000075,-0.9706999999999937],[2.875,0],[2.314999999999941,1.381299999999996],[1.343999999999937,2.389300000000006],[0,2.986699999999999],[-1.30600000000004,2.389300000000006],[-2.27699999999993,1.381299999999996],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[181.87,43.06400000000001],[176.438,42],[172.854,38.976],[171.566,34.384],[172.63,29.960000000000008],[176.046,26.656000000000006],[181.814,24.752],[192.342,23.016000000000005],[192.342,28],[183.046,29.624],[179.35,31.248],[178.174,34.16],[179.462,37.016000000000005],[182.878,38.08],[187.35799999999995,36.96000000000001],[190.38199999999995,33.992],[191.446,29.792],[191.446,22.008],[189.766,18.36800000000001],[185.398,16.912000000000006],[180.974,18.256],[178.23,21.616],[172.966,18.98400000000001],[175.71,15.064000000000007],[180.134,12.376],[185.566,11.424000000000007],[191.894,12.768],[196.206,16.52],[197.774,22.008],[197.774,42.392],[191.726,42.392],[191.726,36.904],[193.014,37.072],[190.27,40.264],[186.518,42.336],[181.87,43.06400000000001],[181.87,43.06400000000001],[181.87,43.06400000000001]],"i":[[0,0],[1.567999999999984,0.7092999999999989],[0.8589999999999236,1.2693000000000012],[0,1.7547],[-0.7089999999999463,1.306699999999992],[-1.530000000000086,0.8960000000000008],[-2.313999999999965,0.3733000000000004],[-3.509333333333302,0.5786666666666633],[0,-1.661333333333332],[3.098666666666645,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8579999999999472,-0.7467000000000041],[-1.381000000000085,0],[-1.268999999999892,0.7466999999999899],[-0.7089999999999463,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829999999999927,0],[1.269999999999982,-0.8960000000000008],[0.5979999999999563,-1.381299999999996],[1.754666666666708,0.8773333333333255],[-1.269000000000005,1.11999999999999],[-1.680000000000064,0.6346999999999952],[-1.903999999999996,0],[-1.828999999999951,-0.8960000000000008],[-1.008000000000038,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999963,0],[0,1.829333333333338],[-0.4293333333333749,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.717999999999961,0],[0,0],[0,0]],"o":[[-2.052999999999997,0],[-1.529999999999973,-0.7467000000000041],[-0.8580000000000609,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999573,-1.306700000000006],[1.530999999999949,-0.8960000000000008],[3.509333333333302,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666645,0.541333333333327],[-1.680000000000064,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.8959999999999582,0.7092999999999989],[1.717999999999961,0],[1.307000000000016,-0.7467000000000041],[0.7100000000000364,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.081999999999994,-0.9707000000000079],[-1.680000000000064,0],[-1.232000000000085,0.8586999999999989],[-1.754666666666708,-0.8773333333333255],[0.5599999999999454,-1.493300000000005],[1.269999999999982,-1.157300000000006],[1.717999999999961,-0.6347000000000094],[2.389999999999986,0],[1.866999999999962,0.8960000000000008],[1.045999999999935,1.568000000000012],[0,6.794666666666672],[-2.015999999999963,0],[0,-1.829333333333338],[0.4293333333333749,0.05599999999999739],[-0.70900000000006,1.2319999999999993],[-1.081999999999994,0.8960000000000008],[-1.380999999999972,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[159.072,42.392],[159.072,0],[165.4,0],[165.4,42.392],[159.072,42.392],[159.072,42.392],[159.072,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-14.13066666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,14.13066666666667],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[138.952,43.06400000000001],[130.888,40.992],[125.456,35.28],[123.496,27.16],[125.456,19.040000000000006],[130.832,13.49600000000001],[138.448,11.424000000000007],[144.552,12.600000000000009],[149.088,15.848],[151.888,20.49600000000001],[152.896,26.096],[152.84,27.608],[152.616,29.064000000000007],[128.48,29.064000000000007],[128.48,24.024],[149.032,24.024],[146.008,26.320000000000007],[145.616,21.448000000000008],[142.816,18.032],[138.448,16.744],[133.968,18.032],[130.944,21.616],[130.104,27.216000000000008],[130.944,32.592],[134.192,36.176],[139.008,37.464],[143.65599999999995,36.232],[146.736,33.040000000000006],[151.888,35.56],[149.088,39.42400000000001],[144.60799999999995,42.11200000000001],[138.952,43.06400000000001],[138.952,43.06400000000001],[138.952,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.307000000000016,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.836999999999989,0],[-1.79200000000003,-0.784000000000006],[-1.231999999999971,-1.381299999999996],[-0.6349999999999909,-1.754700000000014],[0,-1.978700000000003],[0.03699999999992087,-0.5227000000000004],[0.1119999999999663,-0.4480000000000075],[8.04533333333336,0],[0,1.680000000000007],[-6.850666666666712,0],[1.008000000000038,-0.7653333333333308],[0.6349999999999909,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.680000000000064,0],[1.307000000000016,-0.8586999999999989],[0.70900000000006,-1.567999999999998],[-0.1490000000000009,-2.202700000000007],[-0.7469999999999573,-1.530699999999996],[-1.380999999999972,-0.8586999999999989],[-1.79200000000003,0],[-1.268999999999892,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333386,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.755000000000109,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.315000000000055,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999937,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.277000000000044,0],[1.79200000000003,0.7839999999999918],[1.232000000000085,1.344000000000008],[0.6720000000000255,1.7547],[0,0.4852999999999952],[-0.03700000000003456,0.5227000000000004],[-8.04533333333336,0],[0,-1.680000000000007],[6.850666666666712,0],[-1.008000000000038,0.7653333333333308],[0.3729999999999336,-1.829300000000003],[-0.59699999999998,-1.456000000000003],[-1.232000000000085,-0.8586999999999989],[-1.67999999999995,0],[-1.306999999999903,0.8213000000000079],[-0.7089999999999463,1.530699999999996],[-0.1870000000000118,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418999999999983,0.8586999999999989],[1.828999999999951,0],[1.307000000000016,-0.8212999999999937],[1.717333333333386,0.8400000000000034],[-0.59699999999998,1.4187000000000012],[-1.231999999999971,1.11999999999999],[-1.716999999999985,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,7.951999999999998],[111.001,0.6720000000000041],[117.329,0.6720000000000041],[117.329,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,42.392],[111.001,12.096],[117.329,12.096],[117.329,42.392],[111.001,42.392],[111.001,42.392],[111.001,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[101.41,42.72800000000001],[94.01800000000003,40.040000000000006],[91.38599999999997,32.48],[91.38599999999997,17.808000000000007],[86.06600000000003,17.808000000000007],[86.06600000000003,12.096],[86.90599999999995,12.096],[90.21000000000004,10.864],[91.38599999999997,7.504000000000005],[91.38599999999997,5.152000000000001],[97.71400000000006,5.152000000000001],[97.71400000000006,12.096],[104.602,12.096],[104.602,17.808000000000007],[97.71400000000006,17.808000000000007],[97.71400000000006,32.2],[98.21799999999996,34.888000000000005],[99.84199999999998,36.568],[102.754,37.128],[103.76200000000006,37.072],[104.826,36.96000000000001],[104.826,42.392],[103.09,42.616],[101.41,42.72800000000001],[101.41,42.72800000000001],[101.41,42.72800000000001]],"i":[[0,0],[1.754999999999995,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333312,0],[0,1.903999999999996],[-0.2799999999999727,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999935,0],[0,-1.903999999999996],[2.295999999999935,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7469999999999573,-0.4106999999999914],[-1.19500000000005,0],[-0.3730000000000473,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6349999999999909,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333312,0],[0,-1.903999999999996],[0.2799999999999727,0],[1.419000000000096,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999935,0],[0,1.903999999999996],[-2.295999999999935,0],[0,4.797333333333327],[0,1.045299999999997],[0.3360000000000127,0.7092999999999989],[0.7470000000000709,0.3733000000000004],[0.2989999999999782,0],[0.3729999999999336,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[78.71499999999997,42.72800000000001],[71.32299999999998,40.040000000000006],[68.69100000000003,32.48],[68.69100000000003,17.808000000000007],[63.37099999999998,17.808000000000007],[63.37099999999998,12.096],[64.21100000000001,12.096],[67.51499999999999,10.864],[68.69100000000003,7.504000000000005],[68.69100000000003,5.152000000000001],[75.019,5.152000000000001],[75.019,12.096],[81.90700000000004,12.096],[81.90700000000004,17.808000000000007],[75.019,17.808000000000007],[75.019,32.2],[75.52300000000002,34.888000000000005],[77.14699999999999,36.568],[80.05900000000003,37.128],[81.06700000000001,37.072],[82.13099999999997,36.96000000000001],[82.13099999999997,42.392],[80.39499999999998,42.616],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001]],"i":[[0,0],[1.754000000000019,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7470000000000141,-0.4106999999999914],[-1.19500000000005,0],[-0.3740000000000236,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6340000000000146,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173999999999978,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418000000000006,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7460000000000377,0.3733000000000004],[0.297999999999945,0],[0.3730000000000473,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,37.184],[48.94799999999998,35.896],[52.19600000000003,32.36800000000001],[53.428,27.216000000000008],[52.19600000000003,22.12],[48.94799999999998,18.592],[44.18799999999999,17.304],[39.428,18.592],[36.12400000000002,22.12],[34.94799999999998,27.216000000000008],[36.12400000000002,32.36800000000001],[39.428,35.896],[44.18799999999999,37.184],[44.18799999999999,37.184],[44.18799999999999,37.184]],"i":[[0,0],[-1.381999999999948,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.381000000000029,0.8586999999999989],[1.79200000000003,0],[1.418000000000006,-0.8586999999999989],[0.8209999999999695,-1.493300000000005],[0,-1.904000000000011],[-0.7840000000000487,-1.5307000000000102],[-1.382000000000005,-0.8586999999999989],[-1.754999999999995,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.381000000000029,-0.8586999999999989],[0.8209999999999695,-1.5307000000000102],[0,-1.904000000000011],[-0.7840000000000487,-1.493300000000005],[-1.381999999999948,-0.8586999999999989],[-1.754999999999995,0],[-1.382000000000005,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.418000000000006,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,43.06400000000001],[36.18000000000001,40.992],[30.46800000000002,35.336],[28.33999999999997,27.216000000000008],[30.46800000000002,19.096],[36.18000000000001,13.49600000000001],[44.18799999999999,11.424000000000007],[52.19600000000003,13.49600000000001],[57.85199999999998,19.096],[59.98000000000002,27.216000000000008],[57.85199999999998,35.392],[52.139999999999986,41.048],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001]],"i":[[0,0],[2.425999999999988,1.381299999999996],[1.418000000000006,2.389300000000006],[0,3.024000000000001],[-1.41900000000004,2.352000000000004],[-2.389999999999986,1.343999999999994],[-2.949999999999989,0],[-2.352000000000032,-1.3813000000000102],[-1.381999999999948,-2.389300000000006],[0,-3.061300000000003],[1.418000000000006,-2.389299999999992],[2.38900000000001,-1.381299999999996],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.911999999999978,0],[-2.389999999999986,-1.381299999999996],[-1.41900000000004,-2.389299999999992],[0,-3.061300000000003],[1.418000000000006,-2.389300000000006],[2.38900000000001,-1.3813000000000102],[2.98599999999999,0],[2.388999999999953,1.343999999999994],[1.418000000000006,2.352000000000004],[0,3.061299999999989],[-1.418999999999983,2.389300000000006],[-2.389999999999986,1.343999999999994],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.608000000000004,0.6720000000000041],[6.608000000000004,36.512],[24.639999999999986,36.512],[24.639999999999986,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.202666666666687,0],[0,-11.94666666666666],[-6.01066666666668,0],[0,-1.959999999999994],[8.21333333333331,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.202666666666687,0],[0,11.94666666666667],[6.01066666666668,0],[0,1.959999999999994],[-8.21333333333331,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[98.08047485351562,-21.67217254638672],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[246.681,42.392],[246.681,0],[253.009,0],[253.009,18.032],[252.001,17.248],[255.585,12.936000000000007],[261.297,11.424000000000007],[267.23299999999995,12.88],[271.265,16.912000000000006],[272.721,22.792],[272.721,42.392],[266.449,42.392],[266.449,24.528000000000006],[265.553,20.664],[263.201,18.2],[259.729,17.304],[256.25699999999995,18.2],[253.849,20.664],[253.009,24.528000000000006],[253.009,42.392],[246.681,42.392],[246.681,42.392],[246.681,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.010666666666665],[0.3360000000000127,0.2613333333333259],[-1.643000000000029,0.9706999999999937],[-2.166000000000054,0],[-1.717999999999961,-0.9706999999999937],[-0.9710000000000036,-1.717300000000009],[0,-2.202699999999993],[0,-6.533333333333331],[2.090666666666721,0],[0,5.954666666666668],[0.59699999999998,1.045299999999997],[1.007999999999925,0.5600000000000023],[1.305999999999926,0],[1.045000000000073,-0.5973000000000042],[0.59699999999998,-1.082700000000003],[0,-1.493300000000005],[0,-5.954666666666668],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,6.010666666666665],[-0.3360000000000127,-0.2613333333333259],[0.7459999999999809,-1.903999999999996],[1.641999999999967,-1.0080000000000098],[2.240000000000009,0],[1.717000000000098,0.9707000000000079],[0.9700000000000273,1.717299999999994],[0,6.533333333333331],[-2.090666666666721,0],[0,-5.954666666666668],[0,-1.5307000000000102],[-0.5599999999999454,-1.082700000000003],[-1.008000000000038,-0.5973000000000042],[-1.270000000000095,0],[-1.007999999999953,0.5600000000000023],[-0.5600000000000023,1.082700000000003],[0,5.954666666666668],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[237.089,42.72800000000001],[229.697,40.040000000000006],[227.065,32.48],[227.065,17.808000000000007],[221.745,17.808000000000007],[221.745,12.096],[222.585,12.096],[225.889,10.864],[227.065,7.504000000000005],[227.065,5.152000000000001],[233.393,5.152000000000001],[233.393,12.096],[240.281,12.096],[240.281,17.808000000000007],[233.393,17.808000000000007],[233.393,32.2],[233.897,34.888000000000005],[235.521,36.568],[238.433,37.128],[239.441,37.072],[240.505,36.96000000000001],[240.505,42.392],[238.769,42.616],[237.089,42.72800000000001],[237.089,42.72800000000001],[237.089,42.72800000000001]],"i":[[0,0],[1.755000000000052,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7459999999999809,-0.4106999999999914],[-1.194000000000017,0],[-0.3729999999999905,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6350000000000477,-0.07469999999999288],[0.48599999999999,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.753999999999962,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418999999999983,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7470000000000141,0.3733000000000004],[0.2989999999999782,0],[0.3740000000000236,-0.03730000000000189],[0,1.810666666666663],[-0.5220000000000482,0.0747000000000071],[-0.6339999999999577,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,7.951999999999998],[210.259,0.6720000000000041],[216.587,0.6720000000000041],[216.587,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,42.392],[210.259,12.096],[216.587,12.096],[216.587,42.392],[210.259,42.392],[210.259,42.392],[210.259,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[169.688,42.392],[159.272,12.096],[165.992,12.096],[173.944,36.232],[171.592,36.232],[179.712,12.096],[185.48,12.096],[193.544,36.232],[191.192,36.232],[199.2,12.096],[205.92,12.096],[195.448,42.392],[189.736,42.392],[181.56,17.696],[183.632,17.696],[175.456,42.392],[169.688,42.392],[169.688,42.392],[169.688,42.392]],"i":[[0,0],[3.47199999999998,10.09866666666667],[-2.240000000000009,0],[-2.650666666666666,-8.045333333333332],[0.7839999999999918,0],[-2.706666666666649,8.045333333333332],[-1.922666666666657,0],[-2.687999999999988,-8.045333333333332],[0.7839999999999918,0],[-2.669333333333327,8.045333333333332],[-2.240000000000009,0],[3.490666666666641,-10.09866666666667],[1.903999999999996,0],[2.725333333333367,8.232],[-0.6906666666666865,0],[2.72533333333331,-8.232],[1.922666666666657,0],[0,0],[0,0]],"o":[[-3.47199999999998,-10.09866666666666],[2.240000000000009,0],[2.650666666666666,8.045333333333332],[-0.7839999999999918,0],[2.706666666666649,-8.045333333333332],[1.922666666666657,0],[2.687999999999988,8.045333333333332],[-0.7839999999999918,0],[2.669333333333327,-8.045333333333332],[2.240000000000009,0],[-3.490666666666641,10.09866666666666],[-1.903999999999996,0],[-2.725333333333367,-8.232],[0.6906666666666865,0],[-2.72533333333331,8.232],[-1.922666666666657,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[155.86146545410156,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[132.444,43.06400000000001],[124.38,40.992],[118.948,35.28],[116.988,27.16],[118.948,19.040000000000006],[124.324,13.49600000000001],[131.94,11.424000000000007],[138.044,12.600000000000009],[142.58,15.848],[145.38,20.49600000000001],[146.388,26.096],[146.332,27.608],[146.108,29.064000000000007],[121.972,29.064000000000007],[121.972,24.024],[142.524,24.024],[139.5,26.320000000000007],[139.108,21.448000000000008],[136.308,18.032],[131.94,16.744],[127.46,18.032],[124.436,21.616],[123.596,27.216000000000008],[124.436,32.592],[127.684,36.176],[132.5,37.464],[137.148,36.232],[140.228,33.040000000000006],[145.38,35.56],[142.58,39.42400000000001],[138.1,42.11200000000001],[132.444,43.06400000000001],[132.444,43.06400000000001],[132.444,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.305999999999983,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.838000000000022,0],[-1.79200000000003,-0.784000000000006],[-1.232000000000028,-1.381299999999996],[-0.6350000000000477,-1.754700000000014],[0,-1.978700000000003],[0.03699999999997772,-0.5227000000000004],[0.1120000000000232,-0.4480000000000075],[8.045333333333303,0],[0,1.680000000000007],[-6.850666666666655,0],[1.007999999999981,-0.7653333333333308],[0.6340000000000146,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.67999999999995,0],[1.305999999999983,-0.8586999999999989],[0.7090000000000032,-1.567999999999998],[-0.1499999999999773,-2.202700000000007],[-0.7470000000000141,-1.530699999999996],[-1.382000000000005,-0.8586999999999989],[-1.791999999999973,0],[-1.269999999999982,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333329,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.754000000000019,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.314999999999998,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999994,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.276999999999987,0],[1.791999999999973,0.7839999999999918],[1.231999999999971,1.344000000000008],[0.6719999999999686,1.7547],[0,0.4852999999999952],[-0.03800000000001091,0.5227000000000004],[-8.045333333333303,0],[0,-1.680000000000007],[6.850666666666655,0],[-1.007999999999981,0.7653333333333308],[0.3730000000000473,-1.829300000000003],[-0.5979999999999563,-1.456000000000003],[-1.232000000000028,-0.8586999999999989],[-1.680000000000007,0],[-1.307000000000016,0.8213000000000079],[-0.7100000000000364,1.530699999999996],[-0.186999999999955,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418000000000006,0.8586999999999989],[1.829000000000008,0],[1.305999999999983,-0.8212999999999937],[1.717333333333329,0.8400000000000034],[-0.5980000000000132,1.4187000000000012],[-1.232000000000028,1.11999999999999],[-1.718000000000018,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[95.32,37.184],[100.024,35.896],[103.328,32.36800000000001],[104.56,27.216000000000008],[103.328,22.12],[100.024,18.592],[95.32,17.304],[90.56,18.592],[87.256,22.12],[86.08000000000001,27.216000000000008],[87.256,32.36800000000001],[90.50399999999999,35.896],[95.32,37.184],[95.32,37.184],[95.32,37.184]],"i":[[0,0],[-1.381,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.820999999999998,1.493299999999991],[1.419000000000011,0.8586999999999989],[1.754999999999995,0],[1.418999999999983,-0.8586999999999989],[0.7839999999999918,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.381,-0.8586999999999989],[-1.792000000000002,0],[0,0],[0,0]],"o":[[1.754999999999995,0],[1.419000000000011,-0.8586999999999989],[0.820999999999998,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.381,-0.8586999999999989],[-1.754999999999995,0],[-1.419000000000011,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.7839999999999918,1.493299999999991],[1.419000000000011,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[94.70400000000001,43.06400000000001],[86.864,40.992],[81.43199999999999,35.28],[79.47200000000001,27.216000000000008],[81.488,19.152],[86.91999999999999,13.49600000000001],[94.648,11.424000000000007],[101.088,12.88],[105.512,16.912000000000006],[104.56,18.36800000000001],[104.56,0],[110.832,0],[110.832,42.392],[104.84,42.392],[104.84,36.232],[105.568,37.408],[101.088,41.608],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001]],"i":[[0,0],[2.314999999999998,1.381299999999996],[1.344000000000023,2.389300000000006],[0,2.986699999999999],[-1.343999999999994,2.389300000000006],[-2.276999999999987,1.381299999999996],[-2.875,0],[-1.8669999999999902,-0.9706999999999937],[-1.082999999999998,-1.717300000000009],[0.3173333333333233,-0.4853333333333296],[0,6.122666666666674],[-2.090666666666664,0],[0,-14.13066666666667],[1.99733333333333,0],[0,2.053333333333327],[-0.242666666666679,-0.3919999999999959],[1.941000000000003,-0.9707000000000079],[2.314999999999998,0],[0,0],[0,0]],"o":[[-2.912000000000006,0],[-2.277000000000015,-1.4187000000000012],[-1.306999999999988,-2.389300000000006],[0,-2.986699999999999],[1.343999999999994,-2.389300000000006],[2.277000000000015,-1.3813000000000102],[2.426999999999992,0],[1.867000000000019,0.9707000000000079],[-0.3173333333333233,0.4853333333333296],[0,-6.122666666666674],[2.090666666666664,0],[0,14.13066666666667],[-1.99733333333333,0],[0,-2.053333333333327],[0.242666666666679,0.3919999999999959],[-1.045000000000016,1.829300000000003],[-1.941000000000003,0.9706999999999937],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[57.40100000000001,43.06400000000001],[51.968999999999994,42],[48.38499999999999,38.976],[47.09700000000001,34.384],[48.161,29.960000000000008],[51.577,26.656000000000006],[57.345,24.752],[67.87299999999999,23.016000000000005],[67.87299999999999,28],[58.577,29.624],[54.881,31.248],[53.70500000000001,34.16],[54.992999999999995,37.016000000000005],[58.40899999999999,38.08],[62.88900000000001,36.96000000000001],[65.91300000000001,33.992],[66.977,29.792],[66.977,22.008],[65.297,18.36800000000001],[60.929,16.912000000000006],[56.505,18.256],[53.761,21.616],[48.496999999999986,18.98400000000001],[51.240999999999985,15.064000000000007],[55.66499999999999,12.376],[61.09700000000001,11.424000000000007],[67.42500000000001,12.768],[71.737,16.52],[73.305,22.008],[73.305,42.392],[67.257,42.392],[67.257,36.904],[68.54499999999999,37.072],[65.80099999999999,40.264],[62.04900000000001,42.336],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001]],"i":[[0,0],[1.568000000000012,0.7092999999999989],[0.8590000000000089,1.2693000000000012],[0,1.7547],[-0.7090000000000032,1.306699999999992],[-1.531000000000006,0.8960000000000008],[-2.314999999999998,0.3733000000000004],[-3.509333333333331,0.5786666666666633],[0,-1.661333333333332],[3.098666666666674,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8590000000000089,-0.7467000000000041],[-1.381,0],[-1.269000000000005,0.7466999999999899],[-0.7090000000000032,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829000000000008,0],[1.269000000000005,-0.8960000000000008],[0.5970000000000084,-1.381299999999996],[1.754666666666679,0.8773333333333255],[-1.268999999999977,1.11999999999999],[-1.680000000000007,0.6346999999999952],[-1.903999999999996,0],[-1.829000000000008,-0.8960000000000008],[-1.0080000000000098,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999991,0],[0,1.829333333333338],[-0.429333333333318,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.716999999999985,0],[0,0],[0,0]],"o":[[-2.053000000000026,0],[-1.531000000000006,-0.7467000000000041],[-0.8589999999999804,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999857,-1.306700000000006],[1.531000000000006,-0.8960000000000008],[3.509333333333331,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666674,0.541333333333327],[-1.680000000000007,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.896000000000015,0.7092999999999989],[1.717000000000013,0],[1.306999999999988,-0.7467000000000041],[0.7089999999999748,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.082999999999998,-0.9707000000000079],[-1.680000000000007,0],[-1.2319999999999993,0.8586999999999989],[-1.754666666666679,-0.8773333333333255],[0.5600000000000023,-1.493300000000005],[1.269000000000005,-1.157300000000006],[1.717000000000013,-0.6347000000000094],[2.388999999999982,0],[1.86699999999999,0.8960000000000008],[1.045000000000016,1.568000000000012],[0,6.794666666666672],[-2.015999999999991,0],[0,-1.829333333333338],[0.429333333333318,0.05599999999999739],[-0.7089999999999748,1.2319999999999993],[-1.082999999999998,0.8960000000000008],[-1.381,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.159999999999997,0.6720000000000041],[21.84,22.400000000000006],[18.75999999999999,22.400000000000006],[34.16,0.6720000000000041],[40.31999999999999,0.6720000000000041],[40.31999999999999,42.392],[33.768,42.392],[33.768,8.456000000000003],[36.232,9.128],[20.49600000000001,30.632000000000005],[19.824000000000012,30.632000000000005],[4.424000000000007,9.128],[6.608000000000004,8.456000000000003],[6.608000000000004,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.053333333333342,0],[-5.226666666666659,-7.242666666666665],[1.026666666666671,0],[-5.133333333333326,7.242666666666672],[-2.053333333333342,0],[0,-13.90666666666667],[2.183999999999997,0],[0,11.312],[-0.8213333333333424,-0.2240000000000038],[5.245333333333321,-7.168000000000006],[0.2239999999999895,0],[5.133333333333326,7.168000000000006],[-0.7280000000000086,0.2240000000000038],[0,-11.312],[2.202666666666659,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.053333333333342,0],[5.226666666666659,7.242666666666672],[-1.026666666666671,0],[5.133333333333326,-7.242666666666665],[2.053333333333342,0],[0,13.90666666666666],[-2.183999999999997,0],[0,-11.312],[0.8213333333333424,0.2240000000000038],[-5.245333333333321,7.168000000000006],[-0.2239999999999895,0],[-5.133333333333326,-7.168000000000006],[0.7280000000000086,-0.2240000000000038],[0,11.312],[-2.202666666666659,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[2.91259765625,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[702.6863719370097,144],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":72,"ix":2}},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[56.54167175292969,-0.000022762338630855083],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":80,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[122.0000003294881,25.00000012138912],"ix":2},"a":{"a":0,"k":[56.54167175292969,-0.00002288818359375],"ix":2},"s":{"a":0,"k":[34.403572049765366,34.403572049765366],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"ind":31,"ty":3,"nm":"Color & Stroke Change","sr":1,"ks":{"p":{"a":0,"k":[250,250],"ix":2},"a":{"a":0,"k":[50,50],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"0","w":500,"h":500,"ind":32,"ty":0,"nm":"hover-carousel","sr":1,"ks":{"p":{"a":0,"k":[50,50],"ix":2},"a":{"a":0,"k":[250,250],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":31}],"markers":[]} \ No newline at end of file diff --git a/frontend/public/lotties/spinning-coin.json b/frontend/public/lotties/spinning-coin.json new file mode 100644 index 000000000..e5a0ae555 --- /dev/null +++ b/frontend/public/lotties/spinning-coin.json @@ -0,0 +1 @@ +{"v":"5.7.5","fr":100,"ip":0,"op":102,"w":500,"h":500,"nm":"Comp 1","ddd":0,"metadata":{},"assets":[{"id":"0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"1","w":251,"h":122,"ind":2,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[125,379],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":37,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":1},{"ddd":0,"ind":3,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"2","w":460,"h":126,"ind":4,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[20,284],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":37,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":3},{"ddd":0,"ind":5,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"3","w":460,"h":127,"ind":6,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[20,90],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":37,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":5},{"ddd":0,"ind":7,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"4","w":504,"h":127,"ind":8,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[-3,187],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":37,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":7},{"ddd":0,"ind":9,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"5","w":251,"h":123,"ind":10,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[125,-2],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":37,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":9},{"ddd":0,"ind":11,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"6","w":505,"h":503,"ind":12,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[-3,-2],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":37,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":11},{"ddd":0,"ind":13,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"7","w":505,"h":503,"ind":14,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[-3,-2],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":37,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":13},{"ddd":0,"ind":15,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"8","w":251,"h":122,"ind":16,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[125,379],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":37,"s":[100],"h":1},{"t":100,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":15},{"ddd":0,"ind":17,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"9","w":460,"h":126,"ind":18,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[20,284],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":37,"s":[100],"h":1},{"t":100,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":17},{"ddd":0,"ind":19,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"10","w":460,"h":127,"ind":20,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[20,90],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":37,"s":[100],"h":1},{"t":100,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":19},{"ddd":0,"ind":21,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"11","w":504,"h":127,"ind":22,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[-3,187],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":37,"s":[100],"h":1},{"t":100,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":21},{"ddd":0,"ind":23,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"12","w":251,"h":123,"ind":24,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[125,-2],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":37,"s":[100],"h":1},{"t":100,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":23},{"ddd":0,"ind":25,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"13","w":505,"h":503,"ind":26,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[-3,-2],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":37,"s":[100],"h":1},{"t":100,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":25},{"ddd":0,"ind":27,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"14","w":505,"h":503,"ind":28,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[-3,-2],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":37,"s":[100],"h":1},{"t":100,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":27},{"ddd":0,"ind":29,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"15","w":418,"h":418,"ind":30,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[41,41],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"h":1},{"t":2,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":29},{"ddd":0,"ind":31,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"16","w":418,"h":418,"ind":32,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[41,41],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":100,"s":[100],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":31}]},{"id":"1","layers":[{"ddd":0,"ind":33,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-125,-379],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[1,-195],[0.5,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":6,"s":[{"c":false,"v":[[-9.417,-195],[10.917,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":18,"s":[{"c":false,"v":[[-61.5,-195],[63,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":31,"s":[{"c":false,"v":[[-9.417,-195],[10.917,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":37,"s":[{"c":false,"v":[[1,-195],[0.5,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":1,"k":[{"t":0,"s":[250.75,442.5],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]},"ti":[0,-0.087],"to":[0,0.018]},{"t":6,"s":[250.75,442.667],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]},"ti":[0,0],"to":[0,0.287]},{"t":18,"s":[250.75,443.5],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]},"ti":[0,0.287],"to":[0,0]},{"t":31,"s":[250.75,442.667],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]},"ti":[0,0.018],"to":[0,-0.087]},{"t":37,"s":[250.75,442.5],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0.75,-195],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"2","layers":[{"ddd":0,"ind":34,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-20,-284],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":true,"v":[[167.25,-195],[167.25,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":6,"s":[{"c":false,"v":[[129.125,-195],[149.875,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":18,"s":[{"c":false,"v":[[-61.5,-195],[63,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":31,"s":[{"c":false,"v":[[-149.208,-195],[-128.458,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":37,"s":[{"c":true,"v":[[-166.75,-195],[-166.75,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[417.25,347],"ix":2},"a":{"a":0,"k":[167.25,-195],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"3","layers":[{"ddd":0,"ind":35,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-20,-90],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":true,"v":[[167.25,-195],[167.25,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":6,"s":[{"c":false,"v":[[129.125,-195],[149.875,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":18,"s":[{"c":false,"v":[[-61.5,-195],[63,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":31,"s":[{"c":false,"v":[[-149.208,-195],[-128.458,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":37,"s":[{"c":true,"v":[[-166.75,-195],[-166.75,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[417.25,153.5],"ix":2},"a":{"a":0,"k":[167.25,-195],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"4","layers":[{"ddd":0,"ind":36,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[3,-187],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":true,"v":[[193.25,-195],[193.25,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":6,"s":[{"c":false,"v":[[150.792,-195],[171.542,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":18,"s":[{"c":false,"v":[[-61.5,-195],[63,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":31,"s":[{"c":false,"v":[[-170.875,-195],[-150.125,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":37,"s":[{"c":true,"v":[[-192.75,-195],[-192.75,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[443.25,250.5],"ix":2},"a":{"a":0,"k":[193.25,-195],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"5","layers":[{"ddd":0,"ind":37,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-125,2],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[1,-195],[0.5,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":6,"s":[{"c":false,"v":[[-9.417,-195],[10.917,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":18,"s":[{"c":false,"v":[[-61.5,-195],[63,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":31,"s":[{"c":false,"v":[[-9.417,-195],[10.917,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":37,"s":[{"c":false,"v":[[1,-195],[0.5,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":1,"k":[{"t":0,"s":[250.75,57.5],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]},"ti":[0,0.087],"to":[0,-0.018]},{"t":6,"s":[250.75,57.333],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]},"ti":[0,0],"to":[0,-0.287]},{"t":18,"s":[250.75,56.5],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]},"ti":[0,-0.287],"to":[0,0]},{"t":31,"s":[250.75,57.333],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]},"ti":[0,-0.018],"to":[0,0.087]},{"t":37,"s":[250.75,57.5],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0.75,-195],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"6","layers":[{"ddd":0,"ind":38,"ty":3,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[252.998,251.999],"ix":2},"a":{"a":0,"k":[249.998,249.999],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"ind":39,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":38},{"ddd":0,"refId":"17","w":231,"h":387,"ind":40,"ty":0,"nm":"Group 2","sr":1,"ks":{"p":{"a":0,"k":[-124,-84],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"h":1},{"t":18,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":39},{"ddd":0,"ind":41,"ty":4,"nm":"Group 1","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"parent":38,"shapes":[{"ty":"gr","nm":"Group 1","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":true,"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34],[192.71,109.369]],"i":[[0,0],[106.43,0],[0,106.43],[-106.43,0],[0,-106.43]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":6,"s":[{"c":true,"v":[[150.425,109.397],[-10.166,302.106],[-170.757,109.397],[-10.166,-83.313],[150.425,109.397]],"i":[[0,0],[88.692,0],[0,106.43],[-88.692,0],[0,-106.43]],"o":[[0,106.43],[-88.692,0],[0,-106.43],[88.692,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":18,"s":[{"c":true,"v":[[-61.001,109.534],[-61,302.243],[-60.998,109.534],[-61,-83.176],[-61.001,109.534]],"i":[[0,0],[-0.001,0],[0,106.43],[0.001,0],[0,-106.43]],"o":[[0,106.43],[0.001,0],[0,-106.43],[-0.001,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":31,"s":[{"c":true,"v":[[150.425,109.397],[-10.166,302.106],[-170.757,109.397],[-10.166,-83.313],[150.425,109.397]],"i":[[0,0],[88.692,0],[0,106.43],[-88.692,0],[0,-106.43]],"o":[[0,106.43],[-88.692,0],[0,-106.43],[88.692,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":37,"s":[{"c":true,"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34],[192.71,109.369]],"i":[[0,0],[106.43,0],[0,106.43],[-106.43,0],[0,-106.43]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":1,"k":[{"t":0,"s":[100],"h":1},{"t":6,"s":[100],"h":1},{"t":18,"s":[50],"h":1},{"t":31,"s":[50],"h":1}],"ix":2},"o":{"a":0,"k":90,"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"17","layers":[{"ddd":0,"ind":42,"ty":4,"nm":"Group 2: group style","sr":1,"ks":{"p":{"a":0,"k":[124,84],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":"Group 2","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[0,-20.836],[0,20.836]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":6,"s":[{"c":false,"v":[[-10.167,-20.808],[-10.167,20.863]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":18,"s":[{"c":false,"v":[[-60.999,-20.671],[-60.999,21]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":31,"s":[{"c":false,"v":[[-10.167,-20.808],[-10.167,20.863]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":37,"s":[{"c":false,"v":[[0,-20.836],[0,20.836]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[0.006,197.911],[0.006,239.582]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":6,"s":[{"c":false,"v":[[-10.162,197.938],[-10.162,239.61]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":18,"s":[{"c":false,"v":[[-61,198.075],[-61,239.746]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":31,"s":[{"c":false,"v":[[-10.162,197.938],[-10.162,239.61]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":37,"s":[{"c":false,"v":[[0.006,197.911],[0.006,239.582]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 3","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":6,"s":[{"c":false,"v":[[-47.054,158.552],[-10.162,197.943],[26.731,158.552],[-18.148,102.248],[-47.054,60.227],[-10.162,20.859],[26.731,65.13]],"i":[[0,0],[-20.375,0],[0,24.45],[31.566,22.661],[0,24.45],[-20.375,0],[0,-24.45]],"o":[[0,24.45],[20.375,0],[0,-24.45],[-14.85,-10.661],[0,-24.45],[20.375,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":18,"s":[{"c":false,"v":[[-60.999,158.689],[-61,198.08],[-61,158.689],[-60.999,102.385],[-60.999,60.364],[-61,20.995],[-61,65.266]],"i":[[0,0],[0,0],[0,24.45],[0,22.661],[0,24.45],[0,0],[0,-24.45]],"o":[[0,24.45],[0,0],[0,-24.45],[0,-10.661],[0,-24.45],[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":31,"s":[{"c":false,"v":[[-47.054,158.552],[-10.162,197.943],[26.731,158.552],[-18.148,102.248],[-47.054,60.227],[-10.162,20.859],[26.731,65.13]],"i":[[0,0],[-20.375,0],[0,24.45],[31.566,22.661],[0,24.45],[-20.375,0],[0,-24.45]],"o":[[0,24.45],[20.375,0],[0,-24.45],[-14.85,-10.661],[0,-24.45],[20.375,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":37,"s":[{"c":false,"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"7","layers":[{"ddd":0,"ind":43,"ty":3,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[252.998,251.999],"ix":2},"a":{"a":0,"k":[249.998,249.999],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"ind":44,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":43},{"ddd":0,"refId":"18","w":233,"h":387,"ind":45,"ty":0,"nm":"Group 2","sr":1,"ks":{"p":{"a":0,"k":[-107,-84],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":6,"s":[0],"h":1},{"t":18,"s":[100],"h":1},{"t":31,"s":[100],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":44},{"ddd":0,"ind":46,"ty":4,"nm":"Group 1","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"parent":43,"shapes":[{"ty":"gr","nm":"Group 1","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":true,"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34],[192.71,109.369]],"i":[[0,0],[106.43,0],[0,106.43],[-106.43,0],[0,-106.43]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":6,"s":[{"c":true,"v":[[171.091,109.369],[10.501,302.079],[-150.09,109.369],[10.501,-83.34],[171.091,109.369]],"i":[[0,0],[88.692,0],[0,106.43],[-88.692,0],[0,-106.43]],"o":[[0,106.43],[-88.692,0],[0,-106.43],[88.692,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":18,"s":[{"c":true,"v":[[62.999,109.369],[63,302.079],[63.002,109.369],[63,-83.34],[62.999,109.369]],"i":[[0,0],[-0.001,0],[0,106.43],[0.001,0],[0,-106.43]],"o":[[0,106.43],[0.001,0],[0,-106.43],[-0.001,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":31,"s":[{"c":true,"v":[[171.091,109.369],[10.501,302.079],[-150.09,109.369],[10.501,-83.34],[171.091,109.369]],"i":[[0,0],[88.692,0],[0,106.43],[-88.692,0],[0,-106.43]],"o":[[0,106.43],[-88.692,0],[0,-106.43],[88.692,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":37,"s":[{"c":true,"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34],[192.71,109.369]],"i":[[0,0],[106.43,0],[0,106.43],[-106.43,0],[0,-106.43]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":1,"k":[{"t":0,"s":[50],"h":1},{"t":6,"s":[50],"h":1},{"t":18,"s":[100],"h":1},{"t":31,"s":[100],"h":1}],"ix":2},"o":{"a":0,"k":-90,"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"18","layers":[{"ddd":0,"ind":47,"ty":4,"nm":"Group 2: group style","sr":1,"ks":{"p":{"a":0,"k":[107,84],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":"Group 2","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[0,-20.836],[0,20.836]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":6,"s":[{"c":false,"v":[[10.5,-20.836],[10.5,20.836]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":18,"s":[{"c":false,"v":[[63.001,-20.836],[63.001,20.836]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":31,"s":[{"c":false,"v":[[10.5,-20.836],[10.5,20.836]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":37,"s":[{"c":false,"v":[[0,-20.836],[0,20.836]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":1,"k":[{"t":0,"s":[50],"h":1},{"t":6,"s":[50],"h":1},{"t":18,"s":[100],"h":1},{"t":31,"s":[100],"h":1}],"ix":2},"o":{"a":0,"k":-90,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[0.006,197.911],[0.006,239.582]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":6,"s":[{"c":false,"v":[[10.505,197.911],[10.505,239.582]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":18,"s":[{"c":false,"v":[[63,197.911],[63,239.582]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":31,"s":[{"c":false,"v":[[10.505,197.911],[10.505,239.582]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":37,"s":[{"c":false,"v":[[0.006,197.911],[0.006,239.582]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":1,"k":[{"t":0,"s":[50],"h":1},{"t":6,"s":[50],"h":1},{"t":18,"s":[100],"h":1},{"t":31,"s":[100],"h":1}],"ix":2},"o":{"a":0,"k":-90,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 3","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":6,"s":[{"c":false,"v":[[-26.387,158.525],[10.505,197.915],[47.397,158.525],[2.519,102.221],[-26.387,60.2],[10.505,20.831],[47.397,65.102]],"i":[[0,0],[-20.375,0],[0,24.45],[31.566,22.661],[0,24.45],[-20.375,0],[0,-24.45]],"o":[[0,24.45],[20.375,0],[0,-24.45],[-14.85,-10.661],[0,-24.45],[20.375,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":18,"s":[{"c":false,"v":[[63.001,158.525],[63,197.915],[63,158.525],[63.001,102.221],[63.001,60.2],[63,20.831],[63,65.102]],"i":[[0,0],[0,0],[0,24.45],[0,22.661],[0,24.45],[0,0],[0,-24.45]],"o":[[0,24.45],[0,0],[0,-24.45],[0,-10.661],[0,-24.45],[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":31,"s":[{"c":false,"v":[[-26.387,158.525],[10.505,197.915],[47.397,158.525],[2.519,102.221],[-26.387,60.2],[10.505,20.831],[47.397,65.102]],"i":[[0,0],[-20.375,0],[0,24.45],[31.566,22.661],[0,24.45],[-20.375,0],[0,-24.45]],"o":[[0,24.45],[20.375,0],[0,-24.45],[-14.85,-10.661],[0,-24.45],[20.375,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":37,"s":[{"c":false,"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":1,"k":[{"t":0,"s":[50],"h":1},{"t":6,"s":[50],"h":1},{"t":18,"s":[100],"h":1},{"t":31,"s":[100],"h":1}],"ix":2},"o":{"a":0,"k":-90,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"8","layers":[{"ddd":0,"ind":48,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-125,-379],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":37,"s":[{"c":false,"v":[[1,-195],[0.5,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":42,"s":[{"c":false,"v":[[-9.417,-195],[10.917,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":53,"s":[{"c":false,"v":[[-61.5,-195],[63,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.29],"y":[1]},"o":{"x":[0.167],"y":[0.167]}},{"t":100,"s":[{"c":false,"v":[[1,-195],[0.5,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":1,"k":[{"t":37,"s":[250.75,442.5],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]},"ti":[0,-0.087],"to":[0,0.018]},{"t":42,"s":[250.75,442.667],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]},"ti":[0,0],"to":[0,0.287]},{"t":53,"s":[250.75,443.5],"i":{"x":[0.29],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"ti":[0,0.018],"to":[0,0]},{"t":100,"s":[250.75,442.5],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0.75,-195],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"9","layers":[{"ddd":0,"ind":49,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-20,-284],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":37,"s":[{"c":true,"v":[[167.25,-195],[167.25,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":42,"s":[{"c":false,"v":[[129.125,-195],[149.875,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":53,"s":[{"c":false,"v":[[-61.5,-195],[63,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.29],"y":[1]},"o":{"x":[0.167],"y":[0.167]}},{"t":100,"s":[{"c":true,"v":[[-166.75,-195],[-166.75,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[417.25,347],"ix":2},"a":{"a":0,"k":[167.25,-195],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"10","layers":[{"ddd":0,"ind":50,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-20,-90],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":37,"s":[{"c":true,"v":[[167.25,-195],[167.25,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":42,"s":[{"c":false,"v":[[129.125,-195],[149.875,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":53,"s":[{"c":false,"v":[[-61.5,-195],[63,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.29],"y":[1]},"o":{"x":[0.167],"y":[0.167]}},{"t":100,"s":[{"c":true,"v":[[-166.75,-195],[-166.75,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[417.25,153.5],"ix":2},"a":{"a":0,"k":[167.25,-195],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"11","layers":[{"ddd":0,"ind":51,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[3,-187],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":37,"s":[{"c":true,"v":[[193.25,-195],[193.25,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":42,"s":[{"c":false,"v":[[150.792,-195],[171.542,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":53,"s":[{"c":false,"v":[[-61.5,-195],[63,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.29],"y":[1]},"o":{"x":[0.167],"y":[0.167]}},{"t":100,"s":[{"c":true,"v":[[-192.75,-195],[-192.75,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[443.25,250.5],"ix":2},"a":{"a":0,"k":[193.25,-195],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"12","layers":[{"ddd":0,"ind":52,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-125,2],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":37,"s":[{"c":false,"v":[[1,-195],[0.5,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":42,"s":[{"c":false,"v":[[-9.417,-195],[10.917,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":53,"s":[{"c":false,"v":[[-61.5,-195],[63,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.29],"y":[1]},"o":{"x":[0.167],"y":[0.167]}},{"t":100,"s":[{"c":false,"v":[[1,-195],[0.5,-195]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":1,"k":[{"t":37,"s":[250.75,57.5],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]},"ti":[0,0.087],"to":[0,-0.018]},{"t":42,"s":[250.75,57.333],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]},"ti":[0,0],"to":[0,-0.287]},{"t":53,"s":[250.75,56.5],"i":{"x":[0.29],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"ti":[0,-0.018],"to":[0,0]},{"t":100,"s":[250.75,57.5],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0.75,-195],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"13","layers":[{"ddd":0,"ind":53,"ty":3,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[252.998,251.999],"ix":2},"a":{"a":0,"k":[249.998,249.999],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"ind":54,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":53},{"ddd":0,"refId":"19","w":231,"h":387,"ind":55,"ty":0,"nm":"Group 2","sr":1,"ks":{"p":{"a":0,"k":[-124,-84],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":37,"s":[100],"h":1},{"t":53,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":54},{"ddd":0,"ind":56,"ty":4,"nm":"Group 1","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"parent":53,"shapes":[{"ty":"gr","nm":"Group 1","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":37,"s":[{"c":true,"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34],[192.71,109.369]],"i":[[0,0],[106.43,0],[0,106.43],[-106.43,0],[0,-106.43]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":42,"s":[{"c":true,"v":[[150.425,109.397],[-10.166,302.106],[-170.757,109.397],[-10.166,-83.313],[150.425,109.397]],"i":[[0,0],[88.692,0],[0,106.43],[-88.692,0],[0,-106.43]],"o":[[0,106.43],[-88.692,0],[0,-106.43],[88.692,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":53,"s":[{"c":true,"v":[[-61.001,109.534],[-61,302.243],[-60.998,109.534],[-61,-83.176],[-61.001,109.534]],"i":[[0,0],[-0.001,0],[0,106.43],[0.001,0],[0,-106.43]],"o":[[0,106.43],[0.001,0],[0,-106.43],[-0.001,0],[0,0]]}],"i":{"x":[0.29],"y":[1]},"o":{"x":[0.167],"y":[0.167]}},{"t":100,"s":[{"c":true,"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34],[192.71,109.369]],"i":[[0,0],[106.43,0],[0,106.43],[-106.43,0],[0,-106.43]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":1,"k":[{"t":37,"s":[100],"h":1},{"t":42,"s":[100],"h":1},{"t":53,"s":[50],"h":1}],"ix":2},"o":{"a":0,"k":90,"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"19","layers":[{"ddd":0,"ind":57,"ty":4,"nm":"Group 2: group style","sr":1,"ks":{"p":{"a":0,"k":[124,84],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":"Group 2","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":37,"s":[{"c":false,"v":[[0,-20.836],[0,20.836]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":42,"s":[{"c":false,"v":[[-10.167,-20.808],[-10.167,20.863]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":53,"s":[{"c":false,"v":[[-60.999,-20.671],[-60.999,21]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.29],"y":[1]},"o":{"x":[0.167],"y":[0.167]}},{"t":100,"s":[{"c":false,"v":[[0,-20.836],[0,20.836]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":37,"s":[{"c":false,"v":[[0.006,197.911],[0.006,239.582]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":42,"s":[{"c":false,"v":[[-10.162,197.938],[-10.162,239.61]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":53,"s":[{"c":false,"v":[[-61,198.075],[-61,239.746]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.29],"y":[1]},"o":{"x":[0.167],"y":[0.167]}},{"t":100,"s":[{"c":false,"v":[[0.006,197.911],[0.006,239.582]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 3","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":37,"s":[{"c":false,"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":42,"s":[{"c":false,"v":[[-47.054,158.552],[-10.162,197.943],[26.731,158.552],[-18.148,102.248],[-47.054,60.227],[-10.162,20.859],[26.731,65.13]],"i":[[0,0],[-20.375,0],[0,24.45],[31.566,22.661],[0,24.45],[-20.375,0],[0,-24.45]],"o":[[0,24.45],[20.375,0],[0,-24.45],[-14.85,-10.661],[0,-24.45],[20.375,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":53,"s":[{"c":false,"v":[[-60.999,158.689],[-61,198.08],[-61,158.689],[-60.999,102.385],[-60.999,60.364],[-61,20.995],[-61,65.266]],"i":[[0,0],[0,0],[0,24.45],[0,22.661],[0,24.45],[0,0],[0,-24.45]],"o":[[0,24.45],[0,0],[0,-24.45],[0,-10.661],[0,-24.45],[0,0],[0,0]]}],"i":{"x":[0.29],"y":[1]},"o":{"x":[0.167],"y":[0.167]}},{"t":100,"s":[{"c":false,"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"14","layers":[{"ddd":0,"ind":58,"ty":3,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[252.998,251.999],"ix":2},"a":{"a":0,"k":[249.998,249.999],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"ind":59,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":58},{"ddd":0,"refId":"20","w":233,"h":387,"ind":60,"ty":0,"nm":"Group 2","sr":1,"ks":{"p":{"a":0,"k":[-107,-84],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":37,"s":[0],"h":1},{"t":42,"s":[0],"h":1},{"t":53,"s":[100],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":59},{"ddd":0,"ind":61,"ty":4,"nm":"Group 1","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"parent":58,"shapes":[{"ty":"gr","nm":"Group 1","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":37,"s":[{"c":true,"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34],[192.71,109.369]],"i":[[0,0],[106.43,0],[0,106.43],[-106.43,0],[0,-106.43]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":42,"s":[{"c":true,"v":[[171.091,109.369],[10.501,302.079],[-150.09,109.369],[10.501,-83.34],[171.091,109.369]],"i":[[0,0],[88.692,0],[0,106.43],[-88.692,0],[0,-106.43]],"o":[[0,106.43],[-88.692,0],[0,-106.43],[88.692,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":53,"s":[{"c":true,"v":[[62.999,109.369],[63,302.079],[63.002,109.369],[63,-83.34],[62.999,109.369]],"i":[[0,0],[-0.001,0],[0,106.43],[0.001,0],[0,-106.43]],"o":[[0,106.43],[0.001,0],[0,-106.43],[-0.001,0],[0,0]]}],"i":{"x":[0.29],"y":[1]},"o":{"x":[0.167],"y":[0.167]}},{"t":100,"s":[{"c":true,"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34],[192.71,109.369]],"i":[[0,0],[106.43,0],[0,106.43],[-106.43,0],[0,-106.43]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":1,"k":[{"t":37,"s":[50],"h":1},{"t":42,"s":[50],"h":1},{"t":53,"s":[100],"h":1}],"ix":2},"o":{"a":0,"k":-90,"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"20","layers":[{"ddd":0,"ind":62,"ty":4,"nm":"Group 2: group style","sr":1,"ks":{"p":{"a":0,"k":[107,84],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":"Group 2","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":37,"s":[{"c":false,"v":[[0,-20.836],[0,20.836]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":42,"s":[{"c":false,"v":[[10.5,-20.836],[10.5,20.836]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":53,"s":[{"c":false,"v":[[63.001,-20.836],[63.001,20.836]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.29],"y":[1]},"o":{"x":[0.167],"y":[0.167]}},{"t":100,"s":[{"c":false,"v":[[0,-20.836],[0,20.836]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":1,"k":[{"t":37,"s":[50],"h":1},{"t":42,"s":[50],"h":1},{"t":53,"s":[100],"h":1}],"ix":2},"o":{"a":0,"k":-90,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":37,"s":[{"c":false,"v":[[0.006,197.911],[0.006,239.582]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":42,"s":[{"c":false,"v":[[10.505,197.911],[10.505,239.582]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":53,"s":[{"c":false,"v":[[63,197.911],[63,239.582]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.29],"y":[1]},"o":{"x":[0.167],"y":[0.167]}},{"t":100,"s":[{"c":false,"v":[[0.006,197.911],[0.006,239.582]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":1,"k":[{"t":37,"s":[50],"h":1},{"t":42,"s":[50],"h":1},{"t":53,"s":[100],"h":1}],"ix":2},"o":{"a":0,"k":-90,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 3","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":37,"s":[{"c":false,"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":42,"s":[{"c":false,"v":[[-26.387,158.525],[10.505,197.915],[47.397,158.525],[2.519,102.221],[-26.387,60.2],[10.505,20.831],[47.397,65.102]],"i":[[0,0],[-20.375,0],[0,24.45],[31.566,22.661],[0,24.45],[-20.375,0],[0,-24.45]],"o":[[0,24.45],[20.375,0],[0,-24.45],[-14.85,-10.661],[0,-24.45],[20.375,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":53,"s":[{"c":false,"v":[[63.001,158.525],[63,197.915],[63,158.525],[63.001,102.221],[63.001,60.2],[63,20.831],[63,65.102]],"i":[[0,0],[0,0],[0,24.45],[0,22.661],[0,24.45],[0,0],[0,-24.45]],"o":[[0,24.45],[0,0],[0,-24.45],[0,-10.661],[0,-24.45],[0,0],[0,0]]}],"i":{"x":[0.29],"y":[1]},"o":{"x":[0.167],"y":[0.167]}},{"t":100,"s":[{"c":false,"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":1,"k":[{"t":37,"s":[50],"h":1},{"t":42,"s":[50],"h":1},{"t":53,"s":[100],"h":1}],"ix":2},"o":{"a":0,"k":-90,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"15","layers":[{"ddd":0,"ind":63,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-41,-41],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-0.079,-72.891],[-0.005,-72.888],[0.061,-72.891],[28.621,-44.271],[44.271,-28.621],[59.921,-44.271],[15.645,-102.107],[15.645,-130.209],[-0.005,-145.859],[-15.656,-130.209],[-15.656,-102.519],[-59.921,-49.174],[-22.985,3.135],[-17.618,6.278],[-15.969,7.264],[28.621,49.151],[0.075,72.891],[0,72.887],[-0.074,72.891],[-28.621,49.151],[-44.271,33.501],[-59.921,49.151],[-15.65,102.519],[-15.65,130.209],[0,145.859],[15.651,130.209],[15.651,102.519],[59.921,49.151],[42.73,9.364],[0.097,-19.598],[-1.548,-20.583],[-7.292,-23.947],[-28.621,-49.174],[-0.079,-72.891]],"i":[[0,0],[-0.025,0],[-0.022,0],[0,-15.761],[-8.644,0],[0,8.643],[25.476,6.899],[0,0],[8.644,0],[0,-8.643],[0,0],[0,-26.851],[-16.456,-9.535],[-1.805,-1.08],[0,0],[0,-14.091],[13.187,-0.023],[0.025,0],[0.025,0],[0,17.49],[8.644,0],[0,-8.643],[-26.389,-5.847],[0,0],[-8.644,0],[0,8.643],[0,0],[0,26.864],[11.729,11.188],[19.101,11.424],[0,0],[1.892,1.096],[0,12.245],[-13.186,0.025]],"o":[[0.025,0],[0.022,0],[15.753,0.033],[0,8.643],[8.644,0],[0,-27.629],[0,0],[0,-8.643],[-8.644,0],[0,0],[-26.386,5.846],[0,30.906],[1.768,1.024],[0,0],[37.853,22.64],[0,17.49],[-0.025,0],[-0.025,0],[-13.187,-0.023],[0,-8.643],[-8.644,0],[0,26.864],[0,0],[0,8.643],[8.644,0],[0,0],[26.389,-5.847],[0,-15.958],[-9.731,-9.283],[0,0],[-1.932,-1.155],[-15.848,-9.183],[0,-17.472],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[250.003,250.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","nm":"Group 2","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,177.059],[-177.059,0],[0,-177.059],[177.059,0],[0,177.059]],"i":[[0,0],[0,97.631],[-97.631,0],[0,-97.631],[97.631,0]],"o":[[-97.631,0],[0,-97.631],[97.631,0],[0,97.631],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,-208.359],[-208.359,0],[0,208.359],[208.359,0],[0,-208.359]],"i":[[0,0],[0,-114.89],[-114.89,0],[0,114.89],[114.89,0]],"o":[[-114.89,0],[0,114.89],[114.89,0],[0,-114.89],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[249.998,249.999],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[249.998,249.999],"ix":2},"a":{"a":0,"k":[249.998,249.999],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"16","layers":[{"ddd":0,"ind":64,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-41,-41],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-0.079,-72.891],[-0.005,-72.888],[0.061,-72.891],[28.621,-44.271],[44.271,-28.621],[59.921,-44.271],[15.645,-102.107],[15.645,-130.209],[-0.005,-145.859],[-15.656,-130.209],[-15.656,-102.519],[-59.921,-49.174],[-22.985,3.135],[-17.618,6.278],[-15.969,7.264],[28.621,49.151],[0.075,72.891],[0,72.887],[-0.074,72.891],[-28.621,49.151],[-44.271,33.501],[-59.921,49.151],[-15.65,102.519],[-15.65,130.209],[0,145.859],[15.651,130.209],[15.651,102.519],[59.921,49.151],[42.73,9.364],[0.097,-19.598],[-1.548,-20.583],[-7.292,-23.947],[-28.621,-49.174],[-0.079,-72.891]],"i":[[0,0],[-0.025,0],[-0.022,0],[0,-15.761],[-8.644,0],[0,8.643],[25.476,6.899],[0,0],[8.644,0],[0,-8.643],[0,0],[0,-26.851],[-16.456,-9.535],[-1.805,-1.08],[0,0],[0,-14.091],[13.187,-0.023],[0.025,0],[0.025,0],[0,17.49],[8.644,0],[0,-8.643],[-26.389,-5.847],[0,0],[-8.644,0],[0,8.643],[0,0],[0,26.864],[11.729,11.188],[19.101,11.424],[0,0],[1.892,1.096],[0,12.245],[-13.186,0.025]],"o":[[0.025,0],[0.022,0],[15.753,0.033],[0,8.643],[8.644,0],[0,-27.629],[0,0],[0,-8.643],[-8.644,0],[0,0],[-26.386,5.846],[0,30.906],[1.768,1.024],[0,0],[37.853,22.64],[0,17.49],[-0.025,0],[-0.025,0],[-13.187,-0.023],[0,-8.643],[-8.644,0],[0,26.864],[0,0],[0,8.643],[8.644,0],[0,0],[26.389,-5.847],[0,-15.958],[-9.731,-9.283],[0,0],[-1.932,-1.155],[-15.848,-9.183],[0,-17.472],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[250.003,250.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","nm":"Group 2","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,177.059],[-177.059,0],[0,-177.059],[177.059,0],[0,177.059]],"i":[[0,0],[0,97.631],[-97.631,0],[0,-97.631],[97.631,0]],"o":[[-97.631,0],[0,-97.631],[97.631,0],[0,97.631],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,-208.359],[-208.359,0],[0,208.359],[208.359,0],[0,-208.359]],"i":[[0,0],[0,-114.89],[-114.89,0],[0,114.89],[114.89,0]],"o":[[-114.89,0],[0,114.89],[114.89,0],[0,-114.89],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[249.998,249.999],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[249.998,249.999],"ix":2},"a":{"a":0,"k":[249.998,249.999],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":12345679,"ty":4,"nm":"Group Layer 8","sr":1,"ks":{"p":{"a":0,"k":[365,464.38524590163934,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[51.229508196721305,51.229508196721305,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[220.741,37.184],[225.501,35.896],[228.749,32.36800000000001],[229.981,27.216000000000008],[228.749,22.12],[225.501,18.592],[220.741,17.304],[215.981,18.592],[212.677,22.12],[211.501,27.216000000000008],[212.677,32.36800000000001],[215.981,35.896],[220.741,37.184],[220.741,37.184],[220.741,37.184]],"i":[[0,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.382000000000062,0.8586999999999989],[1.79200000000003,0],[1.418999999999983,-0.8586999999999989],[0.8220000000000027,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.382000000000062,-0.8586999999999989],[0.8220000000000027,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.418999999999983,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[221.357,43.06400000000001],[214.917,41.608],[210.49300000000005,37.408],[211.221,36.232],[211.221,42.392],[205.173,42.392],[205.173,0],[211.501,0],[211.501,18.36800000000001],[210.49300000000005,16.912000000000006],[214.973,12.88],[221.357,11.424000000000007],[229.085,13.49600000000001],[234.51700000000005,19.152],[236.533,27.216000000000008],[234.51700000000005,35.28],[229.141,40.992],[221.357,43.06400000000001],[221.357,43.06400000000001],[221.357,43.06400000000001]],"i":[[0,0],[1.942000000000007,0.9706999999999937],[1.045999999999935,1.829300000000003],[-0.2426666666666506,0.3919999999999959],[0,-2.053333333333327],[2.015999999999963,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.122666666666674],[0.3360000000000127,0.4853333333333296],[-1.865999999999985,0.9707000000000079],[-2.38900000000001,0],[-2.277000000000044,-1.3813000000000102],[-1.30600000000004,-2.389300000000006],[0,-2.986699999999999],[1.343999999999937,-2.389300000000006],[2.27800000000002,-1.4187000000000012],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.351999999999975,0],[-1.903999999999996,-0.9707000000000079],[0.2426666666666506,-0.3919999999999959],[0,2.053333333333327],[-2.015999999999963,0],[0,-14.13066666666667],[2.109333333333325,0],[0,6.122666666666667],[-0.3360000000000127,-0.4853333333333296],[1.120000000000005,-1.717300000000009],[1.867000000000075,-0.9706999999999937],[2.875,0],[2.314999999999941,1.381299999999996],[1.343999999999937,2.389300000000006],[0,2.986699999999999],[-1.30600000000004,2.389300000000006],[-2.27699999999993,1.381299999999996],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[181.87,43.06400000000001],[176.438,42],[172.854,38.976],[171.566,34.384],[172.63,29.960000000000008],[176.046,26.656000000000006],[181.814,24.752],[192.342,23.016000000000005],[192.342,28],[183.046,29.624],[179.35,31.248],[178.174,34.16],[179.462,37.016000000000005],[182.878,38.08],[187.35799999999995,36.96000000000001],[190.38199999999995,33.992],[191.446,29.792],[191.446,22.008],[189.766,18.36800000000001],[185.398,16.912000000000006],[180.974,18.256],[178.23,21.616],[172.966,18.98400000000001],[175.71,15.064000000000007],[180.134,12.376],[185.566,11.424000000000007],[191.894,12.768],[196.206,16.52],[197.774,22.008],[197.774,42.392],[191.726,42.392],[191.726,36.904],[193.014,37.072],[190.27,40.264],[186.518,42.336],[181.87,43.06400000000001],[181.87,43.06400000000001],[181.87,43.06400000000001]],"i":[[0,0],[1.567999999999984,0.7092999999999989],[0.8589999999999236,1.2693000000000012],[0,1.7547],[-0.7089999999999463,1.306699999999992],[-1.530000000000086,0.8960000000000008],[-2.313999999999965,0.3733000000000004],[-3.509333333333302,0.5786666666666633],[0,-1.661333333333332],[3.098666666666645,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8579999999999472,-0.7467000000000041],[-1.381000000000085,0],[-1.268999999999892,0.7466999999999899],[-0.7089999999999463,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829999999999927,0],[1.269999999999982,-0.8960000000000008],[0.5979999999999563,-1.381299999999996],[1.754666666666708,0.8773333333333255],[-1.269000000000005,1.11999999999999],[-1.680000000000064,0.6346999999999952],[-1.903999999999996,0],[-1.828999999999951,-0.8960000000000008],[-1.008000000000038,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999963,0],[0,1.829333333333338],[-0.4293333333333749,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.717999999999961,0],[0,0],[0,0]],"o":[[-2.052999999999997,0],[-1.529999999999973,-0.7467000000000041],[-0.8580000000000609,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999573,-1.306700000000006],[1.530999999999949,-0.8960000000000008],[3.509333333333302,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666645,0.541333333333327],[-1.680000000000064,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.8959999999999582,0.7092999999999989],[1.717999999999961,0],[1.307000000000016,-0.7467000000000041],[0.7100000000000364,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.081999999999994,-0.9707000000000079],[-1.680000000000064,0],[-1.232000000000085,0.8586999999999989],[-1.754666666666708,-0.8773333333333255],[0.5599999999999454,-1.493300000000005],[1.269999999999982,-1.157300000000006],[1.717999999999961,-0.6347000000000094],[2.389999999999986,0],[1.866999999999962,0.8960000000000008],[1.045999999999935,1.568000000000012],[0,6.794666666666672],[-2.015999999999963,0],[0,-1.829333333333338],[0.4293333333333749,0.05599999999999739],[-0.70900000000006,1.2319999999999993],[-1.081999999999994,0.8960000000000008],[-1.380999999999972,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[159.072,42.392],[159.072,0],[165.4,0],[165.4,42.392],[159.072,42.392],[159.072,42.392],[159.072,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-14.13066666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,14.13066666666667],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[138.952,43.06400000000001],[130.888,40.992],[125.456,35.28],[123.496,27.16],[125.456,19.040000000000006],[130.832,13.49600000000001],[138.448,11.424000000000007],[144.552,12.600000000000009],[149.088,15.848],[151.888,20.49600000000001],[152.896,26.096],[152.84,27.608],[152.616,29.064000000000007],[128.48,29.064000000000007],[128.48,24.024],[149.032,24.024],[146.008,26.320000000000007],[145.616,21.448000000000008],[142.816,18.032],[138.448,16.744],[133.968,18.032],[130.944,21.616],[130.104,27.216000000000008],[130.944,32.592],[134.192,36.176],[139.008,37.464],[143.65599999999995,36.232],[146.736,33.040000000000006],[151.888,35.56],[149.088,39.42400000000001],[144.60799999999995,42.11200000000001],[138.952,43.06400000000001],[138.952,43.06400000000001],[138.952,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.307000000000016,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.836999999999989,0],[-1.79200000000003,-0.784000000000006],[-1.231999999999971,-1.381299999999996],[-0.6349999999999909,-1.754700000000014],[0,-1.978700000000003],[0.03699999999992087,-0.5227000000000004],[0.1119999999999663,-0.4480000000000075],[8.04533333333336,0],[0,1.680000000000007],[-6.850666666666712,0],[1.008000000000038,-0.7653333333333308],[0.6349999999999909,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.680000000000064,0],[1.307000000000016,-0.8586999999999989],[0.70900000000006,-1.567999999999998],[-0.1490000000000009,-2.202700000000007],[-0.7469999999999573,-1.530699999999996],[-1.380999999999972,-0.8586999999999989],[-1.79200000000003,0],[-1.268999999999892,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333386,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.755000000000109,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.315000000000055,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999937,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.277000000000044,0],[1.79200000000003,0.7839999999999918],[1.232000000000085,1.344000000000008],[0.6720000000000255,1.7547],[0,0.4852999999999952],[-0.03700000000003456,0.5227000000000004],[-8.04533333333336,0],[0,-1.680000000000007],[6.850666666666712,0],[-1.008000000000038,0.7653333333333308],[0.3729999999999336,-1.829300000000003],[-0.59699999999998,-1.456000000000003],[-1.232000000000085,-0.8586999999999989],[-1.67999999999995,0],[-1.306999999999903,0.8213000000000079],[-0.7089999999999463,1.530699999999996],[-0.1870000000000118,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418999999999983,0.8586999999999989],[1.828999999999951,0],[1.307000000000016,-0.8212999999999937],[1.717333333333386,0.8400000000000034],[-0.59699999999998,1.4187000000000012],[-1.231999999999971,1.11999999999999],[-1.716999999999985,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,7.951999999999998],[111.001,0.6720000000000041],[117.329,0.6720000000000041],[117.329,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,42.392],[111.001,12.096],[117.329,12.096],[117.329,42.392],[111.001,42.392],[111.001,42.392],[111.001,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[101.41,42.72800000000001],[94.01800000000003,40.040000000000006],[91.38599999999997,32.48],[91.38599999999997,17.808000000000007],[86.06600000000003,17.808000000000007],[86.06600000000003,12.096],[86.90599999999995,12.096],[90.21000000000004,10.864],[91.38599999999997,7.504000000000005],[91.38599999999997,5.152000000000001],[97.71400000000006,5.152000000000001],[97.71400000000006,12.096],[104.602,12.096],[104.602,17.808000000000007],[97.71400000000006,17.808000000000007],[97.71400000000006,32.2],[98.21799999999996,34.888000000000005],[99.84199999999998,36.568],[102.754,37.128],[103.76200000000006,37.072],[104.826,36.96000000000001],[104.826,42.392],[103.09,42.616],[101.41,42.72800000000001],[101.41,42.72800000000001],[101.41,42.72800000000001]],"i":[[0,0],[1.754999999999995,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333312,0],[0,1.903999999999996],[-0.2799999999999727,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999935,0],[0,-1.903999999999996],[2.295999999999935,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7469999999999573,-0.4106999999999914],[-1.19500000000005,0],[-0.3730000000000473,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6349999999999909,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333312,0],[0,-1.903999999999996],[0.2799999999999727,0],[1.419000000000096,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999935,0],[0,1.903999999999996],[-2.295999999999935,0],[0,4.797333333333327],[0,1.045299999999997],[0.3360000000000127,0.7092999999999989],[0.7470000000000709,0.3733000000000004],[0.2989999999999782,0],[0.3729999999999336,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[78.71499999999997,42.72800000000001],[71.32299999999998,40.040000000000006],[68.69100000000003,32.48],[68.69100000000003,17.808000000000007],[63.37099999999998,17.808000000000007],[63.37099999999998,12.096],[64.21100000000001,12.096],[67.51499999999999,10.864],[68.69100000000003,7.504000000000005],[68.69100000000003,5.152000000000001],[75.019,5.152000000000001],[75.019,12.096],[81.90700000000004,12.096],[81.90700000000004,17.808000000000007],[75.019,17.808000000000007],[75.019,32.2],[75.52300000000002,34.888000000000005],[77.14699999999999,36.568],[80.05900000000003,37.128],[81.06700000000001,37.072],[82.13099999999997,36.96000000000001],[82.13099999999997,42.392],[80.39499999999998,42.616],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001]],"i":[[0,0],[1.754000000000019,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7470000000000141,-0.4106999999999914],[-1.19500000000005,0],[-0.3740000000000236,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6340000000000146,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173999999999978,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418000000000006,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7460000000000377,0.3733000000000004],[0.297999999999945,0],[0.3730000000000473,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,37.184],[48.94799999999998,35.896],[52.19600000000003,32.36800000000001],[53.428,27.216000000000008],[52.19600000000003,22.12],[48.94799999999998,18.592],[44.18799999999999,17.304],[39.428,18.592],[36.12400000000002,22.12],[34.94799999999998,27.216000000000008],[36.12400000000002,32.36800000000001],[39.428,35.896],[44.18799999999999,37.184],[44.18799999999999,37.184],[44.18799999999999,37.184]],"i":[[0,0],[-1.381999999999948,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.381000000000029,0.8586999999999989],[1.79200000000003,0],[1.418000000000006,-0.8586999999999989],[0.8209999999999695,-1.493300000000005],[0,-1.904000000000011],[-0.7840000000000487,-1.5307000000000102],[-1.382000000000005,-0.8586999999999989],[-1.754999999999995,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.381000000000029,-0.8586999999999989],[0.8209999999999695,-1.5307000000000102],[0,-1.904000000000011],[-0.7840000000000487,-1.493300000000005],[-1.381999999999948,-0.8586999999999989],[-1.754999999999995,0],[-1.382000000000005,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.418000000000006,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,43.06400000000001],[36.18000000000001,40.992],[30.46800000000002,35.336],[28.33999999999997,27.216000000000008],[30.46800000000002,19.096],[36.18000000000001,13.49600000000001],[44.18799999999999,11.424000000000007],[52.19600000000003,13.49600000000001],[57.85199999999998,19.096],[59.98000000000002,27.216000000000008],[57.85199999999998,35.392],[52.139999999999986,41.048],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001]],"i":[[0,0],[2.425999999999988,1.381299999999996],[1.418000000000006,2.389300000000006],[0,3.024000000000001],[-1.41900000000004,2.352000000000004],[-2.389999999999986,1.343999999999994],[-2.949999999999989,0],[-2.352000000000032,-1.3813000000000102],[-1.381999999999948,-2.389300000000006],[0,-3.061300000000003],[1.418000000000006,-2.389299999999992],[2.38900000000001,-1.381299999999996],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.911999999999978,0],[-2.389999999999986,-1.381299999999996],[-1.41900000000004,-2.389299999999992],[0,-3.061300000000003],[1.418000000000006,-2.389300000000006],[2.38900000000001,-1.3813000000000102],[2.98599999999999,0],[2.388999999999953,1.343999999999994],[1.418000000000006,2.352000000000004],[0,3.061299999999989],[-1.418999999999983,2.389300000000006],[-2.389999999999986,1.343999999999994],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.608000000000004,0.6720000000000041],[6.608000000000004,36.512],[24.639999999999986,36.512],[24.639999999999986,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.202666666666687,0],[0,-11.94666666666666],[-6.01066666666668,0],[0,-1.959999999999994],[8.21333333333331,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.202666666666687,0],[0,11.94666666666667],[6.01066666666668,0],[0,1.959999999999994],[-8.21333333333331,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[98.08047485351562,-21.67217254638672],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[246.681,42.392],[246.681,0],[253.009,0],[253.009,18.032],[252.001,17.248],[255.585,12.936000000000007],[261.297,11.424000000000007],[267.23299999999995,12.88],[271.265,16.912000000000006],[272.721,22.792],[272.721,42.392],[266.449,42.392],[266.449,24.528000000000006],[265.553,20.664],[263.201,18.2],[259.729,17.304],[256.25699999999995,18.2],[253.849,20.664],[253.009,24.528000000000006],[253.009,42.392],[246.681,42.392],[246.681,42.392],[246.681,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.010666666666665],[0.3360000000000127,0.2613333333333259],[-1.643000000000029,0.9706999999999937],[-2.166000000000054,0],[-1.717999999999961,-0.9706999999999937],[-0.9710000000000036,-1.717300000000009],[0,-2.202699999999993],[0,-6.533333333333331],[2.090666666666721,0],[0,5.954666666666668],[0.59699999999998,1.045299999999997],[1.007999999999925,0.5600000000000023],[1.305999999999926,0],[1.045000000000073,-0.5973000000000042],[0.59699999999998,-1.082700000000003],[0,-1.493300000000005],[0,-5.954666666666668],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,6.010666666666665],[-0.3360000000000127,-0.2613333333333259],[0.7459999999999809,-1.903999999999996],[1.641999999999967,-1.0080000000000098],[2.240000000000009,0],[1.717000000000098,0.9707000000000079],[0.9700000000000273,1.717299999999994],[0,6.533333333333331],[-2.090666666666721,0],[0,-5.954666666666668],[0,-1.5307000000000102],[-0.5599999999999454,-1.082700000000003],[-1.008000000000038,-0.5973000000000042],[-1.270000000000095,0],[-1.007999999999953,0.5600000000000023],[-0.5600000000000023,1.082700000000003],[0,5.954666666666668],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[237.089,42.72800000000001],[229.697,40.040000000000006],[227.065,32.48],[227.065,17.808000000000007],[221.745,17.808000000000007],[221.745,12.096],[222.585,12.096],[225.889,10.864],[227.065,7.504000000000005],[227.065,5.152000000000001],[233.393,5.152000000000001],[233.393,12.096],[240.281,12.096],[240.281,17.808000000000007],[233.393,17.808000000000007],[233.393,32.2],[233.897,34.888000000000005],[235.521,36.568],[238.433,37.128],[239.441,37.072],[240.505,36.96000000000001],[240.505,42.392],[238.769,42.616],[237.089,42.72800000000001],[237.089,42.72800000000001],[237.089,42.72800000000001]],"i":[[0,0],[1.755000000000052,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7459999999999809,-0.4106999999999914],[-1.194000000000017,0],[-0.3729999999999905,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6350000000000477,-0.07469999999999288],[0.48599999999999,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.753999999999962,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418999999999983,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7470000000000141,0.3733000000000004],[0.2989999999999782,0],[0.3740000000000236,-0.03730000000000189],[0,1.810666666666663],[-0.5220000000000482,0.0747000000000071],[-0.6339999999999577,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,7.951999999999998],[210.259,0.6720000000000041],[216.587,0.6720000000000041],[216.587,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,42.392],[210.259,12.096],[216.587,12.096],[216.587,42.392],[210.259,42.392],[210.259,42.392],[210.259,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[169.688,42.392],[159.272,12.096],[165.992,12.096],[173.944,36.232],[171.592,36.232],[179.712,12.096],[185.48,12.096],[193.544,36.232],[191.192,36.232],[199.2,12.096],[205.92,12.096],[195.448,42.392],[189.736,42.392],[181.56,17.696],[183.632,17.696],[175.456,42.392],[169.688,42.392],[169.688,42.392],[169.688,42.392]],"i":[[0,0],[3.47199999999998,10.09866666666667],[-2.240000000000009,0],[-2.650666666666666,-8.045333333333332],[0.7839999999999918,0],[-2.706666666666649,8.045333333333332],[-1.922666666666657,0],[-2.687999999999988,-8.045333333333332],[0.7839999999999918,0],[-2.669333333333327,8.045333333333332],[-2.240000000000009,0],[3.490666666666641,-10.09866666666667],[1.903999999999996,0],[2.725333333333367,8.232],[-0.6906666666666865,0],[2.72533333333331,-8.232],[1.922666666666657,0],[0,0],[0,0]],"o":[[-3.47199999999998,-10.09866666666666],[2.240000000000009,0],[2.650666666666666,8.045333333333332],[-0.7839999999999918,0],[2.706666666666649,-8.045333333333332],[1.922666666666657,0],[2.687999999999988,8.045333333333332],[-0.7839999999999918,0],[2.669333333333327,-8.045333333333332],[2.240000000000009,0],[-3.490666666666641,10.09866666666666],[-1.903999999999996,0],[-2.725333333333367,-8.232],[0.6906666666666865,0],[-2.72533333333331,8.232],[-1.922666666666657,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[155.86146545410156,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[132.444,43.06400000000001],[124.38,40.992],[118.948,35.28],[116.988,27.16],[118.948,19.040000000000006],[124.324,13.49600000000001],[131.94,11.424000000000007],[138.044,12.600000000000009],[142.58,15.848],[145.38,20.49600000000001],[146.388,26.096],[146.332,27.608],[146.108,29.064000000000007],[121.972,29.064000000000007],[121.972,24.024],[142.524,24.024],[139.5,26.320000000000007],[139.108,21.448000000000008],[136.308,18.032],[131.94,16.744],[127.46,18.032],[124.436,21.616],[123.596,27.216000000000008],[124.436,32.592],[127.684,36.176],[132.5,37.464],[137.148,36.232],[140.228,33.040000000000006],[145.38,35.56],[142.58,39.42400000000001],[138.1,42.11200000000001],[132.444,43.06400000000001],[132.444,43.06400000000001],[132.444,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.305999999999983,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.838000000000022,0],[-1.79200000000003,-0.784000000000006],[-1.232000000000028,-1.381299999999996],[-0.6350000000000477,-1.754700000000014],[0,-1.978700000000003],[0.03699999999997772,-0.5227000000000004],[0.1120000000000232,-0.4480000000000075],[8.045333333333303,0],[0,1.680000000000007],[-6.850666666666655,0],[1.007999999999981,-0.7653333333333308],[0.6340000000000146,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.67999999999995,0],[1.305999999999983,-0.8586999999999989],[0.7090000000000032,-1.567999999999998],[-0.1499999999999773,-2.202700000000007],[-0.7470000000000141,-1.530699999999996],[-1.382000000000005,-0.8586999999999989],[-1.791999999999973,0],[-1.269999999999982,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333329,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.754000000000019,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.314999999999998,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999994,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.276999999999987,0],[1.791999999999973,0.7839999999999918],[1.231999999999971,1.344000000000008],[0.6719999999999686,1.7547],[0,0.4852999999999952],[-0.03800000000001091,0.5227000000000004],[-8.045333333333303,0],[0,-1.680000000000007],[6.850666666666655,0],[-1.007999999999981,0.7653333333333308],[0.3730000000000473,-1.829300000000003],[-0.5979999999999563,-1.456000000000003],[-1.232000000000028,-0.8586999999999989],[-1.680000000000007,0],[-1.307000000000016,0.8213000000000079],[-0.7100000000000364,1.530699999999996],[-0.186999999999955,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418000000000006,0.8586999999999989],[1.829000000000008,0],[1.305999999999983,-0.8212999999999937],[1.717333333333329,0.8400000000000034],[-0.5980000000000132,1.4187000000000012],[-1.232000000000028,1.11999999999999],[-1.718000000000018,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[95.32,37.184],[100.024,35.896],[103.328,32.36800000000001],[104.56,27.216000000000008],[103.328,22.12],[100.024,18.592],[95.32,17.304],[90.56,18.592],[87.256,22.12],[86.08000000000001,27.216000000000008],[87.256,32.36800000000001],[90.50399999999999,35.896],[95.32,37.184],[95.32,37.184],[95.32,37.184]],"i":[[0,0],[-1.381,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.820999999999998,1.493299999999991],[1.419000000000011,0.8586999999999989],[1.754999999999995,0],[1.418999999999983,-0.8586999999999989],[0.7839999999999918,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.381,-0.8586999999999989],[-1.792000000000002,0],[0,0],[0,0]],"o":[[1.754999999999995,0],[1.419000000000011,-0.8586999999999989],[0.820999999999998,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.381,-0.8586999999999989],[-1.754999999999995,0],[-1.419000000000011,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.7839999999999918,1.493299999999991],[1.419000000000011,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[94.70400000000001,43.06400000000001],[86.864,40.992],[81.43199999999999,35.28],[79.47200000000001,27.216000000000008],[81.488,19.152],[86.91999999999999,13.49600000000001],[94.648,11.424000000000007],[101.088,12.88],[105.512,16.912000000000006],[104.56,18.36800000000001],[104.56,0],[110.832,0],[110.832,42.392],[104.84,42.392],[104.84,36.232],[105.568,37.408],[101.088,41.608],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001]],"i":[[0,0],[2.314999999999998,1.381299999999996],[1.344000000000023,2.389300000000006],[0,2.986699999999999],[-1.343999999999994,2.389300000000006],[-2.276999999999987,1.381299999999996],[-2.875,0],[-1.8669999999999902,-0.9706999999999937],[-1.082999999999998,-1.717300000000009],[0.3173333333333233,-0.4853333333333296],[0,6.122666666666674],[-2.090666666666664,0],[0,-14.13066666666667],[1.99733333333333,0],[0,2.053333333333327],[-0.242666666666679,-0.3919999999999959],[1.941000000000003,-0.9707000000000079],[2.314999999999998,0],[0,0],[0,0]],"o":[[-2.912000000000006,0],[-2.277000000000015,-1.4187000000000012],[-1.306999999999988,-2.389300000000006],[0,-2.986699999999999],[1.343999999999994,-2.389300000000006],[2.277000000000015,-1.3813000000000102],[2.426999999999992,0],[1.867000000000019,0.9707000000000079],[-0.3173333333333233,0.4853333333333296],[0,-6.122666666666674],[2.090666666666664,0],[0,14.13066666666667],[-1.99733333333333,0],[0,-2.053333333333327],[0.242666666666679,0.3919999999999959],[-1.045000000000016,1.829300000000003],[-1.941000000000003,0.9706999999999937],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[57.40100000000001,43.06400000000001],[51.968999999999994,42],[48.38499999999999,38.976],[47.09700000000001,34.384],[48.161,29.960000000000008],[51.577,26.656000000000006],[57.345,24.752],[67.87299999999999,23.016000000000005],[67.87299999999999,28],[58.577,29.624],[54.881,31.248],[53.70500000000001,34.16],[54.992999999999995,37.016000000000005],[58.40899999999999,38.08],[62.88900000000001,36.96000000000001],[65.91300000000001,33.992],[66.977,29.792],[66.977,22.008],[65.297,18.36800000000001],[60.929,16.912000000000006],[56.505,18.256],[53.761,21.616],[48.496999999999986,18.98400000000001],[51.240999999999985,15.064000000000007],[55.66499999999999,12.376],[61.09700000000001,11.424000000000007],[67.42500000000001,12.768],[71.737,16.52],[73.305,22.008],[73.305,42.392],[67.257,42.392],[67.257,36.904],[68.54499999999999,37.072],[65.80099999999999,40.264],[62.04900000000001,42.336],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001]],"i":[[0,0],[1.568000000000012,0.7092999999999989],[0.8590000000000089,1.2693000000000012],[0,1.7547],[-0.7090000000000032,1.306699999999992],[-1.531000000000006,0.8960000000000008],[-2.314999999999998,0.3733000000000004],[-3.509333333333331,0.5786666666666633],[0,-1.661333333333332],[3.098666666666674,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8590000000000089,-0.7467000000000041],[-1.381,0],[-1.269000000000005,0.7466999999999899],[-0.7090000000000032,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829000000000008,0],[1.269000000000005,-0.8960000000000008],[0.5970000000000084,-1.381299999999996],[1.754666666666679,0.8773333333333255],[-1.268999999999977,1.11999999999999],[-1.680000000000007,0.6346999999999952],[-1.903999999999996,0],[-1.829000000000008,-0.8960000000000008],[-1.0080000000000098,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999991,0],[0,1.829333333333338],[-0.429333333333318,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.716999999999985,0],[0,0],[0,0]],"o":[[-2.053000000000026,0],[-1.531000000000006,-0.7467000000000041],[-0.8589999999999804,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999857,-1.306700000000006],[1.531000000000006,-0.8960000000000008],[3.509333333333331,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666674,0.541333333333327],[-1.680000000000007,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.896000000000015,0.7092999999999989],[1.717000000000013,0],[1.306999999999988,-0.7467000000000041],[0.7089999999999748,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.082999999999998,-0.9707000000000079],[-1.680000000000007,0],[-1.2319999999999993,0.8586999999999989],[-1.754666666666679,-0.8773333333333255],[0.5600000000000023,-1.493300000000005],[1.269000000000005,-1.157300000000006],[1.717000000000013,-0.6347000000000094],[2.388999999999982,0],[1.86699999999999,0.8960000000000008],[1.045000000000016,1.568000000000012],[0,6.794666666666672],[-2.015999999999991,0],[0,-1.829333333333338],[0.429333333333318,0.05599999999999739],[-0.7089999999999748,1.2319999999999993],[-1.082999999999998,0.8960000000000008],[-1.381,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.159999999999997,0.6720000000000041],[21.84,22.400000000000006],[18.75999999999999,22.400000000000006],[34.16,0.6720000000000041],[40.31999999999999,0.6720000000000041],[40.31999999999999,42.392],[33.768,42.392],[33.768,8.456000000000003],[36.232,9.128],[20.49600000000001,30.632000000000005],[19.824000000000012,30.632000000000005],[4.424000000000007,9.128],[6.608000000000004,8.456000000000003],[6.608000000000004,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.053333333333342,0],[-5.226666666666659,-7.242666666666665],[1.026666666666671,0],[-5.133333333333326,7.242666666666672],[-2.053333333333342,0],[0,-13.90666666666667],[2.183999999999997,0],[0,11.312],[-0.8213333333333424,-0.2240000000000038],[5.245333333333321,-7.168000000000006],[0.2239999999999895,0],[5.133333333333326,7.168000000000006],[-0.7280000000000086,0.2240000000000038],[0,-11.312],[2.202666666666659,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.053333333333342,0],[5.226666666666659,7.242666666666672],[-1.026666666666671,0],[5.133333333333326,-7.242666666666665],[2.053333333333342,0],[0,13.90666666666666],[-2.183999999999997,0],[0,-11.312],[0.8213333333333424,0.2240000000000038],[-5.245333333333321,7.168000000000006],[-0.2239999999999895,0],[-5.133333333333326,-7.168000000000006],[0.7280000000000086,-0.2240000000000038],[0,11.312],[-2.202666666666659,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[2.91259765625,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[702.6863719370097,144],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":72,"ix":2}},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[56.54167175292969,-0.000022762338630855083],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":80,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[122.0000003294881,25.00000012138912],"ix":2},"a":{"a":0,"k":[56.54167175292969,-0.00002288818359375],"ix":2},"s":{"a":0,"k":[34.403572049765366,34.403572049765366],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"ind":65,"ty":3,"nm":"Color & Stroke Change","sr":1,"ks":{"p":{"a":0,"k":[250,250],"ix":2},"a":{"a":0,"k":[50,50],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"0","w":500,"h":500,"ind":66,"ty":0,"nm":"hover-coin","sr":1,"ks":{"p":{"a":0,"k":[50,50],"ix":2},"a":{"a":0,"k":[250,250],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":65}],"markers":[]} \ No newline at end of file diff --git a/frontend/public/lotties/system-outline-103-coin-cash-monetization.json b/frontend/public/lotties/system-outline-103-coin-cash-monetization.json deleted file mode 100644 index 2167a770d..000000000 --- a/frontend/public/lotties/system-outline-103-coin-cash-monetization.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.8.1","fr":60,"ip":0,"op":61,"w":500,"h":500,"nm":"103-coin-cash-monetization-outline","ddd":0,"assets":[{"id":"comp_0","nm":"in-coin","fr":60,"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL ","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.301],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[-270]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":22,"s":[-152]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":40,"s":[-189]},{"t":59,"s":[-180]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.301,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[250,529,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":15,"s":[250,212,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":33,"s":[250,274,0],"to":[0,0,0],"ti":[0,0,0]},{"t":52,"s":[250,250,0]}],"ix":2,"l":2},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-180,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[50.75,-143.5,0],"to":[0,0,0],"ti":[0,-0.287,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13.332,"s":[50.75,-142.667,0],"to":[0,0.087,0],"ti":[0,-0.018,0]},{"t":20,"s":[50.75,-142.5,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0.75,-195,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.5,-195],[63,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13.332,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-9.417,-195],[10.917,-195]],"c":false}]},{"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,-195],[0.5,-195]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":20,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-180,"ix":10},"p":{"a":0,"k":[50.75,-47,0],"ix":2,"l":2},"a":{"a":0,"k":[0.75,-195,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.5,-195],[63,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13.332,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-149.208,-195],[-128.458,-195]],"c":false}]},{"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-166.75,-195],[-166.75,-195]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":20,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-180,"ix":10},"p":{"a":0,"k":[50.75,146.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0.75,-195,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.5,-195],[63,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13.332,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-149.208,-195],[-128.458,-195]],"c":false}]},{"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-166.75,-195],[-166.75,-195]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":20,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-180,"ix":10},"p":{"a":0,"k":[50.75,49.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0.75,-195,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.5,-195],[63,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13.332,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-170.875,-195],[-150.125,-195]],"c":false}]},{"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-192.75,-195],[-192.75,-195]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":20,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-180,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[50.75,243.5,0],"to":[0,0,0],"ti":[0,0.287,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13.332,"s":[50.75,242.667,0],"to":[0,-0.087,0],"ti":[0,0.018,0]},{"t":20,"s":[50.75,242.5,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0.75,-195,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.5,-195],[63,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13.332,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-9.417,-195],[10.917,-195]],"c":false}]},{"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,-195],[0.5,-195]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":20,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-180,"ix":10},"p":{"a":0,"k":[49.998,50.001,0],"ix":2,"l":2},"a":{"a":0,"k":[249.998,249.999,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-60.999,-20.671],[-60.999,21]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13.332,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-10.167,-20.808],[-10.167,20.863]],"c":false}]},{"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-20.836],[0,20.836]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61,198.075],[-61,239.746]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13.332,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-10.162,197.938],[-10.162,239.61]],"c":false}]},{"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.006,197.911],[0.006,239.582]],"c":false}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0],[0,24.45],[0,22.661],[0,24.45],[0,0],[0,-24.45]],"o":[[0,24.45],[0,0],[0,-24.45],[0,-10.661],[0,-24.45],[0,0],[0,0]],"v":[[-60.999,158.689],[-61,198.08],[-61,158.689],[-60.999,102.385],[-60.999,60.364],[-61,20.995],[-61,65.266]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13.332,"s":[{"i":[[0,0],[-20.375,0],[0,24.45],[31.566,22.661],[0,24.45],[-20.375,0],[0,-24.45]],"o":[[0,24.45],[20.375,0],[0,-24.45],[-14.85,-10.661],[0,-24.45],[20.375,0],[0,0]],"v":[[-47.054,158.552],[-10.162,197.943],[26.731,158.552],[-18.148,102.248],[-47.054,60.227],[-10.162,20.859],[26.731,65.13]],"c":false}]},{"t":20,"s":[{"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]],"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"c":false}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":0,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,-106.43],[-0.001,0],[0,106.43],[0.001,0]],"o":[[0,106.43],[0.001,0],[0,-106.43],[-0.001,0]],"v":[[-61.001,109.534],[-61,302.243],[-60.998,109.534],[-61,-83.176]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13.332,"s":[{"i":[[0,-106.43],[88.692,0],[0,106.43],[-88.692,0]],"o":[[0,106.43],[-88.692,0],[0,-106.43],[88.692,0]],"v":[[150.425,109.397],[-10.166,302.106],[-170.757,109.397],[-10.166,-83.313]],"c":true}]},{"t":20,"s":[{"i":[[0,-106.43],[106.43,0],[0,106.43],[-106.43,0]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0]],"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34]],"c":true}]}],"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"t":0,"s":[50],"h":1},{"t":13.33203125,"s":[50],"h":1}],"ix":2},"o":{"a":0,"k":90,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":20,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-180,"ix":10},"p":{"a":0,"k":[49.998,50.001,0],"ix":2,"l":2},"a":{"a":0,"k":[249.998,249.999,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[63.001,-20.836],[63.001,20.836]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13.332,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[10.5,-20.836],[10.5,20.836]],"c":false}]},{"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-20.836],[0,20.836]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[63,197.911],[63,239.582]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13.332,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[10.505,197.911],[10.505,239.582]],"c":false}]},{"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.006,197.911],[0.006,239.582]],"c":false}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0],[0,24.45],[0,22.661],[0,24.45],[0,0],[0,-24.45]],"o":[[0,24.45],[0,0],[0,-24.45],[0,-10.661],[0,-24.45],[0,0],[0,0]],"v":[[63.001,158.525],[63,197.915],[63,158.525],[63.001,102.221],[63.001,60.2],[63,20.831],[63,65.102]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13.332,"s":[{"i":[[0,0],[-20.375,0],[0,24.45],[31.566,22.661],[0,24.45],[-20.375,0],[0,-24.45]],"o":[[0,24.45],[20.375,0],[0,-24.45],[-14.85,-10.661],[0,-24.45],[20.375,0],[0,0]],"v":[[-26.387,158.525],[10.505,197.915],[47.397,158.525],[2.519,102.221],[-26.387,60.2],[10.505,20.831],[47.397,65.102]],"c":false}]},{"t":20,"s":[{"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]],"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"c":false}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"t":0,"s":[100],"h":1},{"t":13.33203125,"s":[100],"h":1}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,-106.43],[-0.001,0],[0,106.43],[0.001,0]],"o":[[0,106.43],[0.001,0],[0,-106.43],[-0.001,0]],"v":[[62.999,109.369],[63,302.079],[63.002,109.369],[63,-83.34]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13.332,"s":[{"i":[[0,-106.43],[88.692,0],[0,106.43],[-88.692,0]],"o":[[0,106.43],[-88.692,0],[0,-106.43],[88.692,0]],"v":[[171.091,109.369],[10.501,302.079],[-150.09,109.369],[10.501,-83.34]],"c":true}]},{"t":20,"s":[{"i":[[0,-106.43],[106.43,0],[0,106.43],[-106.43,0]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0]],"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34]],"c":true}]}],"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"t":0,"s":[100],"h":1},{"t":13.33203125,"s":[100],"h":1}],"ix":2},"o":{"a":0,"k":-90,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":1,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":20,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-180,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[50.75,-142.5,0],"to":[0,-0.018,0],"ti":[0,0.087,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23.5,"s":[50.75,-142.667,0],"to":[0,-0.287,0],"ti":[0,0,0]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[50.75,-143.5,0],"to":[0,0,0],"ti":[0,-0.018,0]},{"t":60,"s":[50.75,-142.5,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0.75,-195,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,-195],[0.5,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23.5,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-9.417,-195],[10.917,-195]],"c":false}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.5,-195],[63,-195]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,-195],[0.5,-195]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":20,"op":60,"st":1,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-180,"ix":10},"p":{"a":0,"k":[50.75,-47,0],"ix":2,"l":2},"a":{"a":0,"k":[0.75,-195,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[167.25,-195],[167.25,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23.5,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[129.125,-195],[149.875,-195]],"c":false}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.5,-195],[63,-195]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-166.75,-195],[-166.75,-195]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":20,"op":60,"st":1,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-180,"ix":10},"p":{"a":0,"k":[50.75,146.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0.75,-195,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[167.25,-195],[167.25,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23.5,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[129.125,-195],[149.875,-195]],"c":false}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.5,-195],[63,-195]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-166.75,-195],[-166.75,-195]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":20,"op":60,"st":1,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-180,"ix":10},"p":{"a":0,"k":[50.75,49.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0.75,-195,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[193.25,-195],[193.25,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23.5,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[150.792,-195],[171.542,-195]],"c":false}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.5,-195],[63,-195]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-192.75,-195],[-192.75,-195]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":20,"op":60,"st":1,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-180,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[50.75,242.5,0],"to":[0,0.018,0],"ti":[0,-0.087,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23.5,"s":[50.75,242.667,0],"to":[0,0.287,0],"ti":[0,0,0]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[50.75,243.5,0],"to":[0,0,0],"ti":[0,0.018,0]},{"t":60,"s":[50.75,242.5,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0.75,-195,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,-195],[0.5,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23.5,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-9.417,-195],[10.917,-195]],"c":false}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.5,-195],[63,-195]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,-195],[0.5,-195]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":20,"op":60,"st":1,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-180,"ix":10},"p":{"a":0,"k":[49.998,50.001,0],"ix":2,"l":2},"a":{"a":0,"k":[249.998,249.999,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-20.836],[0,20.836]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23.5,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-10.167,-20.808],[-10.167,20.863]],"c":false}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-60.999,-20.671],[-60.999,21]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-20.836],[0,20.836]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.006,197.911],[0.006,239.582]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23.5,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-10.162,197.938],[-10.162,239.61]],"c":false}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61,198.075],[-61,239.746]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.006,197.911],[0.006,239.582]],"c":false}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]],"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23.5,"s":[{"i":[[0,0],[-20.375,0],[0,24.45],[31.566,22.661],[0,24.45],[-20.375,0],[0,-24.45]],"o":[[0,24.45],[20.375,0],[0,-24.45],[-14.85,-10.661],[0,-24.45],[20.375,0],[0,0]],"v":[[-47.054,158.552],[-10.162,197.943],[26.731,158.552],[-18.148,102.248],[-47.054,60.227],[-10.162,20.859],[26.731,65.13]],"c":false}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0],[0,24.45],[0,22.661],[0,24.45],[0,0],[0,-24.45]],"o":[[0,24.45],[0,0],[0,-24.45],[0,-10.661],[0,-24.45],[0,0],[0,0]],"v":[[-60.999,158.689],[-61,198.08],[-61,158.689],[-60.999,102.385],[-60.999,60.364],[-61,20.995],[-61,65.266]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]],"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"c":false}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"t":20,"s":[100],"h":1},{"t":30,"s":[0],"h":1}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,-106.43],[106.43,0],[0,106.43],[-106.43,0]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0]],"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23.5,"s":[{"i":[[0,-106.43],[88.692,0],[0,106.43],[-88.692,0]],"o":[[0,106.43],[-88.692,0],[0,-106.43],[88.692,0]],"v":[[150.425,109.397],[-10.166,302.106],[-170.757,109.397],[-10.166,-83.313]],"c":true}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,-106.43],[-0.001,0],[0,106.43],[0.001,0]],"o":[[0,106.43],[0.001,0],[0,-106.43],[-0.001,0]],"v":[[-61.001,109.534],[-61,302.243],[-60.998,109.534],[-61,-83.176]],"c":true}]},{"t":60,"s":[{"i":[[0,-106.43],[106.43,0],[0,106.43],[-106.43,0]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0]],"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34]],"c":true}]}],"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"t":20,"s":[100],"h":1},{"t":23.5,"s":[100],"h":1},{"t":30,"s":[50],"h":1}],"ix":2},"o":{"a":0,"k":90,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":20,"op":60,"st":1,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-180,"ix":10},"p":{"a":0,"k":[49.998,50.001,0],"ix":2,"l":2},"a":{"a":0,"k":[249.998,249.999,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-20.836],[0,20.836]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23.5,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[10.5,-20.836],[10.5,20.836]],"c":false}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[63.001,-20.836],[63.001,20.836]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-20.836],[0,20.836]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.006,197.911],[0.006,239.582]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23.5,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[10.505,197.911],[10.505,239.582]],"c":false}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[63,197.911],[63,239.582]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.006,197.911],[0.006,239.582]],"c":false}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]],"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23.5,"s":[{"i":[[0,0],[-20.375,0],[0,24.45],[31.566,22.661],[0,24.45],[-20.375,0],[0,-24.45]],"o":[[0,24.45],[20.375,0],[0,-24.45],[-14.85,-10.661],[0,-24.45],[20.375,0],[0,0]],"v":[[-26.387,158.525],[10.505,197.915],[47.397,158.525],[2.519,102.221],[-26.387,60.2],[10.505,20.831],[47.397,65.102]],"c":false}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0],[0,24.45],[0,22.661],[0,24.45],[0,0],[0,-24.45]],"o":[[0,24.45],[0,0],[0,-24.45],[0,-10.661],[0,-24.45],[0,0],[0,0]],"v":[[63.001,158.525],[63,197.915],[63,158.525],[63.001,102.221],[63.001,60.2],[63,20.831],[63,65.102]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]],"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"c":false}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"t":20,"s":[0],"h":1},{"t":23.5,"s":[0],"h":1},{"t":30,"s":[100],"h":1}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,-106.43],[106.43,0],[0,106.43],[-106.43,0]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0]],"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23.5,"s":[{"i":[[0,-106.43],[88.692,0],[0,106.43],[-88.692,0]],"o":[[0,106.43],[-88.692,0],[0,-106.43],[88.692,0]],"v":[[171.091,109.369],[10.501,302.079],[-150.09,109.369],[10.501,-83.34]],"c":true}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,-106.43],[-0.001,0],[0,106.43],[0.001,0]],"o":[[0,106.43],[0.001,0],[0,-106.43],[-0.001,0]],"v":[[62.999,109.369],[63,302.079],[63.002,109.369],[63,-83.34]],"c":true}]},{"t":60,"s":[{"i":[[0,-106.43],[106.43,0],[0,106.43],[-106.43,0]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0]],"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34]],"c":true}]}],"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"t":20,"s":[50],"h":1},{"t":23.5,"s":[50],"h":1},{"t":30,"s":[100],"h":1}],"ix":2},"o":{"a":0,"k":-90,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":1,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":20,"op":60,"st":1,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.998,249.999,0],"ix":2,"l":2},"a":{"a":0,"k":[249.998,249.999,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-13.186,0.025],[-0.025,0],[-0.022,0],[0,-15.761],[-8.644,0],[0,8.643],[25.476,6.899],[0,0],[8.644,0],[0,-8.643],[0,0],[0,-26.851],[-16.456,-9.535],[-1.805,-1.08],[0,0],[0,-14.091],[13.187,-0.023],[0.025,0],[0.025,0],[0,17.49],[8.644,0],[0,-8.643],[-26.389,-5.847],[0,0],[-8.644,0],[0,8.643],[0,0],[0,26.864],[11.729,11.188],[19.101,11.424],[0,0],[1.892,1.096],[0,12.245]],"o":[[0.025,0],[0.022,0],[15.753,0.033],[0,8.643],[8.644,0],[0,-27.629],[0,0],[0,-8.643],[-8.644,0],[0,0],[-26.386,5.846],[0,30.906],[1.768,1.024],[0,0],[37.853,22.64],[0,17.49],[-0.025,0],[-0.025,0],[-13.187,-0.023],[0,-8.643],[-8.644,0],[0,26.864],[0,0],[0,8.643],[8.644,0],[0,0],[26.389,-5.847],[0,-15.958],[-9.731,-9.283],[0,0],[-1.932,-1.155],[-15.848,-9.183],[0,-17.472]],"v":[[-0.079,-72.891],[-0.005,-72.888],[0.061,-72.891],[28.621,-44.271],[44.271,-28.621],[59.921,-44.271],[15.645,-102.107],[15.645,-130.209],[-0.005,-145.859],[-15.656,-130.209],[-15.656,-102.519],[-59.921,-49.174],[-22.985,3.135],[-17.618,6.278],[-15.969,7.264],[28.621,49.151],[0.075,72.891],[0,72.887],[-0.074,72.891],[-28.621,49.151],[-44.271,33.501],[-59.921,49.151],[-15.65,102.519],[-15.65,130.209],[0,145.859],[15.651,130.209],[15.651,102.519],[59.921,49.151],[42.73,9.364],[0.097,-19.598],[-1.548,-20.583],[-7.292,-23.947],[-28.621,-49.174]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.003,250.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[97.631,0],[0,97.631],[-97.631,0],[0,-97.631]],"o":[[-97.631,0],[0,-97.631],[97.631,0],[0,97.631]],"v":[[0,177.059],[-177.059,0],[0,-177.059],[177.059,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[114.89,0],[0,-114.89],[-114.89,0],[0,114.89]],"o":[[-114.89,0],[0,114.89],[114.89,0],[0,-114.89]],"v":[[0,-208.359],[-208.359,0],[0,208.359],[208.359,0]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,249.999],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":300,"st":0,"bm":0}]},{"id":"comp_1","nm":"hover-coin","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[250.75,442.5,0],"to":[0,0.018,0],"ti":[0,-0.087,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3.668,"s":[250.75,442.667,0],"to":[0,0.287,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[250.75,443.5,0],"to":[0,0,0],"ti":[0,0.287,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18.332,"s":[250.75,442.667,0],"to":[0,-0.087,0],"ti":[0,0.018,0]},{"t":22,"s":[250.75,442.5,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0.75,-195,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,-195],[0.5,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3.668,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-9.417,-195],[10.917,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.5,-195],[63,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18.332,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-9.417,-195],[10.917,-195]],"c":false}]},{"t":22,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,-195],[0.5,-195]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":22,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.75,347,0],"ix":2,"l":2},"a":{"a":0,"k":[0.75,-195,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[167.25,-195],[167.25,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3.668,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[129.125,-195],[149.875,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.5,-195],[63,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18.332,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-149.208,-195],[-128.458,-195]],"c":false}]},{"t":22,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-166.75,-195],[-166.75,-195]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":22,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.75,153.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0.75,-195,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[167.25,-195],[167.25,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3.668,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[129.125,-195],[149.875,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.5,-195],[63,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18.332,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-149.208,-195],[-128.458,-195]],"c":false}]},{"t":22,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-166.75,-195],[-166.75,-195]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":22,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.75,250.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0.75,-195,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[193.25,-195],[193.25,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3.668,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[150.792,-195],[171.542,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.5,-195],[63,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18.332,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-170.875,-195],[-150.125,-195]],"c":false}]},{"t":22,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-192.75,-195],[-192.75,-195]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":22,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[250.75,57.5,0],"to":[0,-0.018,0],"ti":[0,0.087,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3.668,"s":[250.75,57.333,0],"to":[0,-0.287,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[250.75,56.5,0],"to":[0,0,0],"ti":[0,-0.287,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18.332,"s":[250.75,57.333,0],"to":[0,0.087,0],"ti":[0,-0.018,0]},{"t":22,"s":[250.75,57.5,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0.75,-195,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,-195],[0.5,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3.668,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-9.417,-195],[10.917,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.5,-195],[63,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18.332,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-9.417,-195],[10.917,-195]],"c":false}]},{"t":22,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,-195],[0.5,-195]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":22,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.998,249.999,0],"ix":2,"l":2},"a":{"a":0,"k":[249.998,249.999,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-20.836],[0,20.836]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3.668,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-10.167,-20.808],[-10.167,20.863]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-60.999,-20.671],[-60.999,21]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18.332,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-10.167,-20.808],[-10.167,20.863]],"c":false}]},{"t":22,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-20.836],[0,20.836]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.006,197.911],[0.006,239.582]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3.668,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-10.162,197.938],[-10.162,239.61]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61,198.075],[-61,239.746]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18.332,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-10.162,197.938],[-10.162,239.61]],"c":false}]},{"t":22,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.006,197.911],[0.006,239.582]],"c":false}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]],"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3.668,"s":[{"i":[[0,0],[-20.375,0],[0,24.45],[31.566,22.661],[0,24.45],[-20.375,0],[0,-24.45]],"o":[[0,24.45],[20.375,0],[0,-24.45],[-14.85,-10.661],[0,-24.45],[20.375,0],[0,0]],"v":[[-47.054,158.552],[-10.162,197.943],[26.731,158.552],[-18.148,102.248],[-47.054,60.227],[-10.162,20.859],[26.731,65.13]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[0,0],[0,0],[0,24.45],[0,22.661],[0,24.45],[0,0],[0,-24.45]],"o":[[0,24.45],[0,0],[0,-24.45],[0,-10.661],[0,-24.45],[0,0],[0,0]],"v":[[-60.999,158.689],[-61,198.08],[-61,158.689],[-60.999,102.385],[-60.999,60.364],[-61,20.995],[-61,65.266]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18.332,"s":[{"i":[[0,0],[-20.375,0],[0,24.45],[31.566,22.661],[0,24.45],[-20.375,0],[0,-24.45]],"o":[[0,24.45],[20.375,0],[0,-24.45],[-14.85,-10.661],[0,-24.45],[20.375,0],[0,0]],"v":[[-47.054,158.552],[-10.162,197.943],[26.731,158.552],[-18.148,102.248],[-47.054,60.227],[-10.162,20.859],[26.731,65.13]],"c":false}]},{"t":22,"s":[{"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]],"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"c":false}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"t":0,"s":[100],"h":1},{"t":11,"s":[0],"h":1}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,-106.43],[106.43,0],[0,106.43],[-106.43,0]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0]],"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3.668,"s":[{"i":[[0,-106.43],[88.692,0],[0,106.43],[-88.692,0]],"o":[[0,106.43],[-88.692,0],[0,-106.43],[88.692,0]],"v":[[150.425,109.397],[-10.166,302.106],[-170.757,109.397],[-10.166,-83.313]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[0,-106.43],[-0.001,0],[0,106.43],[0.001,0]],"o":[[0,106.43],[0.001,0],[0,-106.43],[-0.001,0]],"v":[[-61.001,109.534],[-61,302.243],[-60.998,109.534],[-61,-83.176]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18.332,"s":[{"i":[[0,-106.43],[88.692,0],[0,106.43],[-88.692,0]],"o":[[0,106.43],[-88.692,0],[0,-106.43],[88.692,0]],"v":[[150.425,109.397],[-10.166,302.106],[-170.757,109.397],[-10.166,-83.313]],"c":true}]},{"t":22,"s":[{"i":[[0,-106.43],[106.43,0],[0,106.43],[-106.43,0]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0]],"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34]],"c":true}]}],"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"t":0,"s":[100],"h":1},{"t":3.668,"s":[100],"h":1},{"t":11,"s":[50],"h":1},{"t":18.33203125,"s":[50],"h":1}],"ix":2},"o":{"a":0,"k":90,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":22,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.998,249.999,0],"ix":2,"l":2},"a":{"a":0,"k":[249.998,249.999,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-20.836],[0,20.836]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3.668,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[10.5,-20.836],[10.5,20.836]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[63.001,-20.836],[63.001,20.836]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18.332,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[10.5,-20.836],[10.5,20.836]],"c":false}]},{"t":22,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-20.836],[0,20.836]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.006,197.911],[0.006,239.582]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3.668,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[10.505,197.911],[10.505,239.582]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[63,197.911],[63,239.582]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18.332,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[10.505,197.911],[10.505,239.582]],"c":false}]},{"t":22,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.006,197.911],[0.006,239.582]],"c":false}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]],"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3.668,"s":[{"i":[[0,0],[-20.375,0],[0,24.45],[31.566,22.661],[0,24.45],[-20.375,0],[0,-24.45]],"o":[[0,24.45],[20.375,0],[0,-24.45],[-14.85,-10.661],[0,-24.45],[20.375,0],[0,0]],"v":[[-26.387,158.525],[10.505,197.915],[47.397,158.525],[2.519,102.221],[-26.387,60.2],[10.505,20.831],[47.397,65.102]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[0,0],[0,0],[0,24.45],[0,22.661],[0,24.45],[0,0],[0,-24.45]],"o":[[0,24.45],[0,0],[0,-24.45],[0,-10.661],[0,-24.45],[0,0],[0,0]],"v":[[63.001,158.525],[63,197.915],[63,158.525],[63.001,102.221],[63.001,60.2],[63,20.831],[63,65.102]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18.332,"s":[{"i":[[0,0],[-20.375,0],[0,24.45],[31.566,22.661],[0,24.45],[-20.375,0],[0,-24.45]],"o":[[0,24.45],[20.375,0],[0,-24.45],[-14.85,-10.661],[0,-24.45],[20.375,0],[0,0]],"v":[[-26.387,158.525],[10.505,197.915],[47.397,158.525],[2.519,102.221],[-26.387,60.2],[10.505,20.831],[47.397,65.102]],"c":false}]},{"t":22,"s":[{"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]],"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"c":false}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":3.668,"s":[0],"h":1},{"t":11,"s":[100],"h":1},{"t":18.33203125,"s":[100],"h":1}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,-106.43],[106.43,0],[0,106.43],[-106.43,0]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0]],"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3.668,"s":[{"i":[[0,-106.43],[88.692,0],[0,106.43],[-88.692,0]],"o":[[0,106.43],[-88.692,0],[0,-106.43],[88.692,0]],"v":[[171.091,109.369],[10.501,302.079],[-150.09,109.369],[10.501,-83.34]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[0,-106.43],[-0.001,0],[0,106.43],[0.001,0]],"o":[[0,106.43],[0.001,0],[0,-106.43],[-0.001,0]],"v":[[62.999,109.369],[63,302.079],[63.002,109.369],[63,-83.34]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18.332,"s":[{"i":[[0,-106.43],[88.692,0],[0,106.43],[-88.692,0]],"o":[[0,106.43],[-88.692,0],[0,-106.43],[88.692,0]],"v":[[171.091,109.369],[10.501,302.079],[-150.09,109.369],[10.501,-83.34]],"c":true}]},{"t":22,"s":[{"i":[[0,-106.43],[106.43,0],[0,106.43],[-106.43,0]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0]],"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34]],"c":true}]}],"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"t":0,"s":[50],"h":1},{"t":3.668,"s":[50],"h":1},{"t":11,"s":[100],"h":1},{"t":18.33203125,"s":[100],"h":1}],"ix":2},"o":{"a":0,"k":-90,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":1,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":22,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[250.75,442.5,0],"to":[0,0.018,0],"ti":[0,-0.087,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25.324,"s":[250.75,442.667,0],"to":[0,0.287,0],"ti":[0,0,0]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":31.5,"s":[250.75,443.5,0],"to":[0,0,0],"ti":[0,0.018,0]},{"t":60,"s":[250.75,442.5,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0.75,-195,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,-195],[0.5,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25.324,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-9.417,-195],[10.917,-195]],"c":false}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":31.5,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.5,-195],[63,-195]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,-195],[0.5,-195]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":22,"op":60,"st":1,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.75,347,0],"ix":2,"l":2},"a":{"a":0,"k":[0.75,-195,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[167.25,-195],[167.25,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25.324,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[129.125,-195],[149.875,-195]],"c":false}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":31.5,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.5,-195],[63,-195]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-166.75,-195],[-166.75,-195]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":22,"op":60,"st":1,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.75,153.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0.75,-195,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[167.25,-195],[167.25,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25.324,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[129.125,-195],[149.875,-195]],"c":false}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":31.5,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.5,-195],[63,-195]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-166.75,-195],[-166.75,-195]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":22,"op":60,"st":1,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.75,250.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0.75,-195,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[193.25,-195],[193.25,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25.324,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[150.792,-195],[171.542,-195]],"c":false}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":31.5,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.5,-195],[63,-195]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-192.75,-195],[-192.75,-195]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":22,"op":60,"st":1,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[250.75,57.5,0],"to":[0,-0.018,0],"ti":[0,0.087,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25.324,"s":[250.75,57.333,0],"to":[0,-0.287,0],"ti":[0,0,0]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":31.5,"s":[250.75,56.5,0],"to":[0,0,0],"ti":[0,-0.018,0]},{"t":60,"s":[250.75,57.5,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0.75,-195,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,-195],[0.5,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25.324,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-9.417,-195],[10.917,-195]],"c":false}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":31.5,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.5,-195],[63,-195]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,-195],[0.5,-195]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":22,"op":60,"st":1,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.998,249.999,0],"ix":2,"l":2},"a":{"a":0,"k":[249.998,249.999,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-20.836],[0,20.836]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25.324,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-10.167,-20.808],[-10.167,20.863]],"c":false}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":31.5,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-60.999,-20.671],[-60.999,21]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-20.836],[0,20.836]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.006,197.911],[0.006,239.582]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25.324,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-10.162,197.938],[-10.162,239.61]],"c":false}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":31.5,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61,198.075],[-61,239.746]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.006,197.911],[0.006,239.582]],"c":false}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]],"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25.324,"s":[{"i":[[0,0],[-20.375,0],[0,24.45],[31.566,22.661],[0,24.45],[-20.375,0],[0,-24.45]],"o":[[0,24.45],[20.375,0],[0,-24.45],[-14.85,-10.661],[0,-24.45],[20.375,0],[0,0]],"v":[[-47.054,158.552],[-10.162,197.943],[26.731,158.552],[-18.148,102.248],[-47.054,60.227],[-10.162,20.859],[26.731,65.13]],"c":false}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":31.5,"s":[{"i":[[0,0],[0,0],[0,24.45],[0,22.661],[0,24.45],[0,0],[0,-24.45]],"o":[[0,24.45],[0,0],[0,-24.45],[0,-10.661],[0,-24.45],[0,0],[0,0]],"v":[[-60.999,158.689],[-61,198.08],[-61,158.689],[-60.999,102.385],[-60.999,60.364],[-61,20.995],[-61,65.266]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]],"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"c":false}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"t":22,"s":[100],"h":1},{"t":31.5,"s":[0],"h":1}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[0,-106.43],[106.43,0],[0,106.43],[-106.43,0]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0]],"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25.324,"s":[{"i":[[0,-106.43],[88.692,0],[0,106.43],[-88.692,0]],"o":[[0,106.43],[-88.692,0],[0,-106.43],[88.692,0]],"v":[[150.425,109.397],[-10.166,302.106],[-170.757,109.397],[-10.166,-83.313]],"c":true}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":31.5,"s":[{"i":[[0,-106.43],[-0.001,0],[0,106.43],[0.001,0]],"o":[[0,106.43],[0.001,0],[0,-106.43],[-0.001,0]],"v":[[-61.001,109.534],[-61,302.243],[-60.998,109.534],[-61,-83.176]],"c":true}]},{"t":60,"s":[{"i":[[0,-106.43],[106.43,0],[0,106.43],[-106.43,0]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0]],"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34]],"c":true}]}],"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"t":22,"s":[100],"h":1},{"t":25.324,"s":[100],"h":1},{"t":31.5,"s":[50],"h":1}],"ix":2},"o":{"a":0,"k":90,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":22,"op":60,"st":1,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.998,249.999,0],"ix":2,"l":2},"a":{"a":0,"k":[249.998,249.999,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-20.836],[0,20.836]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25.324,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[10.5,-20.836],[10.5,20.836]],"c":false}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":31.5,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[63.001,-20.836],[63.001,20.836]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-20.836],[0,20.836]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.006,197.911],[0.006,239.582]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25.324,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[10.505,197.911],[10.505,239.582]],"c":false}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":31.5,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[63,197.911],[63,239.582]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.006,197.911],[0.006,239.582]],"c":false}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]],"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25.324,"s":[{"i":[[0,0],[-20.375,0],[0,24.45],[31.566,22.661],[0,24.45],[-20.375,0],[0,-24.45]],"o":[[0,24.45],[20.375,0],[0,-24.45],[-14.85,-10.661],[0,-24.45],[20.375,0],[0,0]],"v":[[-26.387,158.525],[10.505,197.915],[47.397,158.525],[2.519,102.221],[-26.387,60.2],[10.505,20.831],[47.397,65.102]],"c":false}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":31.5,"s":[{"i":[[0,0],[0,0],[0,24.45],[0,22.661],[0,24.45],[0,0],[0,-24.45]],"o":[[0,24.45],[0,0],[0,-24.45],[0,-10.661],[0,-24.45],[0,0],[0,0]],"v":[[63.001,158.525],[63,197.915],[63,158.525],[63.001,102.221],[63.001,60.2],[63,20.831],[63,65.102]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]],"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"c":false}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"t":22,"s":[0],"h":1},{"t":25.324,"s":[0],"h":1},{"t":31.5,"s":[100],"h":1}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[0,-106.43],[106.43,0],[0,106.43],[-106.43,0]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0]],"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25.324,"s":[{"i":[[0,-106.43],[88.692,0],[0,106.43],[-88.692,0]],"o":[[0,106.43],[-88.692,0],[0,-106.43],[88.692,0]],"v":[[171.091,109.369],[10.501,302.079],[-150.09,109.369],[10.501,-83.34]],"c":true}]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0.167},"t":31.5,"s":[{"i":[[0,-106.43],[-0.001,0],[0,106.43],[0.001,0]],"o":[[0,106.43],[0.001,0],[0,-106.43],[-0.001,0]],"v":[[62.999,109.369],[63,302.079],[63.002,109.369],[63,-83.34]],"c":true}]},{"t":60,"s":[{"i":[[0,-106.43],[106.43,0],[0,106.43],[-106.43,0]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0]],"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34]],"c":true}]}],"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"t":22,"s":[50],"h":1},{"t":25.324,"s":[50],"h":1},{"t":31.5,"s":[100],"h":1}],"ix":2},"o":{"a":0,"k":-90,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":1,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":22,"op":60,"st":1,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.998,249.999,0],"ix":2,"l":2},"a":{"a":0,"k":[249.998,249.999,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-13.186,0.025],[-0.025,0],[-0.022,0],[0,-15.761],[-8.644,0],[0,8.643],[25.476,6.899],[0,0],[8.644,0],[0,-8.643],[0,0],[0,-26.851],[-16.456,-9.535],[-1.805,-1.08],[0,0],[0,-14.091],[13.187,-0.023],[0.025,0],[0.025,0],[0,17.49],[8.644,0],[0,-8.643],[-26.389,-5.847],[0,0],[-8.644,0],[0,8.643],[0,0],[0,26.864],[11.729,11.188],[19.101,11.424],[0,0],[1.892,1.096],[0,12.245]],"o":[[0.025,0],[0.022,0],[15.753,0.033],[0,8.643],[8.644,0],[0,-27.629],[0,0],[0,-8.643],[-8.644,0],[0,0],[-26.386,5.846],[0,30.906],[1.768,1.024],[0,0],[37.853,22.64],[0,17.49],[-0.025,0],[-0.025,0],[-13.187,-0.023],[0,-8.643],[-8.644,0],[0,26.864],[0,0],[0,8.643],[8.644,0],[0,0],[26.389,-5.847],[0,-15.958],[-9.731,-9.283],[0,0],[-1.932,-1.155],[-15.848,-9.183],[0,-17.472]],"v":[[-0.079,-72.891],[-0.005,-72.888],[0.061,-72.891],[28.621,-44.271],[44.271,-28.621],[59.921,-44.271],[15.645,-102.107],[15.645,-130.209],[-0.005,-145.859],[-15.656,-130.209],[-15.656,-102.519],[-59.921,-49.174],[-22.985,3.135],[-17.618,6.278],[-15.969,7.264],[28.621,49.151],[0.075,72.891],[0,72.887],[-0.074,72.891],[-28.621,49.151],[-44.271,33.501],[-59.921,49.151],[-15.65,102.519],[-15.65,130.209],[0,145.859],[15.651,130.209],[15.651,102.519],[59.921,49.151],[42.73,9.364],[0.097,-19.598],[-1.548,-20.583],[-7.292,-23.947],[-28.621,-49.174]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.003,250.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[97.631,0],[0,97.631],[-97.631,0],[0,-97.631]],"o":[[-97.631,0],[0,-97.631],[97.631,0],[0,97.631]],"v":[[0,177.059],[-177.059,0],[0,-177.059],[177.059,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[114.89,0],[0,-114.89],[-114.89,0],[0,114.89]],"o":[[-114.89,0],[0,114.89],[114.89,0],[0,-114.89]],"v":[[0,-208.359],[-208.359,0],[0,208.359],[208.359,0]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,249.999],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":-60,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.998,249.999,0],"ix":2,"l":2},"a":{"a":0,"k":[249.998,249.999,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-13.186,0.025],[-0.025,0],[-0.022,0],[0,-15.761],[-8.644,0],[0,8.643],[25.476,6.899],[0,0],[8.644,0],[0,-8.643],[0,0],[0,-26.851],[-16.456,-9.535],[-1.805,-1.08],[0,0],[0,-14.091],[13.187,-0.023],[0.025,0],[0.025,0],[0,17.49],[8.644,0],[0,-8.643],[-26.389,-5.847],[0,0],[-8.644,0],[0,8.643],[0,0],[0,26.864],[11.729,11.188],[19.101,11.424],[0,0],[1.892,1.096],[0,12.245]],"o":[[0.025,0],[0.022,0],[15.753,0.033],[0,8.643],[8.644,0],[0,-27.629],[0,0],[0,-8.643],[-8.644,0],[0,0],[-26.386,5.846],[0,30.906],[1.768,1.024],[0,0],[37.853,22.64],[0,17.49],[-0.025,0],[-0.025,0],[-13.187,-0.023],[0,-8.643],[-8.644,0],[0,26.864],[0,0],[0,8.643],[8.644,0],[0,0],[26.389,-5.847],[0,-15.958],[-9.731,-9.283],[0,0],[-1.932,-1.155],[-15.848,-9.183],[0,-17.472]],"v":[[-0.079,-72.891],[-0.005,-72.888],[0.061,-72.891],[28.621,-44.271],[44.271,-28.621],[59.921,-44.271],[15.645,-102.107],[15.645,-130.209],[-0.005,-145.859],[-15.656,-130.209],[-15.656,-102.519],[-59.921,-49.174],[-22.985,3.135],[-17.618,6.278],[-15.969,7.264],[28.621,49.151],[0.075,72.891],[0,72.887],[-0.074,72.891],[-28.621,49.151],[-44.271,33.501],[-59.921,49.151],[-15.65,102.519],[-15.65,130.209],[0,145.859],[15.651,130.209],[15.651,102.519],[59.921,49.151],[42.73,9.364],[0.097,-19.598],[-1.548,-20.583],[-7.292,-23.947],[-28.621,-49.174]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.003,250.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[97.631,0],[0,97.631],[-97.631,0],[0,-97.631]],"o":[[-97.631,0],[0,-97.631],[97.631,0],[0,97.631]],"v":[[0,177.059],[-177.059,0],[0,-177.059],[177.059,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[114.89,0],[0,-114.89],[-114.89,0],[0,114.89]],"o":[[-114.89,0],[0,114.89],[114.89,0],[0,-114.89]],"v":[[0,-208.359],[-208.359,0],[0,208.359],[208.359,0]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,249.999],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":300,"st":0,"bm":0}]},{"id":"comp_2","nm":"loop-coin","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[250.75,442.5,0],"to":[0,0.018,0],"ti":[0,-0.087,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[250.75,442.667,0],"to":[0,0.287,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[250.75,443.5,0],"to":[0,0,0],"ti":[0,0.287,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[250.75,442.667,0],"to":[0,-0.087,0],"ti":[0,0.018,0]},{"t":60,"s":[250.75,442.5,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0.75,-195,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,-195],[0.5,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-9.417,-195],[10.917,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.5,-195],[63,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-9.417,-195],[10.917,-195]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,-195],[0.5,-195]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":170,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.75,347,0],"ix":2,"l":2},"a":{"a":0,"k":[0.75,-195,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[167.25,-195],[167.25,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[129.125,-195],[149.875,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.5,-195],[63,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-149.208,-195],[-128.458,-195]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-166.75,-195],[-166.75,-195]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":170,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.75,153.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0.75,-195,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[167.25,-195],[167.25,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[129.125,-195],[149.875,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.5,-195],[63,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-149.208,-195],[-128.458,-195]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-166.75,-195],[-166.75,-195]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":170,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.75,250.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0.75,-195,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[193.25,-195],[193.25,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[150.792,-195],[171.542,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.5,-195],[63,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-170.875,-195],[-150.125,-195]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-192.75,-195],[-192.75,-195]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":170,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[250.75,57.5,0],"to":[0,-0.018,0],"ti":[0,0.087,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[250.75,57.333,0],"to":[0,-0.287,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[250.75,56.5,0],"to":[0,0,0],"ti":[0,-0.287,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[250.75,57.333,0],"to":[0,0.087,0],"ti":[0,-0.018,0]},{"t":60,"s":[250.75,57.5,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0.75,-195,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,-195],[0.5,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-9.417,-195],[10.917,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61.5,-195],[63,-195]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-9.417,-195],[10.917,-195]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,-195],[0.5,-195]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":170,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.998,249.999,0],"ix":2,"l":2},"a":{"a":0,"k":[249.998,249.999,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-20.836],[0,20.836]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-10.167,-20.808],[-10.167,20.863]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-60.999,-20.671],[-60.999,21]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-10.167,-20.808],[-10.167,20.863]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-20.836],[0,20.836]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.006,197.911],[0.006,239.582]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-10.162,197.938],[-10.162,239.61]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-61,198.075],[-61,239.746]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-10.162,197.938],[-10.162,239.61]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.006,197.911],[0.006,239.582]],"c":false}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]],"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[-20.375,0],[0,24.45],[31.566,22.661],[0,24.45],[-20.375,0],[0,-24.45]],"o":[[0,24.45],[20.375,0],[0,-24.45],[-14.85,-10.661],[0,-24.45],[20.375,0],[0,0]],"v":[[-47.054,158.552],[-10.162,197.943],[26.731,158.552],[-18.148,102.248],[-47.054,60.227],[-10.162,20.859],[26.731,65.13]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0],[0,24.45],[0,22.661],[0,24.45],[0,0],[0,-24.45]],"o":[[0,24.45],[0,0],[0,-24.45],[0,-10.661],[0,-24.45],[0,0],[0,0]],"v":[[-60.999,158.689],[-61,198.08],[-61,158.689],[-60.999,102.385],[-60.999,60.364],[-61,20.995],[-61,65.266]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[0,0],[-20.375,0],[0,24.45],[31.566,22.661],[0,24.45],[-20.375,0],[0,-24.45]],"o":[[0,24.45],[20.375,0],[0,-24.45],[-14.85,-10.661],[0,-24.45],[20.375,0],[0,0]],"v":[[-47.054,158.552],[-10.162,197.943],[26.731,158.552],[-18.148,102.248],[-47.054,60.227],[-10.162,20.859],[26.731,65.13]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]],"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"c":false}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"t":0,"s":[100],"h":1},{"t":30,"s":[0],"h":1}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,-106.43],[106.43,0],[0,106.43],[-106.43,0]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0]],"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,-106.43],[88.692,0],[0,106.43],[-88.692,0]],"o":[[0,106.43],[-88.692,0],[0,-106.43],[88.692,0]],"v":[[150.425,109.397],[-10.166,302.106],[-170.757,109.397],[-10.166,-83.313]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,-106.43],[-0.001,0],[0,106.43],[0.001,0]],"o":[[0,106.43],[0.001,0],[0,-106.43],[-0.001,0]],"v":[[-61.001,109.534],[-61,302.243],[-60.998,109.534],[-61,-83.176]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[0,-106.43],[88.692,0],[0,106.43],[-88.692,0]],"o":[[0,106.43],[-88.692,0],[0,-106.43],[88.692,0]],"v":[[150.425,109.397],[-10.166,302.106],[-170.757,109.397],[-10.166,-83.313]],"c":true}]},{"t":60,"s":[{"i":[[0,-106.43],[106.43,0],[0,106.43],[-106.43,0]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0]],"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34]],"c":true}]}],"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"t":0,"s":[100],"h":1},{"t":10,"s":[100],"h":1},{"t":30,"s":[50],"h":1},{"t":50,"s":[50],"h":1}],"ix":2},"o":{"a":0,"k":90,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":170,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.998,249.999,0],"ix":2,"l":2},"a":{"a":0,"k":[249.998,249.999,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-20.836],[0,20.836]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[10.5,-20.836],[10.5,20.836]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[63.001,-20.836],[63.001,20.836]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[10.5,-20.836],[10.5,20.836]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-20.836],[0,20.836]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.006,197.911],[0.006,239.582]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[10.505,197.911],[10.505,239.582]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[63,197.911],[63,239.582]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[10.505,197.911],[10.505,239.582]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.006,197.911],[0.006,239.582]],"c":false}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]],"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[-20.375,0],[0,24.45],[31.566,22.661],[0,24.45],[-20.375,0],[0,-24.45]],"o":[[0,24.45],[20.375,0],[0,-24.45],[-14.85,-10.661],[0,-24.45],[20.375,0],[0,0]],"v":[[-26.387,158.525],[10.505,197.915],[47.397,158.525],[2.519,102.221],[-26.387,60.2],[10.505,20.831],[47.397,65.102]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0],[0,24.45],[0,22.661],[0,24.45],[0,0],[0,-24.45]],"o":[[0,24.45],[0,0],[0,-24.45],[0,-10.661],[0,-24.45],[0,0],[0,0]],"v":[[63.001,158.525],[63,197.915],[63,158.525],[63.001,102.221],[63.001,60.2],[63,20.831],[63,65.102]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[0,0],[-20.375,0],[0,24.45],[31.566,22.661],[0,24.45],[-20.375,0],[0,-24.45]],"o":[[0,24.45],[20.375,0],[0,-24.45],[-14.85,-10.661],[0,-24.45],[20.375,0],[0,0]],"v":[[-26.387,158.525],[10.505,197.915],[47.397,158.525],[2.519,102.221],[-26.387,60.2],[10.505,20.831],[47.397,65.102]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[-24.45,0],[0,24.45],[37.879,22.661],[0,24.45],[-24.45,0],[0,-24.45]],"o":[[0,24.45],[24.45,0],[0,-24.45],[-17.82,-10.661],[0,-24.45],[24.45,0],[0,0]],"v":[[-44.265,158.525],[0.006,197.915],[44.277,158.525],[-9.578,102.221],[-44.265,60.2],[0.006,20.831],[44.277,65.102]],"c":false}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":10,"s":[0],"h":1},{"t":30,"s":[100],"h":1},{"t":50,"s":[100],"h":1}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,-106.43],[106.43,0],[0,106.43],[-106.43,0]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0]],"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,-106.43],[88.692,0],[0,106.43],[-88.692,0]],"o":[[0,106.43],[-88.692,0],[0,-106.43],[88.692,0]],"v":[[171.091,109.369],[10.501,302.079],[-150.09,109.369],[10.501,-83.34]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,-106.43],[-0.001,0],[0,106.43],[0.001,0]],"o":[[0,106.43],[0.001,0],[0,-106.43],[-0.001,0]],"v":[[62.999,109.369],[63,302.079],[63.002,109.369],[63,-83.34]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[0,-106.43],[88.692,0],[0,106.43],[-88.692,0]],"o":[[0,106.43],[-88.692,0],[0,-106.43],[88.692,0]],"v":[[171.091,109.369],[10.501,302.079],[-150.09,109.369],[10.501,-83.34]],"c":true}]},{"t":60,"s":[{"i":[[0,-106.43],[106.43,0],[0,106.43],[-106.43,0]],"o":[[0,106.43],[-106.43,0],[0,-106.43],[106.43,0]],"v":[[192.71,109.369],[0.001,302.079],[-192.709,109.369],[0.001,-83.34]],"c":true}]}],"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,140.63],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"t":0,"s":[50],"h":1},{"t":10,"s":[50],"h":1},{"t":30,"s":[100],"h":1},{"t":50,"s":[100],"h":1}],"ix":2},"o":{"a":0,"k":-90,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('103-coin-cash-monetization-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":1,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":170,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"lordicon.com Outlines","cl":"com","sr":1,"ks":{"o":{"a":0,"k":20,"ix":11,"x":"var $bm_rt;\nvar checkbox = thisComp.layer('02092020').effect('02092020002')('Checkbox');\nif (checkbox == 1) {\n $bm_rt = 20;\n} else {\n $bm_rt = 0;\n}\n;"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.934,481.369,0],"ix":2,"l":2},"a":{"a":0,"k":[79.934,0.369,0],"ix":1,"l":2},"s":{"a":0,"k":[265.159,265.159,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[1.415,0],[11.014,0],[11.014,-2.523],[4.656,-2.523],[4.656,-14.809],[1.415,-14.809]],"c":true},"ix":2},"nm":"l","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"l","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[11.167,-7.199],[12.992,-1.661],[18.243,0.369],[23.514,-1.743],[25.381,-7.548],[23.494,-13.127],[18.284,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[14.49,-7.302],[15.577,-11.609],[18.305,-12.86],[21.689,-10.235],[22.058,-7.589],[21.053,-3.343],[18.284,-1.969],[15.597,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[-0.287,-0.841],[-0.144,-0.82],[0,0],[0.164,0.656],[0.226,1.743],[2.236,0.205],[0,2.769],[0.923,0.8],[1.641,-0.021],[0,0]],"o":[[0,0],[0,0],[0,0],[0.533,0],[0.205,0.574],[0,0],[-0.164,-0.246],[-0.103,-0.41],[-0.267,-1.928],[0.718,-0.205],[0,-0.964],[-1.19,-1.026],[0,0],[0,0]],"v":[[27.381,0],[30.622,0],[30.622,-5.989],[33.411,-5.989],[35.011,-5.148],[35.811,0],[39.318,0],[38.867,-1.067],[38.416,-3.938],[35.749,-7.343],[38.847,-10.973],[37.554,-13.824],[33.063,-14.829],[27.381,-14.829]],"c":true},"ix":2},"nm":"r","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-0.492,-0.349],[0,-1.005],[0.226,-0.164],[0.369,0],[0,0]],"o":[[0,0],[1.005,0],[0.287,0.185],[0,1.046],[-0.513,0.41],[0,0],[0,0]],"v":[[30.519,-12.491],[32.652,-12.491],[34.744,-12.142],[35.524,-10.481],[34.703,-8.758],[33.083,-8.348],[30.519,-8.348]],"c":true},"ix":2},"nm":"r","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"r","np":5,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-0.554,0.103],[0,4.553],[1.866,1.374],[0.82,0],[0,0]],"o":[[0,0],[1.497,0],[2.81,-0.513],[0,-2.113],[-1.784,-1.313],[0,0],[0,0]],"v":[[41.068,0],[45.683,0],[48.349,-0.164],[53.6,-7.609],[51.077,-13.434],[45.97,-14.768],[41.068,-14.788]],"c":true},"ix":2},"nm":"d","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-0.656,-0.185],[0,-2.092],[1.251,-1.251],[1.354,0],[0.349,0.021]],"o":[[1.825,-0.082],[1.99,0.554],[0,0.718],[-0.923,0.923],[-0.369,0],[0,0]],"v":[[44.288,-12.388],[47.611,-12.183],[50.318,-7.609],[48.985,-3.425],[45.539,-2.4],[44.288,-2.441]],"c":true},"ix":2},"nm":"d","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"d","np":5,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[55.669,0],[58.849,0],[58.849,-14.87],[55.669,-14.87]],"c":true},"ix":2},"nm":"i","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"i","np":3,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[3.241,0],[0,-4.697],[-5.107,0],[-1.313,1.354],[-0.062,0.882],[0,0],[1.333,0],[0,0.882],[-2.359,0],[-0.062,-0.513]],"o":[[0,-2.954],[-4.164,0],[0,3.671],[1.354,0],[1.19,-1.231],[0,0],[-0.062,1.969],[-3.097,0],[0,-3.056],[2.154,0],[0,0]],"v":[[73.104,-9.989],[67.587,-14.911],[60.798,-7.097],[67.566,0.349],[71.894,-1.313],[73.227,-4.799],[69.884,-4.799],[67.218,-1.99],[64.121,-7.076],[67.464,-12.593],[69.864,-9.989]],"c":true},"ix":2},"nm":"c","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"c","np":3,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[74.546,-7.199],[76.372,-1.661],[81.622,0.369],[86.894,-1.743],[88.76,-7.548],[86.873,-13.127],[81.663,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[77.869,-7.302],[78.956,-11.609],[81.684,-12.86],[85.068,-10.235],[85.437,-7.589],[84.432,-3.343],[81.663,-1.969],[78.977,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[91.007,0],[94.001,0],[94.001,-12.306],[99.744,0],[104.113,0],[104.113,-14.829],[101.159,-14.829],[101.159,-3.159],[95.601,-14.829],[91.007,-14.829]],"c":true},"ix":2},"nm":"n","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"n","np":3,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[106.893,0],[109.497,0],[109.497,-2.728],[106.893,-2.728]],"c":true},"ix":2},"nm":".","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":".","np":3,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[3.241,0],[0,-4.697],[-5.107,0],[-1.313,1.354],[-0.062,0.882],[0,0],[1.333,0],[0,0.882],[-2.359,0],[-0.062,-0.513]],"o":[[0,-2.954],[-4.164,0],[0,3.671],[1.354,0],[1.19,-1.231],[0,0],[-0.062,1.969],[-3.097,0],[0,-3.056],[2.154,0],[0,0]],"v":[[124.04,-9.989],[118.523,-14.911],[111.734,-7.097],[118.502,0.349],[122.83,-1.313],[124.163,-4.799],[120.82,-4.799],[118.154,-1.99],[115.057,-7.076],[118.4,-12.593],[120.8,-9.989]],"c":true},"ix":2},"nm":"c","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"c","np":3,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[125.482,-7.199],[127.308,-1.661],[132.558,0.369],[137.829,-1.743],[139.696,-7.548],[137.809,-13.127],[132.599,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[128.805,-7.302],[129.892,-11.609],[132.62,-12.86],[136.004,-10.235],[136.373,-7.589],[135.368,-3.343],[132.599,-1.969],[129.912,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[141.696,0],[144.67,0],[144.67,-12.716],[148.629,0],[151.254,0],[155.295,-12.716],[155.295,0],[158.453,0],[158.453,-14.829],[153.408,-14.829],[150.024,-4.041],[146.885,-14.829],[141.696,-14.829]],"c":true},"ix":2},"nm":"m","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"m","np":3,"cix":2,"bm":0,"ix":12,"mn":"ADBE Vector Group","hd":false}],"ip":2.5,"op":25,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":3,"nm":"02092020","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-105,15,0],"ix":2,"l":2},"a":{"a":0,"k":[60,60,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"02092020002","np":3,"mn":"ADBE Checkbox Control","ix":1,"en":1,"ef":[{"ty":7,"nm":"Checkbox","mn":"ADBE Checkbox Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"Color & Stroke Change","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-55,-102,0],"ix":2,"l":2,"x":"var $bm_rt;\n$bm_rt = effect('Axis')('Point');"},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2,"x":"var $bm_rt;\nvar temp;\ntemp = effect('Scale')('Slider');\n$bm_rt = [\n temp,\n temp\n];"}},"ao":0,"ef":[{"ty":5,"nm":"Primary","np":3,"mn":"ADBE Color Control","ix":1,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]},{"ty":5,"nm":"Axis","np":3,"mn":"ADBE Point Control","ix":2,"en":1,"ef":[{"ty":3,"nm":"Point","mn":"ADBE Point Control-0001","ix":1,"v":{"a":0,"k":[250,250],"ix":1}}]},{"ty":5,"nm":"Scale","np":3,"mn":"ADBE Slider Control","ix":3,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":100,"ix":1}}]},{"ty":5,"nm":"State-Intro","np":3,"mn":"ADBE Slider Control","ix":4,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]},{"ty":5,"nm":"State-Hover","np":3,"mn":"ADBE Slider Control","ix":5,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":1,"ix":1}}]},{"ty":5,"nm":"State-Loop","np":3,"mn":"ADBE Slider Control","ix":6,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"in-coin","parent":3,"refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Intro')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":71,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"hover-coin","parent":3,"refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Hover')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":71,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"loop-coin","parent":3,"refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Loop')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":71,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/frontend/public/lotties/system-outline-109-slider-toggle-settings.json b/frontend/public/lotties/system-outline-109-slider-toggle-settings.json deleted file mode 100644 index ba9609bc3..000000000 --- a/frontend/public/lotties/system-outline-109-slider-toggle-settings.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.8.1","fr":60,"ip":0,"op":31,"w":500,"h":500,"nm":"109-slider-toggle-settings-outline","ddd":0,"assets":[{"id":"comp_0","nm":"in-slider","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.998,250.654,0],"ix":2,"l":2},"a":{"a":0,"k":[249.998,250.654,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[17.218,0],[0,17.218],[-17.218,0],[0,-17.217]],"o":[[-17.218,0],[0,-17.217],[17.218,0],[0,17.218]],"v":[[61.63,31.225],[30.405,0],[61.63,-31.225],[92.854,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[29.074,0],[6.965,-26.928],[0,0],[0,-8.643],[-8.644,0],[0,0],[-29.074,0],[-6.965,26.928],[0,0],[0,8.643]],"o":[[0,0],[-6.965,-26.928],[-29.074,0],[0,0],[-8.644,0],[0,8.643],[0,0],[6.965,26.928],[29.074,0],[0,0],[8.644,0],[0,-8.643]],"v":[[170.998,-15.65],[122.171,-15.65],[61.63,-62.525],[1.088,-15.65],[-170.998,-15.65],[-186.648,0],[-170.998,15.65],[1.088,15.65],[61.63,62.525],[122.171,15.65],[170.998,15.65],[186.648,0]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.874,249.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[17.218,0],[0,17.217],[-17.218,0],[0,-17.218]],"o":[[-17.218,0],[0,-17.218],[17.218,0],[0,17.217]],"v":[[-124.781,31.225],[-156.005,0],[-124.781,-31.225],[-93.556,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[29.074,0],[0,-34.477],[-34.477,0],[-6.965,26.928],[0,0],[0,8.643]],"o":[[0,0],[-6.965,-26.928],[-34.477,0],[0,34.477],[29.074,0],[0,0],[8.644,0],[0,-8.643]],"v":[[171.656,-15.65],[-64.239,-15.65],[-124.781,-62.525],[-187.306,0],[-124.781,62.525],[-64.239,15.65],[171.656,15.65],[187.306,0]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[249.779,376.306],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-17.218,0],[0,-17.217],[17.218,0],[0,17.218]],"o":[[17.218,0],[0,17.218],[-17.218,0],[0,-17.217]],"v":[[-124.781,-31.225],[-93.556,0],[-124.781,31.225],[-156.005,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-34.477,0],[-6.965,26.928],[0,0],[0,8.643],[8.644,0],[0,0],[29.074,0],[0,-34.477]],"o":[[29.074,0],[0,0],[8.644,0],[0,-8.643],[0,0],[-6.965,-26.928],[-34.477,0],[0,34.477]],"v":[[-124.781,62.525],[-64.239,15.65],[171.656,15.65],[187.306,0],[171.656,-15.65],[-64.239,-15.65],[-124.781,-62.525],[-187.306,0]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[249.784,125.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":3,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":28,"op":224,"st":28,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.779,376.306,0],"ix":2,"l":2},"a":{"a":0,"k":[249.779,376.306,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":1,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[183.081,0],[183.132,0]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[124.567,0],[30.132,0]],"c":false}]},{"t":28,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[124.567,0],[-124.567,0]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[296.868,376.306],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":1,"s":[{"i":[[0,-1.533],[1.533,0],[0,1.533],[-1.533,0]],"o":[[0,1.533],[-1.533,0],[0,-1.533],[1.533,0]],"v":[[354.977,0],[352.201,2.775],[349.426,0],[352.201,-2.775]],"c":true}]},{"i":{"x":0.4,"y":1},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[0,-33.661],[33.661,0],[0,33.661],[-33.661,0]],"o":[[0,33.661],[-33.661,0],[0,-33.661],[33.661,0]],"v":[[201.445,0],[140.497,60.949],[79.548,0],[140.497,-60.949]],"c":true}]},{"t":28,"s":[{"i":[[0,-25.888],[25.888,0],[0,25.888],[-25.888,0]],"o":[[0,25.888],[-25.888,0],[0,-25.888],[25.888,0]],"v":[[46.875,0],[0,46.875],[-46.875,0],[0,-46.875]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[124.998,376.306],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":28,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.874,249.998,0],"ix":2,"l":2},"a":{"a":0,"k":[250.874,249.998,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-367.376,0],[-367.753,0]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-175.524,0],[-44.7,0]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-104.053,0],[33.32,0]],"c":false}]},{"t":28,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-31.247,0],[31.247,0]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[390.626,249.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-149.212,0],[-149.288,0]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-123.604,0],[-89.161,0]],"c":false}]},{"t":28,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-90.712,0],[90.712,0]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[170.589,249.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[0,-0.55],[-0.55,0],[0,0.55],[0.55,0]],"o":[[0,0.55],[0.55,0],[0,-0.55],[-0.55,0]],"v":[[-291.246,0],[-290.25,0.996],[-289.254,0],[-290.25,-0.996]],"c":true}]},{"i":{"x":0.4,"y":1},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[0,-35.345],[-35.345,0],[0,35.345],[35.345,0]],"o":[[0,35.345],[35.345,0],[0,-35.345],[-35.345,0]],"v":[[-225.398,0],[-161.4,63.998],[-97.402,0],[-161.4,-63.998]],"c":true}]},{"t":28,"s":[{"i":[[0,-25.888],[-25.888,0],[0,25.888],[25.888,0]],"o":[[0,25.888],[25.888,0],[0,-25.888],[-25.888,0]],"v":[[-46.875,0],[0,46.875],[46.875,0],[0,-46.875]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[312.504,249.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":4,"op":28,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.784,125.002,0],"ix":2,"l":2},"a":{"a":0,"k":[249.784,125.002,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":7,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[185.113,0.001],[185.127,0.001]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.167,"y":0.167},"t":14,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[124.567,0],[29.127,0]],"c":false}]},{"t":28,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[124.567,0],[-124.567,0]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[296.873,125.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":7,"s":[{"i":[[0,-0.431],[0.43,0],[0,0.431],[-0.431,0]],"o":[[0,0.431],[-0.43,0],[0,-0.431],[0.431,0]],"v":[[356.99,0.001],[356.21,0.78],[355.431,0.001],[356.21,-0.779]],"c":true}]},{"i":{"x":0.4,"y":1},"o":{"x":0.167,"y":0.167},"t":14,"s":[{"i":[[0,-33.109],[33.109,0],[0,33.109],[-33.108,0]],"o":[[0,33.109],[-33.109,0],[0,-33.109],[33.108,0]],"v":[[200.45,0],[140.501,59.949],[80.553,0],[140.501,-59.948]],"c":true}]},{"t":28,"s":[{"i":[[0,-25.888],[25.888,0],[0,25.888],[-25.888,0]],"o":[[0,25.888],[-25.888,0],[0,-25.888],[25.888,0]],"v":[[46.875,0],[0,46.875],[-46.875,0],[0,-46.875]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[125.003,125.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":7,"op":28,"st":0,"bm":0}]},{"id":"comp_1","nm":"hover-slider","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.998,250.654,0],"ix":2,"l":2},"a":{"a":0,"k":[249.998,250.654,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[17.218,0],[0,17.218],[-17.218,0],[0,-17.217]],"o":[[-17.218,0],[0,-17.217],[17.218,0],[0,17.218]],"v":[[61.63,31.225],[30.405,0],[61.63,-31.225],[92.854,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[29.074,0],[6.965,-26.928],[0,0],[0,-8.643],[-8.644,0],[0,0],[-29.074,0],[-6.965,26.928],[0,0],[0,8.643]],"o":[[0,0],[-6.965,-26.928],[-29.074,0],[0,0],[-8.644,0],[0,8.643],[0,0],[6.965,26.928],[29.074,0],[0,0],[8.644,0],[0,-8.643]],"v":[[170.998,-15.65],[122.171,-15.65],[61.63,-62.525],[1.088,-15.65],[-170.998,-15.65],[-186.648,0],[-170.998,15.65],[1.088,15.65],[61.63,62.525],[122.171,15.65],[170.998,15.65],[186.648,0]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.874,249.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[17.218,0],[0,17.217],[-17.218,0],[0,-17.218]],"o":[[-17.218,0],[0,-17.218],[17.218,0],[0,17.217]],"v":[[-124.781,31.225],[-156.005,0],[-124.781,-31.225],[-93.556,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[29.074,0],[0,-34.477],[-34.477,0],[-6.965,26.928],[0,0],[0,8.643]],"o":[[0,0],[-6.965,-26.928],[-34.477,0],[0,34.477],[29.074,0],[0,0],[8.644,0],[0,-8.643]],"v":[[171.656,-15.65],[-64.239,-15.65],[-124.781,-62.525],[-187.306,0],[-124.781,62.525],[-64.239,15.65],[171.656,15.65],[187.306,0]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[249.779,376.306],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-17.218,0],[0,-17.217],[17.218,0],[0,17.218]],"o":[[17.218,0],[0,17.218],[-17.218,0],[0,-17.217]],"v":[[-124.781,-31.225],[-93.556,0],[-124.781,31.225],[-156.005,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-34.477,0],[-6.965,26.928],[0,0],[0,8.643],[8.644,0],[0,0],[29.074,0],[0,-34.477]],"o":[[29.074,0],[0,0],[8.644,0],[0,-8.643],[0,0],[-6.965,-26.928],[-34.477,0],[0,34.477]],"v":[[-124.781,62.525],[-64.239,15.65],[171.656,15.65],[187.306,0],[171.656,-15.65],[-64.239,-15.65],[-124.781,-62.525],[-187.306,0]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[249.784,125.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":3,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":29,"op":238,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.998,250.654,0],"ix":2,"l":2},"a":{"a":0,"k":[249.998,250.654,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[17.218,0],[0,17.218],[-17.218,0],[0,-17.217]],"o":[[-17.218,0],[0,-17.217],[17.218,0],[0,17.218]],"v":[[61.63,31.225],[30.405,0],[61.63,-31.225],[92.854,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[29.074,0],[6.965,-26.928],[0,0],[0,-8.643],[-8.644,0],[0,0],[-29.074,0],[-6.965,26.928],[0,0],[0,8.643]],"o":[[0,0],[-6.965,-26.928],[-29.074,0],[0,0],[-8.644,0],[0,8.643],[0,0],[6.965,26.928],[29.074,0],[0,0],[8.644,0],[0,-8.643]],"v":[[170.998,-15.65],[122.171,-15.65],[61.63,-62.525],[1.088,-15.65],[-170.998,-15.65],[-186.648,0],[-170.998,15.65],[1.088,15.65],[61.63,62.525],[122.171,15.65],[170.998,15.65],[186.648,0]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.874,249.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[17.218,0],[0,17.217],[-17.218,0],[0,-17.218]],"o":[[-17.218,0],[0,-17.218],[17.218,0],[0,17.217]],"v":[[-124.781,31.225],[-156.005,0],[-124.781,-31.225],[-93.556,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[29.074,0],[0,-34.477],[-34.477,0],[-6.965,26.928],[0,0],[0,8.643]],"o":[[0,0],[-6.965,-26.928],[-34.477,0],[0,34.477],[29.074,0],[0,0],[8.644,0],[0,-8.643]],"v":[[171.656,-15.65],[-64.239,-15.65],[-124.781,-62.525],[-187.306,0],[-124.781,62.525],[-64.239,15.65],[171.656,15.65],[187.306,0]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[249.779,376.306],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-17.218,0],[0,-17.217],[17.218,0],[0,17.218]],"o":[[17.218,0],[0,17.218],[-17.218,0],[0,-17.217]],"v":[[-124.781,-31.225],[-93.556,0],[-124.781,31.225],[-156.005,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-34.477,0],[-6.965,26.928],[0,0],[0,8.643],[8.644,0],[0,0],[29.074,0],[0,-34.477]],"o":[[29.074,0],[0,0],[8.644,0],[0,-8.643],[0,0],[-6.965,-26.928],[-34.477,0],[0,34.477]],"v":[[-124.781,62.525],[-64.239,15.65],[171.656,15.65],[187.306,0],[171.656,-15.65],[-64.239,-15.65],[-124.781,-62.525],[-187.306,0]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[249.784,125.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":3,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.779,376.306,0],"ix":2,"l":2},"a":{"a":0,"k":[249.779,376.306,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[326.5,376.25],[78.5,376.25]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":1,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[68.292]},{"t":30,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":1,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[100]},{"t":30,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[124.567,0],[-124.567,0]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":15,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[124.567,0],[-45.297,0]],"c":false}]},{"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[124.567,0],[-124.567,0]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[296.868,376.306],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[0,-25.888],[25.888,0],[0,25.888],[-25.888,0]],"o":[[0,25.888],[-25.888,0],[0,-25.888],[25.888,0]],"v":[[46.875,0],[0,46.875],[-46.875,0],[0,-46.875]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":15,"s":[{"i":[[0,-25.888],[25.888,0],[0,25.888],[-25.888,0]],"o":[[0,25.888],[-25.888,0],[0,-25.888],[25.888,0]],"v":[[126.145,0],[79.27,46.875],[32.394,0],[79.27,-46.875]],"c":true}]},{"t":30,"s":[{"i":[[0,-25.888],[25.888,0],[0,25.888],[-25.888,0]],"o":[[0,25.888],[-25.888,0],[0,-25.888],[25.888,0]],"v":[[46.875,0],[0,46.875],[-46.875,0],[0,-46.875]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[124.998,376.306],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":29,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.874,249.998,0],"ix":2,"l":2},"a":{"a":0,"k":[250.874,249.998,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-31.247,0],[31.247,0]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":13,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[24.753,0],[31.247,0]],"c":false}]},{"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-31.247,0],[31.247,0]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[390.626,249.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-90.712,0],[90.712,0]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":13,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-90.712,0],[146.712,0]],"c":false}]},{"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-90.712,0],[90.712,0]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[170.589,249.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[0,-25.888],[-25.888,0],[0,25.888],[25.888,0]],"o":[[0,25.888],[25.888,0],[0,-25.888],[-25.888,0]],"v":[[-46.875,0],[0,46.875],[46.875,0],[0,-46.875]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":13,"s":[{"i":[[0,-25.888],[-25.888,0],[0,25.888],[25.888,0]],"o":[[0,25.888],[25.888,0],[0,-25.888],[-25.888,0]],"v":[[9.125,0],[56,46.875],[102.875,0],[56,-46.875]],"c":true}]},{"t":30,"s":[{"i":[[0,-25.888],[-25.888,0],[0,25.888],[25.888,0]],"o":[[0,25.888],[25.888,0],[0,-25.888],[-25.888,0]],"v":[[-46.875,0],[0,46.875],[46.875,0],[0,-46.875]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[312.504,249.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":29,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.784,125.002,0],"ix":2,"l":2},"a":{"a":0,"k":[249.784,125.002,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[124.567,0],[-124.567,0]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":16,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[124.567,0],[29.714,0]],"c":false}]},{"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[124.567,0],[-124.567,0]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[296.873,125.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[0,-25.888],[25.888,0],[0,25.888],[-25.888,0]],"o":[[0,25.888],[-25.888,0],[0,-25.888],[25.888,0]],"v":[[46.875,0],[0,46.875],[-46.875,0],[0,-46.875]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":16,"s":[{"i":[[0,-25.888],[25.888,0],[0,25.888],[-25.888,0]],"o":[[0,25.888],[-25.888,0],[0,-25.888],[25.888,0]],"v":[[201.156,0],[154.28,46.876],[107.405,0],[154.28,-46.875]],"c":true}]},{"t":30,"s":[{"i":[[0,-25.888],[25.888,0],[0,25.888],[-25.888,0]],"o":[[0,25.888],[-25.888,0],[0,-25.888],[25.888,0]],"v":[[46.875,0],[0,46.875],[-46.875,0],[0,-46.875]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[125.003,125.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[326.5,125.25],[78.5,125.25]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":1,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":16,"s":[40.292]},{"t":30,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":1,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":16,"s":[100]},{"t":30,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":29,"st":0,"bm":0}]},{"id":"comp_2","nm":"morph-slider","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":0,"k":[249.998,250.654,0],"ix":2,"l":2},"a":{"a":0,"k":[249.998,250.654,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[17.218,0],[0,17.218],[-17.218,0],[0,-17.217]],"o":[[-17.218,0],[0,-17.217],[17.218,0],[0,17.218]],"v":[[61.255,32.163],[30.03,0.937],[61.255,-30.288],[92.479,0.937]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[29.074,0],[6.965,-26.928],[0,0],[0,-8.643],[-8.644,0],[0,0],[-29.074,0],[-6.965,26.928],[0,0],[0,8.643]],"o":[[0,0],[-6.965,-26.928],[-29.074,0],[0,0],[-8.644,0],[0,8.643],[0,0],[6.965,26.928],[29.074,0],[0,0],[8.644,0],[0,-8.643]],"v":[[170.061,-14.713],[121.796,-14.713],[61.255,-61.588],[0.713,-14.713],[-172.123,-14.713],[-187.773,0.937],[-172.123,16.587],[0.713,16.587],[61.255,63.463],[121.796,16.587],[170.061,16.587],[185.711,0.937]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.874,249.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[17.218,0],[0,17.217],[-17.218,0],[0,-17.218]],"o":[[-17.218,0],[0,-17.218],[17.218,0],[0,17.217]],"v":[[-124.781,31.225],[-156.005,0],[-124.781,-31.225],[-93.556,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[29.074,0],[0,-34.477],[-34.477,0],[-6.965,26.928],[0,0],[0,8.643]],"o":[[0,0],[-6.965,-26.928],[-34.477,0],[0,34.477],[29.074,0],[0,0],[8.644,0],[0,-8.643]],"v":[[171.656,-15.65],[-64.239,-15.65],[-124.781,-62.525],[-187.306,0],[-124.781,62.525],[-64.239,15.65],[171.656,15.65],[187.306,0]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[249.779,376.306],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-17.218,0],[0,-17.217],[17.218,0],[0,17.218]],"o":[[17.218,0],[0,17.218],[-17.218,0],[0,-17.217]],"v":[[-124.781,-31.225],[-93.556,0],[-124.781,31.225],[-156.005,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-34.477,0],[-6.965,26.928],[0,0],[0,8.643],[8.644,0],[0,0],[29.074,0],[0,-34.477]],"o":[[29.074,0],[0,0],[8.644,0],[0,-8.643],[0,0],[-6.965,-26.928],[-34.477,0],[0,34.477]],"v":[[-124.781,62.525],[-64.239,15.65],[171.656,15.65],[187.306,0],[171.656,-15.65],[-64.239,-15.65],[-124.781,-62.525],[-187.306,0]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[249.784,125.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":3,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":28,"op":238,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.998,250.654,0],"ix":2,"l":2},"a":{"a":0,"k":[249.998,250.654,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[17.218,0],[0,17.218],[-17.218,0],[0,-17.217]],"o":[[-17.218,0],[0,-17.217],[17.218,0],[0,17.218]],"v":[[61.63,31.225],[30.405,0],[61.63,-31.225],[92.854,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[29.074,0],[6.965,-26.928],[0,0],[0,-8.643],[-8.644,0],[0,0],[-29.074,0],[-6.965,26.928],[0,0],[0,8.643]],"o":[[0,0],[-6.965,-26.928],[-29.074,0],[0,0],[-8.644,0],[0,8.643],[0,0],[6.965,26.928],[29.074,0],[0,0],[8.644,0],[0,-8.643]],"v":[[170.998,-15.65],[122.171,-15.65],[61.63,-62.525],[1.088,-15.65],[-170.998,-15.65],[-186.648,0],[-170.998,15.65],[1.088,15.65],[61.63,62.525],[122.171,15.65],[170.998,15.65],[186.648,0]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.874,249.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[17.218,0],[0,17.217],[-17.218,0],[0,-17.218]],"o":[[-17.218,0],[0,-17.218],[17.218,0],[0,17.217]],"v":[[-124.781,31.225],[-156.005,0],[-124.781,-31.225],[-93.556,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[29.074,0],[0,-34.477],[-34.477,0],[-6.965,26.928],[0,0],[0,8.643]],"o":[[0,0],[-6.965,-26.928],[-34.477,0],[0,34.477],[29.074,0],[0,0],[8.644,0],[0,-8.643]],"v":[[171.656,-15.65],[-64.239,-15.65],[-124.781,-62.525],[-187.306,0],[-124.781,62.525],[-64.239,15.65],[171.656,15.65],[187.306,0]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[249.779,376.306],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-17.218,0],[0,-17.217],[17.218,0],[0,17.218]],"o":[[17.218,0],[0,17.218],[-17.218,0],[0,-17.217]],"v":[[-124.781,-31.225],[-93.556,0],[-124.781,31.225],[-156.005,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-34.477,0],[-6.965,26.928],[0,0],[0,8.643],[8.644,0],[0,0],[29.074,0],[0,-34.477]],"o":[[29.074,0],[0,0],[8.644,0],[0,-8.643],[0,0],[-6.965,-26.928],[-34.477,0],[0,34.477]],"v":[[-124.781,62.525],[-64.239,15.65],[171.656,15.65],[187.306,0],[171.656,-15.65],[-64.239,-15.65],[-124.781,-62.525],[-187.306,0]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[249.784,125.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":3,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.874,249.998,0],"ix":2,"l":2},"a":{"a":0,"k":[250.874,249.998,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":1,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-31.247,0],[31.247,0]],"c":false}]},{"t":28,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-156.247,0],[31.247,0]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[390.626,249.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":1,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-90.712,0],[90.712,0]],"c":false}]},{"t":28,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-90.712,0],[-34.288,0]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[170.589,249.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":1,"s":[{"i":[[0,-25.888],[-25.888,0],[0,25.888],[25.888,0]],"o":[[0,25.888],[25.888,0],[0,-25.888],[-25.888,0]],"v":[[-46.875,0],[0,46.875],[46.875,0],[0,-46.875]],"c":true}]},{"t":28,"s":[{"i":[[0,-25.888],[-25.888,0],[0,25.888],[25.888,0]],"o":[[0,25.888],[25.888,0],[0,-25.888],[-25.888,0]],"v":[[-171.875,0],[-125,46.875],[-78.125,0],[-125,-46.875]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[312.504,249.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":28,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.779,376.306,0],"ix":2,"l":2},"a":{"a":0,"k":[249.779,376.306,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[326.5,376.25],[78.5,376.25]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":1,"s":[100]},{"t":28,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":1,"s":[100]},{"t":28,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":1,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[124.567,0],[-124.567,0]],"c":false}]},{"t":28,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[124.567,0],[125.433,0]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[296.868,376.306],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":1,"s":[{"i":[[0,-25.888],[25.888,0],[0,25.888],[-25.888,0]],"o":[[0,25.888],[-25.888,0],[0,-25.888],[25.888,0]],"v":[[46.875,0],[0,46.875],[-46.875,0],[0,-46.875]],"c":true}]},{"t":28,"s":[{"i":[[0,-25.888],[25.888,0],[0,25.888],[-25.888,0]],"o":[[0,25.888],[-25.888,0],[0,-25.888],[25.888,0]],"v":[[296.875,0],[250,46.875],[203.125,0],[250,-46.875]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[124.998,376.306],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":28,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.784,125.002,0],"ix":2,"l":2},"a":{"a":0,"k":[249.784,125.002,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":1,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[124.567,0],[-124.567,0]],"c":false}]},{"t":28,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[124.567,0],[125.433,0]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[296.873,125.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":1,"s":[{"i":[[0,-25.888],[25.888,0],[0,25.888],[-25.888,0]],"o":[[0,25.888],[-25.888,0],[0,-25.888],[25.888,0]],"v":[[46.875,0],[0,46.875],[-46.875,0],[0,-46.875]],"c":true}]},{"t":28,"s":[{"i":[[0,-25.888],[25.888,0],[0,25.888],[-25.888,0]],"o":[[0,25.888],[-25.888,0],[0,-25.888],[25.888,0]],"v":[[296.875,0],[250,46.876],[203.125,0],[250,-46.875]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[125.003,125.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[326.5,125.25],[78.5,125.25]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":1,"s":[100]},{"t":28,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":1,"s":[100]},{"t":28,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('109-slider-toggle-settings-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":28,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"lordicon.com Outlines","cl":"com","sr":1,"ks":{"o":{"a":0,"k":20,"ix":11,"x":"var $bm_rt;\nvar checkbox = thisComp.layer('02092020').effect('02092020002')('Checkbox');\nif (checkbox == 1) {\n $bm_rt = 20;\n} else {\n $bm_rt = 0;\n}\n;"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.934,481.369,0],"ix":2,"l":2},"a":{"a":0,"k":[79.934,0.369,0],"ix":1,"l":2},"s":{"a":0,"k":[265.159,265.159,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[1.415,0],[11.014,0],[11.014,-2.523],[4.656,-2.523],[4.656,-14.809],[1.415,-14.809]],"c":true},"ix":2},"nm":"l","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"l","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[11.167,-7.199],[12.992,-1.661],[18.243,0.369],[23.514,-1.743],[25.381,-7.548],[23.494,-13.127],[18.284,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[14.49,-7.302],[15.577,-11.609],[18.305,-12.86],[21.689,-10.235],[22.058,-7.589],[21.053,-3.343],[18.284,-1.969],[15.597,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[-0.287,-0.841],[-0.144,-0.82],[0,0],[0.164,0.656],[0.226,1.743],[2.236,0.205],[0,2.769],[0.923,0.8],[1.641,-0.021],[0,0]],"o":[[0,0],[0,0],[0,0],[0.533,0],[0.205,0.574],[0,0],[-0.164,-0.246],[-0.103,-0.41],[-0.267,-1.928],[0.718,-0.205],[0,-0.964],[-1.19,-1.026],[0,0],[0,0]],"v":[[27.381,0],[30.622,0],[30.622,-5.989],[33.411,-5.989],[35.011,-5.148],[35.811,0],[39.318,0],[38.867,-1.067],[38.416,-3.938],[35.749,-7.343],[38.847,-10.973],[37.554,-13.824],[33.063,-14.829],[27.381,-14.829]],"c":true},"ix":2},"nm":"r","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-0.492,-0.349],[0,-1.005],[0.226,-0.164],[0.369,0],[0,0]],"o":[[0,0],[1.005,0],[0.287,0.185],[0,1.046],[-0.513,0.41],[0,0],[0,0]],"v":[[30.519,-12.491],[32.652,-12.491],[34.744,-12.142],[35.524,-10.481],[34.703,-8.758],[33.083,-8.348],[30.519,-8.348]],"c":true},"ix":2},"nm":"r","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"r","np":5,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-0.554,0.103],[0,4.553],[1.866,1.374],[0.82,0],[0,0]],"o":[[0,0],[1.497,0],[2.81,-0.513],[0,-2.113],[-1.784,-1.313],[0,0],[0,0]],"v":[[41.068,0],[45.683,0],[48.349,-0.164],[53.6,-7.609],[51.077,-13.434],[45.97,-14.768],[41.068,-14.788]],"c":true},"ix":2},"nm":"d","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-0.656,-0.185],[0,-2.092],[1.251,-1.251],[1.354,0],[0.349,0.021]],"o":[[1.825,-0.082],[1.99,0.554],[0,0.718],[-0.923,0.923],[-0.369,0],[0,0]],"v":[[44.288,-12.388],[47.611,-12.183],[50.318,-7.609],[48.985,-3.425],[45.539,-2.4],[44.288,-2.441]],"c":true},"ix":2},"nm":"d","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"d","np":5,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[55.669,0],[58.849,0],[58.849,-14.87],[55.669,-14.87]],"c":true},"ix":2},"nm":"i","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"i","np":3,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[3.241,0],[0,-4.697],[-5.107,0],[-1.313,1.354],[-0.062,0.882],[0,0],[1.333,0],[0,0.882],[-2.359,0],[-0.062,-0.513]],"o":[[0,-2.954],[-4.164,0],[0,3.671],[1.354,0],[1.19,-1.231],[0,0],[-0.062,1.969],[-3.097,0],[0,-3.056],[2.154,0],[0,0]],"v":[[73.104,-9.989],[67.587,-14.911],[60.798,-7.097],[67.566,0.349],[71.894,-1.313],[73.227,-4.799],[69.884,-4.799],[67.218,-1.99],[64.121,-7.076],[67.464,-12.593],[69.864,-9.989]],"c":true},"ix":2},"nm":"c","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"c","np":3,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[74.546,-7.199],[76.372,-1.661],[81.622,0.369],[86.894,-1.743],[88.76,-7.548],[86.873,-13.127],[81.663,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[77.869,-7.302],[78.956,-11.609],[81.684,-12.86],[85.068,-10.235],[85.437,-7.589],[84.432,-3.343],[81.663,-1.969],[78.977,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[91.007,0],[94.001,0],[94.001,-12.306],[99.744,0],[104.113,0],[104.113,-14.829],[101.159,-14.829],[101.159,-3.159],[95.601,-14.829],[91.007,-14.829]],"c":true},"ix":2},"nm":"n","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"n","np":3,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[106.893,0],[109.497,0],[109.497,-2.728],[106.893,-2.728]],"c":true},"ix":2},"nm":".","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":".","np":3,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[3.241,0],[0,-4.697],[-5.107,0],[-1.313,1.354],[-0.062,0.882],[0,0],[1.333,0],[0,0.882],[-2.359,0],[-0.062,-0.513]],"o":[[0,-2.954],[-4.164,0],[0,3.671],[1.354,0],[1.19,-1.231],[0,0],[-0.062,1.969],[-3.097,0],[0,-3.056],[2.154,0],[0,0]],"v":[[124.04,-9.989],[118.523,-14.911],[111.734,-7.097],[118.502,0.349],[122.83,-1.313],[124.163,-4.799],[120.82,-4.799],[118.154,-1.99],[115.057,-7.076],[118.4,-12.593],[120.8,-9.989]],"c":true},"ix":2},"nm":"c","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"c","np":3,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[125.482,-7.199],[127.308,-1.661],[132.558,0.369],[137.829,-1.743],[139.696,-7.548],[137.809,-13.127],[132.599,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[128.805,-7.302],[129.892,-11.609],[132.62,-12.86],[136.004,-10.235],[136.373,-7.589],[135.368,-3.343],[132.599,-1.969],[129.912,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[141.696,0],[144.67,0],[144.67,-12.716],[148.629,0],[151.254,0],[155.295,-12.716],[155.295,0],[158.453,0],[158.453,-14.829],[153.408,-14.829],[150.024,-4.041],[146.885,-14.829],[141.696,-14.829]],"c":true},"ix":2},"nm":"m","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"m","np":3,"cix":2,"bm":0,"ix":12,"mn":"ADBE Vector Group","hd":false}],"ip":2.5,"op":25,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":3,"nm":"02092020","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-105,15,0],"ix":2,"l":2},"a":{"a":0,"k":[60,60,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"02092020002","np":3,"mn":"ADBE Checkbox Control","ix":1,"en":1,"ef":[{"ty":7,"nm":"Checkbox","mn":"ADBE Checkbox Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"Color & Stroke Change","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-55,-102,0],"ix":2,"l":2,"x":"var $bm_rt;\n$bm_rt = effect('Axis')('Point');"},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2,"x":"var $bm_rt;\nvar temp;\ntemp = effect('Scale')('Slider');\n$bm_rt = [\n temp,\n temp\n];"}},"ao":0,"ef":[{"ty":5,"nm":"Primary","np":3,"mn":"ADBE Color Control","ix":1,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]},{"ty":5,"nm":"Axis","np":3,"mn":"ADBE Point Control","ix":2,"en":1,"ef":[{"ty":3,"nm":"Point","mn":"ADBE Point Control-0001","ix":1,"v":{"a":0,"k":[250,250],"ix":1}}]},{"ty":5,"nm":"Scale","np":3,"mn":"ADBE Slider Control","ix":3,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":100,"ix":1}}]},{"ty":5,"nm":"State-Intro","np":3,"mn":"ADBE Slider Control","ix":4,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]},{"ty":5,"nm":"State-Hover","np":3,"mn":"ADBE Slider Control","ix":5,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]},{"ty":5,"nm":"State-Morph","np":3,"mn":"ADBE Slider Control","ix":6,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":1,"ix":1}}]}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"in-slider","parent":3,"refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Intro')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":41,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"hover-slider","parent":3,"refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Hover')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":41,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"morph-slider","parent":3,"refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Morph')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":41,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/frontend/public/lotties/system-outline-126-verified.json b/frontend/public/lotties/system-outline-126-verified.json deleted file mode 100644 index e612e8161..000000000 --- a/frontend/public/lotties/system-outline-126-verified.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.8.1","fr":60,"ip":0,"op":61,"w":500,"h":500,"nm":"126-verified-outline","ddd":0,"assets":[{"id":"comp_0","nm":"in-verified","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.004,250.003,0],"ix":2,"l":2},"a":{"a":0,"k":[250.004,250.003,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[6.109,-6.116],[-6.115,-6.108],[0,0],[-4.002,0],[-3.055,3.051],[0,0],[6.107,6.115],[6.115,-6.107],[0,0]],"o":[[-6.115,-6.107],[-6.107,6.116],[0,0],[3.056,3.052],[4.002,0],[0,0],[6.115,-6.108],[-6.109,-6.116],[0,0],[0,0]],"v":[[-69.803,-8.539],[-91.936,-8.526],[-91.922,13.607],[-39.704,65.762],[-28.644,70.339],[-17.584,65.762],[91.922,-43.616],[91.936,-65.749],[69.803,-65.762],[-28.644,32.57]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588238537,0.074509806931,0.192156866193,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('126-verified-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[263.158,242.187],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[73.521,-47.165],[0,0],[0,0],[11.506,86.895],[0,0],[-39.84,20.453],[-33.146,-7.159]],"o":[[-11.51,86.919],[0,0],[0,0],[-73.503,-47.153],[0,0],[33.146,-7.159],[39.84,20.453],[0,0]],"v":[[149.277,-67.68],[15.49,143.298],[0,153.266],[-15.509,143.287],[-149.274,-67.655],[-154.479,-107.449],[0,-153.936],[154.479,-107.449]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.073,1.615],[32.173,20.107],[5.074,-3.172],[35.445,-7.092],[-1.068,-8.164],[0,0],[-81.252,-52.124],[0,0],[-2.944,0],[-2.579,1.66],[0,0],[-12.724,96.093],[0,0]],"o":[[-35.445,-7.092],[-5.074,-3.172],[-32.173,20.107],[-8.073,1.615],[0,0],[12.72,96.068],[0,0],[2.579,1.66],[2.944,0],[0,0],[81.27,-52.136],[0,0],[1.068,-8.164]],"v":[[174.946,-135.138],[8.294,-185.147],[-8.294,-185.147],[-174.946,-135.138],[-187.394,-117.763],[-180.307,-63.572],[-32.428,169.62],[-8.469,185.036],[0,187.526],[8.469,185.036],[32.409,169.632],[180.31,-63.596],[187.394,-117.763]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588238537,0.074509806931,0.192156866193,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('126-verified-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[13.16,-7.817,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[137.654,-115.818],[-28.644,54.689],[-80.863,2.534]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588238537,0.074509806931,0.192156866193,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('126-verified-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.1],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[0]},{"t":39,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.1],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[0]},{"t":39,"s":[26.2]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":25,"op":60,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.584],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":15.695,"s":[77]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":36.752,"s":[-25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":52.188,"s":[8]},{"t":60,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.281,"y":1},"o":{"x":0.333,"y":0},"t":15.695,"s":[249.783,113.999,0],"to":[0,8.667,0],"ti":[0,4.667,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[249.783,165.999,0],"to":[0,-4.667,0],"ti":[0,8.667,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":42.541,"s":[249.783,85.999,0],"to":[0,-8.667,0],"ti":[0,-4.667,0]},{"t":52.1875,"s":[249.783,113.999,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-0.215,-136.005,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.807},"o":{"x":0.333,"y":0},"t":15.695,"s":[{"i":[[0,0],[0,0],[8.87,-28.834],[42.606,0],[0,0],[0,0],[0,0],[-42.127,45.088],[0,0],[-40.99,1.371]],"o":[[0,0],[16.136,17.513],[-9.413,30.597],[0,0],[0,0],[0,0],[-77.329,0.092],[0,0],[0,0],[39.509,-0.629]],"v":[[64.622,-188.295],[64.244,-188.35],[78.727,-109.993],[-0.255,-53.189],[0.097,-53.125],[0.089,-53.125],[-0.314,-53.189],[-63.37,-190.092],[-63.377,-190.045],[-0.007,-218.875]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.193},"t":21.746,"s":[{"i":[[0,0],[0,0],[12.256,-30.674],[42.668,-8.454],[0,0],[0,0],[0,0],[-25.502,59.236],[0,0],[-73.493,5.608]],"o":[[0,0],[9.535,24.634],[-14.097,34.46],[0,0],[0,0],[0,0],[-77.329,-15.207],[0,0],[0,0],[67.007,-0.392]],"v":[[94.002,-151.387],[95.117,-150.643],[93.331,-61.263],[7.167,11.228],[0.136,8.492],[0.129,8.492],[-7.561,11.227],[-94.511,-151.847],[-94.64,-152.329],[-0.005,-204.437]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":30,"s":[{"i":[[0,0],[0,0],[19.891,-34.822],[42.806,-27.519],[0,0],[0,0],[0,0],[15.895,90.546],[0,0],[-41.666,26.039]],"o":[[0,0],[-6.33,40.555],[-24.661,43.173],[0,0],[0,0],[0,0],[-77.33,-49.714],[0,0],[0,0],[41.667,26.039]],"v":[[183.699,-122.675],[175.269,-68.668],[126.269,48.638],[23.905,156.51],[0.007,171.873],[0,171.875],[-23.904,156.508],[-175.51,-64.584],[-184.963,-118.431],[0,-171.875]],"c":true}]},{"t":42.541015625,"s":[{"i":[[0,0],[0,0],[19.891,-34.822],[42.806,-27.519],[0,0],[0,0],[0,0],[11.993,91.145],[0,0],[-41.666,26.039]],"o":[[0,0],[-5.353,40.695],[-24.661,43.173],[0,0],[0,0],[0,0],[-77.33,-49.714],[0,0],[0,0],[41.667,26.039]],"v":[[171.875,-119.795],[164.746,-65.6],[126.269,48.638],[23.905,156.51],[0.007,171.873],[0,171.875],[-23.904,156.508],[-164.743,-65.592],[-171.875,-119.795],[0,-171.875]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588238537,0.074509806931,0.192156866193,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('126-verified-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":16,"op":60,"st":-253,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":1,"s":[250,441,0],"to":[0,-54.5,0],"ti":[0,54.5,0]},{"t":15.6953125,"s":[250,114,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,-136,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":1,"s":[{"i":[[-10.416,0],[-0.936,-10.176],[0,-0.626],[11.046,0],[0,11.046],[-0.057,0.615]],"o":[[10.42,0],[0.056,0.611],[0,11.046],[-11.046,0],[0,-0.63],[0.942,-10.17]],"v":[[0,-100],[20.103,-81.856],[20,0],[0,20],[-20,0],[-20.102,-81.867]],"c":true}]},{"t":15.6953125,"s":[{"i":[[-43.112,0],[-4.042,-42.069],[0,-2.711],[45.84,0],[0,45.84],[-0.257,2.66]],"o":[[43.129,0],[0.254,2.644],[0,45.84],[-45.84,0],[0,-2.728],[4.065,-42.046]],"v":[[0,-83],[82.616,-8.036],[83,0],[0,83],[-83,0],[-82.611,-8.085]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588238537,0.074509806931,0.192156866193,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-136],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":16,"st":0,"bm":0}]},{"id":"comp_1","nm":"hover-verified","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.004,250.003,0],"ix":2,"l":2},"a":{"a":0,"k":[250.004,250.003,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[6.109,-6.116],[-6.115,-6.108],[0,0],[-4.002,0],[-3.055,3.051],[0,0],[6.107,6.115],[6.115,-6.107],[0,0]],"o":[[-6.115,-6.107],[-6.107,6.116],[0,0],[3.056,3.052],[4.002,0],[0,0],[6.115,-6.108],[-6.109,-6.116],[0,0],[0,0]],"v":[[-69.803,-8.539],[-91.936,-8.526],[-91.922,13.607],[-39.704,65.762],[-28.644,70.339],[-17.584,65.762],[91.922,-43.616],[91.936,-65.749],[69.803,-65.762],[-28.644,32.57]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('126-verified-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[263.158,242.187],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[73.521,-47.165],[0,0],[0,0],[11.506,86.895],[0,0],[-39.84,20.453],[-33.146,-7.159]],"o":[[-11.51,86.919],[0,0],[0,0],[-73.503,-47.153],[0,0],[33.146,-7.159],[39.84,20.453],[0,0]],"v":[[149.277,-67.68],[15.49,143.298],[0,153.266],[-15.509,143.287],[-149.274,-67.655],[-154.479,-107.449],[0,-153.936],[154.479,-107.449]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.073,1.615],[32.173,20.107],[5.074,-3.172],[35.445,-7.092],[-1.068,-8.164],[0,0],[-81.252,-52.124],[0,0],[-2.944,0],[-2.579,1.66],[0,0],[-12.724,96.093],[0,0]],"o":[[-35.445,-7.092],[-5.074,-3.172],[-32.173,20.107],[-8.073,1.615],[0,0],[12.72,96.068],[0,0],[2.579,1.66],[2.944,0],[0,0],[81.27,-52.136],[0,0],[1.068,-8.164]],"v":[[174.946,-135.138],[8.294,-185.147],[-8.294,-185.147],[-174.946,-135.138],[-187.394,-117.763],[-180.307,-63.572],[-32.428,169.62],[-8.469,185.036],[0,187.526],[8.469,185.036],[32.409,169.632],[180.31,-63.596],[187.394,-117.763]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('126-verified-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":300,"st":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.004,250.003,0],"ix":2,"l":2},"a":{"a":0,"k":[250.004,250.003,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[6.109,-6.116],[-6.115,-6.108],[0,0],[-4.002,0],[-3.055,3.051],[0,0],[6.107,6.115],[6.115,-6.107],[0,0]],"o":[[-6.115,-6.107],[-6.107,6.116],[0,0],[3.056,3.052],[4.002,0],[0,0],[6.115,-6.108],[-6.109,-6.116],[0,0],[0,0]],"v":[[-69.803,-8.539],[-91.936,-8.526],[-91.922,13.607],[-39.704,65.762],[-28.644,70.339],[-17.584,65.762],[91.922,-43.616],[91.936,-65.749],[69.803,-65.762],[-28.644,32.57]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('126-verified-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[263.158,242.187],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[73.521,-47.165],[0,0],[0,0],[11.506,86.895],[0,0],[-39.84,20.453],[-33.146,-7.159]],"o":[[-11.51,86.919],[0,0],[0,0],[-73.503,-47.153],[0,0],[33.146,-7.159],[39.84,20.453],[0,0]],"v":[[149.277,-67.68],[15.49,143.298],[0,153.266],[-15.509,143.287],[-149.274,-67.655],[-154.479,-107.449],[0,-153.936],[154.479,-107.449]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.073,1.615],[32.173,20.107],[5.074,-3.172],[35.445,-7.092],[-1.068,-8.164],[0,0],[-81.252,-52.124],[0,0],[-2.944,0],[-2.579,1.66],[0,0],[-12.724,96.093],[0,0]],"o":[[-35.445,-7.092],[-5.074,-3.172],[-32.173,20.107],[-8.073,1.615],[0,0],[12.72,96.068],[0,0],[2.579,1.66],[2.944,0],[0,0],[81.27,-52.136],[0,0],[1.068,-8.164]],"v":[[174.946,-135.138],[8.294,-185.147],[-8.294,-185.147],[-174.946,-135.138],[-187.394,-117.763],[-180.307,-63.572],[-32.428,169.62],[-8.469,185.036],[0,187.526],[8.469,185.036],[32.409,169.632],[180.31,-63.596],[187.394,-117.763]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('126-verified-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.131],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":1,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.628],"y":[0]},"t":30,"s":[27]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":46,"s":[-11]},{"t":60,"s":[0]}],"ix":10},"p":{"a":0,"k":[249.998,250.004,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.131,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[0,0],[0,0],[77.333,-49.716],[0,0],[0,0],[0,0],[11.993,91.145],[0,0],[-41.666,26.039]],"o":[[0,0],[-11.99,91.149],[0,0],[0,0],[0,0],[-77.33,-49.714],[0,0],[0,0],[41.667,26.039]],"v":[[171.875,-119.795],[164.746,-65.6],[23.905,156.51],[0.007,171.873],[0,171.875],[-23.904,156.508],[-164.743,-65.592],[-171.875,-119.795],[0,-171.875]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.628,"y":0},"t":21,"s":[{"i":[[0,0],[0,0],[99.436,-63.925],[0,0],[0,0],[0,0],[15.421,117.196],[0,0],[-53.575,33.481]],"o":[[0,0],[-15.417,117.201],[0,0],[0,0],[0,0],[-99.432,-63.922],[0,0],[0,0],[53.576,33.481]],"v":[[191.412,-122.573],[182.245,-52.889],[1.15,232.704],[-29.579,252.458],[-29.588,252.46],[-60.324,232.701],[-241.417,-52.879],[-250.588,-122.573],[-29.588,-189.539]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":41.801,"s":[{"i":[[0,0],[0,0],[56.69,-36.445],[0,0],[0,0],[0,0],[8.792,66.816],[0,0],[-30.544,19.088]],"o":[[0,0],[-8.79,66.819],[0,0],[0,0],[0,0],[-56.688,-36.443],[0,0],[0,0],[30.545,19.088]],"v":[[125.996,-87.818],[120.77,-48.09],[17.524,114.733],[0.005,125.995],[0,125.996],[-17.523,114.731],[-120.768,-48.083],[-125.996,-87.818],[0,-125.996]],"c":true}]},{"t":60,"s":[{"i":[[0,0],[0,0],[77.333,-49.716],[0,0],[0,0],[0,0],[11.993,91.145],[0,0],[-41.666,26.039]],"o":[[0,0],[-11.99,91.149],[0,0],[0,0],[0,0],[-77.33,-49.714],[0,0],[0,0],[41.667,26.039]],"v":[[171.875,-119.795],[164.746,-65.6],[23.905,156.51],[0.007,171.873],[0,171.875],[-23.904,156.508],[-164.743,-65.592],[-171.875,-119.795],[0,-171.875]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('126-verified-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":-239,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.006,-0.001,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.157,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[153.363,-119.689],[-14.644,46.689],[-66.863,-5.466]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.6,"y":0},"t":21,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[167.916,-123.188],[-50.252,92.864],[-118.061,25.138]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":42,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[114.144,-89.081],[-10.899,34.749],[-49.764,-4.068]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[153.363,-119.689],[-14.644,46.689],[-66.863,-5.466]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('126-verified-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":1,"s":[100]},{"t":20,"s":[100],"h":1},{"i":{"x":[0.1],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[0]},{"t":60,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":1,"s":[26.2]},{"t":20,"s":[100],"h":1},{"i":{"x":[0.1],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[0]},{"t":60,"s":[26.2]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":1,"op":60,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"lordicon.com Outlines","cl":"com","sr":1,"ks":{"o":{"a":0,"k":20,"ix":11,"x":"var $bm_rt;\nvar checkbox = thisComp.layer('02092020').effect('02092020002')('Checkbox');\nif (checkbox == 1) {\n $bm_rt = 20;\n} else {\n $bm_rt = 0;\n}\n;"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.934,481.369,0],"ix":2,"l":2},"a":{"a":0,"k":[79.934,0.369,0],"ix":1,"l":2},"s":{"a":0,"k":[265.159,265.159,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[1.415,0],[11.014,0],[11.014,-2.523],[4.656,-2.523],[4.656,-14.809],[1.415,-14.809]],"c":true},"ix":2},"nm":"l","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"l","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[11.167,-7.199],[12.992,-1.661],[18.243,0.369],[23.514,-1.743],[25.381,-7.548],[23.494,-13.127],[18.284,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[14.49,-7.302],[15.577,-11.609],[18.305,-12.86],[21.689,-10.235],[22.058,-7.589],[21.053,-3.343],[18.284,-1.969],[15.597,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[-0.287,-0.841],[-0.144,-0.82],[0,0],[0.164,0.656],[0.226,1.743],[2.236,0.205],[0,2.769],[0.923,0.8],[1.641,-0.021],[0,0]],"o":[[0,0],[0,0],[0,0],[0.533,0],[0.205,0.574],[0,0],[-0.164,-0.246],[-0.103,-0.41],[-0.267,-1.928],[0.718,-0.205],[0,-0.964],[-1.19,-1.026],[0,0],[0,0]],"v":[[27.381,0],[30.622,0],[30.622,-5.989],[33.411,-5.989],[35.011,-5.148],[35.811,0],[39.318,0],[38.867,-1.067],[38.416,-3.938],[35.749,-7.343],[38.847,-10.973],[37.554,-13.824],[33.063,-14.829],[27.381,-14.829]],"c":true},"ix":2},"nm":"r","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-0.492,-0.349],[0,-1.005],[0.226,-0.164],[0.369,0],[0,0]],"o":[[0,0],[1.005,0],[0.287,0.185],[0,1.046],[-0.513,0.41],[0,0],[0,0]],"v":[[30.519,-12.491],[32.652,-12.491],[34.744,-12.142],[35.524,-10.481],[34.703,-8.758],[33.083,-8.348],[30.519,-8.348]],"c":true},"ix":2},"nm":"r","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"r","np":5,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-0.554,0.103],[0,4.553],[1.866,1.374],[0.82,0],[0,0]],"o":[[0,0],[1.497,0],[2.81,-0.513],[0,-2.113],[-1.784,-1.313],[0,0],[0,0]],"v":[[41.068,0],[45.683,0],[48.349,-0.164],[53.6,-7.609],[51.077,-13.434],[45.97,-14.768],[41.068,-14.788]],"c":true},"ix":2},"nm":"d","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-0.656,-0.185],[0,-2.092],[1.251,-1.251],[1.354,0],[0.349,0.021]],"o":[[1.825,-0.082],[1.99,0.554],[0,0.718],[-0.923,0.923],[-0.369,0],[0,0]],"v":[[44.288,-12.388],[47.611,-12.183],[50.318,-7.609],[48.985,-3.425],[45.539,-2.4],[44.288,-2.441]],"c":true},"ix":2},"nm":"d","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"d","np":5,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[55.669,0],[58.849,0],[58.849,-14.87],[55.669,-14.87]],"c":true},"ix":2},"nm":"i","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"i","np":3,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[3.241,0],[0,-4.697],[-5.107,0],[-1.313,1.354],[-0.062,0.882],[0,0],[1.333,0],[0,0.882],[-2.359,0],[-0.062,-0.513]],"o":[[0,-2.954],[-4.164,0],[0,3.671],[1.354,0],[1.19,-1.231],[0,0],[-0.062,1.969],[-3.097,0],[0,-3.056],[2.154,0],[0,0]],"v":[[73.104,-9.989],[67.587,-14.911],[60.798,-7.097],[67.566,0.349],[71.894,-1.313],[73.227,-4.799],[69.884,-4.799],[67.218,-1.99],[64.121,-7.076],[67.464,-12.593],[69.864,-9.989]],"c":true},"ix":2},"nm":"c","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"c","np":3,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[74.546,-7.199],[76.372,-1.661],[81.622,0.369],[86.894,-1.743],[88.76,-7.548],[86.873,-13.127],[81.663,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[77.869,-7.302],[78.956,-11.609],[81.684,-12.86],[85.068,-10.235],[85.437,-7.589],[84.432,-3.343],[81.663,-1.969],[78.977,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[91.007,0],[94.001,0],[94.001,-12.306],[99.744,0],[104.113,0],[104.113,-14.829],[101.159,-14.829],[101.159,-3.159],[95.601,-14.829],[91.007,-14.829]],"c":true},"ix":2},"nm":"n","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"n","np":3,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[106.893,0],[109.497,0],[109.497,-2.728],[106.893,-2.728]],"c":true},"ix":2},"nm":".","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":".","np":3,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[3.241,0],[0,-4.697],[-5.107,0],[-1.313,1.354],[-0.062,0.882],[0,0],[1.333,0],[0,0.882],[-2.359,0],[-0.062,-0.513]],"o":[[0,-2.954],[-4.164,0],[0,3.671],[1.354,0],[1.19,-1.231],[0,0],[-0.062,1.969],[-3.097,0],[0,-3.056],[2.154,0],[0,0]],"v":[[124.04,-9.989],[118.523,-14.911],[111.734,-7.097],[118.502,0.349],[122.83,-1.313],[124.163,-4.799],[120.82,-4.799],[118.154,-1.99],[115.057,-7.076],[118.4,-12.593],[120.8,-9.989]],"c":true},"ix":2},"nm":"c","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"c","np":3,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[125.482,-7.199],[127.308,-1.661],[132.558,0.369],[137.829,-1.743],[139.696,-7.548],[137.809,-13.127],[132.599,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[128.805,-7.302],[129.892,-11.609],[132.62,-12.86],[136.004,-10.235],[136.373,-7.589],[135.368,-3.343],[132.599,-1.969],[129.912,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[141.696,0],[144.67,0],[144.67,-12.716],[148.629,0],[151.254,0],[155.295,-12.716],[155.295,0],[158.453,0],[158.453,-14.829],[153.408,-14.829],[150.024,-4.041],[146.885,-14.829],[141.696,-14.829]],"c":true},"ix":2},"nm":"m","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"m","np":3,"cix":2,"bm":0,"ix":12,"mn":"ADBE Vector Group","hd":false}],"ip":2.5,"op":25,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":3,"nm":"02092020","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-105,15,0],"ix":2,"l":2},"a":{"a":0,"k":[60,60,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"02092020002","np":3,"mn":"ADBE Checkbox Control","ix":1,"en":1,"ef":[{"ty":7,"nm":"Checkbox","mn":"ADBE Checkbox Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"Color & Stroke Change","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-55,-102,0],"ix":2,"l":2,"x":"var $bm_rt;\n$bm_rt = effect('Axis')('Point');"},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2,"x":"var $bm_rt;\nvar temp;\ntemp = effect('Scale')('Slider');\n$bm_rt = [\n temp,\n temp\n];"}},"ao":0,"ef":[{"ty":5,"nm":"Primary","np":3,"mn":"ADBE Color Control","ix":1,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]},{"ty":5,"nm":"Axis","np":3,"mn":"ADBE Point Control","ix":2,"en":1,"ef":[{"ty":3,"nm":"Point","mn":"ADBE Point Control-0001","ix":1,"v":{"a":0,"k":[250,250],"ix":1}}]},{"ty":5,"nm":"Scale","np":3,"mn":"ADBE Slider Control","ix":3,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":100,"ix":1}}]},{"ty":5,"nm":"State-Intro","np":3,"mn":"ADBE Slider Control","ix":4,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]},{"ty":5,"nm":"State-Hover","np":3,"mn":"ADBE Slider Control","ix":5,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":1,"ix":1}}]}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"in-verified","parent":3,"refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Intro')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":71,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"hover-verified","parent":3,"refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Hover')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":71,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/frontend/public/lotties/system-outline-165-view-carousel.json b/frontend/public/lotties/system-outline-165-view-carousel.json deleted file mode 100644 index 9cbde6944..000000000 --- a/frontend/public/lotties/system-outline-165-view-carousel.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.8.1","fr":60,"ip":0,"op":61,"w":500,"h":500,"nm":"165-view-carousel-outline","ddd":0,"assets":[{"id":"comp_0","nm":"in-carousel","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[2.856,0],[0,0],[0,0],[0,0],[0,-2.856]],"o":[[0,2.862],[0,0],[0,0],[0,0],[2.856,0],[0,0]],"v":[[177.06,88.542],[171.88,93.732],[125.028,93.732],[125.028,-93.718],[171.88,-93.718],[177.06,-88.538]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[2.858,0],[0,0],[0,2.858],[0,0],[0,0.013],[0,0.013],[0,0],[0,0.013],[0,0.013],[0,0],[-2.858,0],[0,0],[0,-2.858]],"o":[[0,2.858],[0,0],[-2.858,0],[0,0],[0,-0.013],[0,-0.013],[0,0],[0,-0.013],[0,-0.013],[0,0],[0,-2.858],[0,0],[2.858,0],[0,0]],"v":[[93.728,130.212],[88.544,135.395],[-88.538,135.395],[-93.722,130.212],[-93.722,109.421],[-93.72,109.382],[-93.722,109.343],[-93.722,-109.329],[-93.72,-109.368],[-93.722,-109.407],[-93.722,-130.206],[-88.538,-135.39],[88.544,-135.39],[93.728,-130.206]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,2.862],[0,0],[-2.861,0],[0,0],[0,0]],"o":[[-2.861,0],[0,0],[0,-2.856],[0,0],[0,0],[0,0]],"v":[[-171.87,93.732],[-177.06,88.542],[-177.06,-88.538],[-171.87,-93.718],[-125.022,-93.718],[-125.022,93.732]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[20.115,0],[0,0],[0,0],[20.117,0],[0,0],[0,-20.117],[0,0],[0,0],[0,-20.115],[0,0],[-20.12,0],[0,0],[0,0],[-20.117,0],[0,0],[0,20.117],[0,0],[0,0],[0,20.121],[0,0]],"o":[[0,0],[0,0],[0,-20.117],[0,0],[-20.117,0],[0,0],[0,0],[-20.12,0],[0,0],[0,20.121],[0,0],[0,0],[0,20.117],[0,0],[20.117,0],[0,0],[0,0],[20.115,0],[0,0],[0,-20.115]],"v":[[171.88,-125.018],[125.028,-125.018],[125.028,-130.206],[88.544,-166.69],[-88.538,-166.69],[-125.022,-130.206],[-125.022,-125.018],[-171.87,-125.018],[-208.36,-88.538],[-208.36,88.542],[-171.87,125.032],[-125.022,125.032],[-125.022,130.212],[-88.538,166.695],[88.544,166.695],[125.028,130.212],[125.028,125.032],[171.88,125.032],[208.36,88.542],[208.36,-88.538]],"c":true},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('165-view-carousel-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":47,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.003,250.003,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.17,"y":1},"o":{"x":0.167,"y":0.167},"t":1,"s":[{"i":[[11.506,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.506,0],[0,0],[0,-11.506]],"v":[[0.007,-5.179],[-0.007,-5.179],[-20.841,15.655],[-20.841,183.184],[-0.007,204.018],[0.007,204.018],[20.841,183.184],[20.841,15.655]],"c":true}]},{"t":30,"s":[{"i":[[11.506,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.506,0],[0,0],[0,-11.506]],"v":[[88.541,-151.042],[-88.541,-151.042],[-109.375,-130.209],[-109.375,130.209],[-88.541,151.042],[88.541,151.042],[109.375,130.209],[109.375,-130.209]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('165-view-carousel-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":1,"op":47,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[158.997,250.003,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.17,"y":1},"o":{"x":0.167,"y":0.167},"t":1,"s":[{"i":[[11.506,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.506,0],[0,0],[0,-11.506]],"v":[[-92.066,-3.857],[-92.368,-3.749],[-113.202,17.084],[-113.454,185.46],[-92.621,206.293],[-92.319,206.186],[-71.485,185.352],[-71.233,16.976]],"c":true}]},{"t":47,"s":[{"i":[[11.506,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.506,0],[0,0],[0,-11.506]],"v":[[81.291,-108.915],[-33.291,-108.915],[-54.125,-88.081],[-54.125,89.003],[-33.291,109.836],[81.291,109.836],[102.125,89.003],[102.125,-88.081]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('165-view-carousel-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.688],"y":[0.857]},"o":{"x":[0.364],"y":[0.014]},"t":7,"s":[21]},{"i":{"x":[0.618],"y":[0.11]},"o":{"x":[0.323],"y":[0.82]},"t":9,"s":[25.025]},{"i":{"x":[0.576],"y":[0.802]},"o":{"x":[0.263],"y":[0.837]},"t":13,"s":[26.481]},{"i":{"x":[0.462],"y":[1]},"o":{"x":[0.176],"y":[-0.167]},"t":18,"s":[27.812]},{"t":47,"s":[24]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.17],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":7,"s":[63]},{"t":47,"s":[74]}],"ix":2},"o":{"a":0,"k":122,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":7,"op":47,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[364.587,250.003,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.17,"y":1},"o":{"x":0.167,"y":0.167},"t":1,"s":[{"i":[[11.506,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.506,0],[0,0],[0,-11.506]],"v":[[-114.066,-3.901],[-114.368,-3.793],[-135.202,17.041],[-135.454,185.417],[-114.621,206.25],[-114.319,206.142],[-93.485,185.309],[-93.233,16.933]],"c":true}]},{"t":47,"s":[{"i":[[11.506,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.506,0],[0,0],[0,-11.506]],"v":[[57.291,-109.376],[-57.291,-109.376],[-78.125,-88.542],[-78.125,88.542],[-57.291,109.376],[57.291,109.376],[78.125,88.542],[78.125,-88.542]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('165-view-carousel-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.688],"y":[0.815]},"o":{"x":[0.364],"y":[0.028]},"t":7,"s":[19]},{"i":{"x":[0.618],"y":[0.54]},"o":{"x":[0.323],"y":[0.627]},"t":9,"s":[23.609]},{"i":{"x":[0.576],"y":[3.937]},"o":{"x":[0.263],"y":[-5.335]},"t":13,"s":[26.437]},{"i":{"x":[0.462],"y":[1]},"o":{"x":[0.176],"y":[-6.536]},"t":18,"s":[26.227]},{"t":47,"s":[26]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.17],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":7,"s":[62]},{"t":47,"s":[74]}],"ix":2},"o":{"a":0,"k":121,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":7,"op":47,"st":0,"bm":0}]},{"id":"comp_1","nm":"hover-carousel","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[2.856,0],[0,0],[0,0],[0,0],[0,-2.856]],"o":[[0,2.862],[0,0],[0,0],[0,0],[2.856,0],[0,0]],"v":[[177.06,88.542],[171.88,93.732],[125.028,93.732],[125.028,-93.718],[171.88,-93.718],[177.06,-88.538]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[2.858,0],[0,0],[0,2.858],[0,0],[0,0.013],[0,0.013],[0,0],[0,0.013],[0,0.013],[0,0],[-2.858,0],[0,0],[0,-2.858]],"o":[[0,2.858],[0,0],[-2.858,0],[0,0],[0,-0.013],[0,-0.013],[0,0],[0,-0.013],[0,-0.013],[0,0],[0,-2.858],[0,0],[2.858,0],[0,0]],"v":[[93.728,130.212],[88.544,135.395],[-88.538,135.395],[-93.722,130.212],[-93.722,109.421],[-93.72,109.382],[-93.722,109.343],[-93.722,-109.329],[-93.72,-109.368],[-93.722,-109.407],[-93.722,-130.206],[-88.538,-135.39],[88.544,-135.39],[93.728,-130.206]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,2.862],[0,0],[-2.861,0],[0,0],[0,0]],"o":[[-2.861,0],[0,0],[0,-2.856],[0,0],[0,0],[0,0]],"v":[[-171.87,93.732],[-177.06,88.542],[-177.06,-88.538],[-171.87,-93.718],[-125.022,-93.718],[-125.022,93.732]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[20.115,0],[0,0],[0,0],[20.117,0],[0,0],[0,-20.117],[0,0],[0,0],[0,-20.115],[0,0],[-20.12,0],[0,0],[0,0],[-20.117,0],[0,0],[0,20.117],[0,0],[0,0],[0,20.121],[0,0]],"o":[[0,0],[0,0],[0,-20.117],[0,0],[-20.117,0],[0,0],[0,0],[-20.12,0],[0,0],[0,20.121],[0,0],[0,0],[0,20.117],[0,0],[20.117,0],[0,0],[0,0],[20.115,0],[0,0],[0,-20.115]],"v":[[171.88,-125.018],[125.028,-125.018],[125.028,-130.206],[88.544,-166.69],[-88.538,-166.69],[-125.022,-130.206],[-125.022,-125.018],[-171.87,-125.018],[-208.36,-88.538],[-208.36,88.542],[-171.87,125.032],[-125.022,125.032],[-125.022,130.212],[-88.538,166.695],[88.544,166.695],[125.028,130.212],[125.028,125.032],[171.88,125.032],[208.36,88.542],[208.36,-88.538]],"c":true},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('165-view-carousel-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('165-view-carousel-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":55,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[2.856,0],[0,0],[0,0],[0,0],[0,-2.856]],"o":[[0,2.862],[0,0],[0,0],[0,0],[2.856,0],[0,0]],"v":[[177.06,88.542],[171.88,93.732],[125.028,93.732],[125.028,-93.718],[171.88,-93.718],[177.06,-88.538]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[2.858,0],[0,0],[0,2.858],[0,0],[0,0.013],[0,0.013],[0,0],[0,0.013],[0,0.013],[0,0],[-2.858,0],[0,0],[0,-2.858]],"o":[[0,2.858],[0,0],[-2.858,0],[0,0],[0,-0.013],[0,-0.013],[0,0],[0,-0.013],[0,-0.013],[0,0],[0,-2.858],[0,0],[2.858,0],[0,0]],"v":[[93.728,130.212],[88.544,135.395],[-88.538,135.395],[-93.722,130.212],[-93.722,109.421],[-93.72,109.382],[-93.722,109.343],[-93.722,-109.329],[-93.72,-109.368],[-93.722,-109.407],[-93.722,-130.206],[-88.538,-135.39],[88.544,-135.39],[93.728,-130.206]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,2.862],[0,0],[-2.861,0],[0,0],[0,0]],"o":[[-2.861,0],[0,0],[0,-2.856],[0,0],[0,0],[0,0]],"v":[[-171.87,93.732],[-177.06,88.542],[-177.06,-88.538],[-171.87,-93.718],[-125.022,-93.718],[-125.022,93.732]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[20.115,0],[0,0],[0,0],[20.117,0],[0,0],[0,-20.117],[0,0],[0,0],[0,-20.115],[0,0],[-20.12,0],[0,0],[0,0],[-20.117,0],[0,0],[0,20.117],[0,0],[0,0],[0,20.121],[0,0]],"o":[[0,0],[0,0],[0,-20.117],[0,0],[-20.117,0],[0,0],[0,0],[-20.12,0],[0,0],[0,20.121],[0,0],[0,0],[0,20.117],[0,0],[20.117,0],[0,0],[0,0],[20.115,0],[0,0],[0,-20.115]],"v":[[171.88,-125.018],[125.028,-125.018],[125.028,-130.206],[88.544,-166.69],[-88.538,-166.69],[-125.022,-130.206],[-125.022,-125.018],[-171.87,-125.018],[-208.36,-88.538],[-208.36,88.542],[-171.87,125.032],[-125.022,125.032],[-125.022,130.212],[-88.538,166.695],[88.544,166.695],[125.028,130.212],[125.028,125.032],[171.88,125.032],[208.36,88.542],[208.36,-88.538]],"c":true},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('165-view-carousel-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('165-view-carousel-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.77,"y":0},"t":1,"s":[158.997,250.003,0],"to":[-11.333,0,0],"ti":[-15.167,0,0]},{"i":{"x":0.159,"y":1},"o":{"x":0.407,"y":0},"t":17,"s":[90.997,250.003,0],"to":[7.313,0,0],"ti":[-13.722,0,0]},{"t":55,"s":[249.997,250.003,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"t":1,"s":[{"i":[[11.506,0],[0,0],[0,0],[3.77,-3.77],[0,-5.753],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0]],"o":[[0,0],[0,0],[-5.753,0],[-3.77,3.77],[0,0],[0,11.505],[0,0],[11.506,0],[0,0],[0,-11.506]],"v":[[81.291,-108.915],[24,-108.915],[-33.291,-108.915],[-48.023,-102.813],[-54.125,-88.081],[-54.125,89.003],[-33.291,109.836],[81.291,109.836],[102.125,89.003],[102.125,-88.081]],"c":true}],"h":1},{"i":{"x":0.833,"y":0.859},"o":{"x":0.333,"y":0},"t":2.18,"s":[{"i":[[-10.5,0],[-33,0],[0,0],[0,-11.506],[0,0],[10.599,-0.506],[31.5,0],[0,0],[0,9.674],[0,0]],"o":[[0,0],[30.33,0],[14.039,-0.421],[0,0],[0,11.505],[0,0],[-32,0],[-10,-0.837],[0,0],[0,-9.675]],"v":[[-34.003,-109.325],[18.497,-109.489],[79.459,-109.409],[100.997,-87.576],[100.997,88.342],[80.898,109.181],[18.497,109.193],[-34.503,109.76],[-55.503,92.484],[-55.503,-91.976]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.141},"t":4.539,"s":[{"i":[[-10.707,0.102],[-33.475,1.617],[0,0],[0,-11.506],[0,0],[11.602,0.847],[31.916,1.156],[0,0],[0,9.674],[0,0]],"o":[[0,0],[30.272,-1.173],[14.232,-0.984],[0,0],[0,11.505],[0,0],[-32.538,-0.824],[-10.206,-0.926],[0,0],[0,-9.675]],"v":[[-32.567,-109.991],[23.211,-112.208],[83.578,-115.094],[105.014,-93.835],[105.196,92.407],[84.952,115.024],[22.77,112.067],[-33.068,110.27],[-53.988,92.985],[-53.988,-92.637]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-11.32,0.404],[-29.19,6.404],[0,0],[0,-11.506],[0,0],[12.597,4.854],[27.72,4.58],[0,0],[0,9.673],[0,0]],"o":[[0,0],[24.953,-4.645],[12.385,-2.651],[0,0],[0,11.505],[0,0],[-28.6,-3.263],[-10.817,-1.188],[0,0],[0,-9.675]],"v":[[-28.317,-111.962],[31.476,-120.26],[79.82,-131.925],[97.307,-112.367],[97.997,104.443],[80.768,132.323],[29.803,120.575],[-28.819,111.778],[-49.503,94.468],[-49.503,-94.593]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[-9.013,0.807],[-17.317,12.809],[0,0],[0,-11.506],[0,0],[11.116,10.214],[16.283,9.16],[0,0],[0,9.673],[0,0]],"o":[[0,0],[12.683,-9.289],[7.31,-4.881],[0,0],[0,11.505],[0,0],[-17.3,-6.527],[-8.646,-1.539],[0,0],[0,-9.675]],"v":[[7.243,-114.557],[57.814,-130.99],[80.187,-154.399],[88.793,-137.117],[89.982,120.584],[80.382,155.507],[54.93,131.999],[6.876,113.837],[-10.503,96.493],[-10.503,-97.168]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[-7.525,1.615],[-1.633,25.617],[0,0],[0,-11.506],[0,0],[-6.935,3.293],[1.067,18.321],[0,0],[0,9.674],[0,0]],"o":[[0,0],[-9.023,-16.317],[-6.728,1.014],[0,0],[0,11.505],[0,0],[-2.6,-13.053],[-7.292,-2.239],[0,0],[0,-9.675]],"v":[[0.489,-118.579],[59.13,-154.283],[32.725,-168.179],[22.399,-155.812],[24.967,153.672],[41.432,165.042],[58.93,144.014],[0.256,119.124],[-13.503,101.711],[-13.503,-101.15]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":19.873,"s":[{"i":[[-6.038,2.422],[8.3,17.524],[0,0],[0,-11.506],[0,0],[-6.249,-3.706],[-24.4,30.92],[0,0],[0,9.674],[0,0]],"o":[[0,0],[-28.7,-24.476],[-7.161,4.779],[0,0],[0,11.505],[0,0],[12.1,-19.58],[-5.938,-2.94],[0,0],[0,-9.675]],"v":[[25.735,-123.553],[61.197,-154.527],[-29.141,-161.911],[-41.9,-139.278],[-36.9,138.74],[-24.654,160.375],[60.897,155.077],[25.635,123.459],[15.497,105.977],[15.497,-106.084]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[-4.922,3.028],[15.827,9.377],[0,0],[0,-11.506],[0,0],[-10.461,-4.506],[-34,22.494],[0,0],[0,9.674],[0,0]],"o":[[0,0],[-37,-21.921],[-12.461,6.079],[0,0],[0,11.505],[0,0],[24.637,-16.3],[-4.922,-3.466],[0,0],[0,-9.675]],"v":[[42.419,-127.031],[57.997,-160.082],[-64.541,-153.082],[-85.875,-130.248],[-85.875,130.169],[-65.541,152.003],[54.497,161.003],[42.419,126.963],[34.997,109.43],[34.997,-109.531]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-1.776,2.595],[23.373,8.037],[0,0],[0,-11.506],[0,0],[-10.611,-3.862],[-41.925,18.346],[0,0],[0,9.935],[0,0]],"o":[[0,0],[-44.2,-18.355],[-12.325,5.211],[0,0],[0,11.505],[0,0],[31.73,-13.971],[-1.776,-2.971],[0,0],[0,-9.936]],"v":[[58.47,-130.461],[59.054,-159.208],[-67.97,-152.791],[-89.232,-130.243],[-89.232,130.175],[-68.827,151.866],[56.422,159.163],[58.47,130.403],[56.711,112.398],[56.711,-112.485]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":31,"s":[{"i":[[6.089,1.514],[42.238,4.688],[0,0],[0,-11.506],[0,0],[-10.984,-2.253],[-61.736,7.974],[0,0],[0,10.589],[0,0]],"o":[[0,0],[-62.199,-9.441],[-11.984,3.04],[0,0],[0,11.505],[0,0],[49.46,-8.15],[6.089,-1.733],[0,0],[0,-10.591]],"v":[[98.595,-139.037],[30.196,-155.562],[-76.541,-152.062],[-97.625,-130.229],[-97.625,130.189],[-77.041,151.523],[29.733,156.023],[98.595,139.003],[110.997,119.819],[110.997,-119.87]],"c":true}]},{"i":{"x":0.25,"y":1},"o":{"x":0.167,"y":0.167},"t":34.428,"s":[{"i":[[8.754,0.899],[38.858,0.619],[0,0],[0,-11.506],[0,0],[-11.208,-1.286],[-49.166,1.112],[0,0],[0,11.425],[0,0]],"o":[[0,0],[-47.257,-0.753],[-11.779,1.735],[0,0],[0,11.505],[0,0],[44.834,-0.888],[8.754,-1.029],[0,0],[0,-11.427]],"v":[[93.957,-150.003],[16.803,-153.622],[-81.693,-151.624],[-102.669,-130.22],[-102.669,130.198],[-81.978,151.316],[17.827,153.885],[93.957,149.984],[110.624,129.29],[110.624,-129.319]],"c":true}]},{"t":47.28515625,"s":[{"i":[[11.506,0],[29.848,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[-30.18,0],[0,0],[0,11.505],[0,0]],"o":[[0,0],[-29.179,0],[-11.506,0],[0,0],[0,11.505],[0,0],[28.848,0],[11.506,0],[0,0],[0,-11.506]],"v":[[88.541,-151.042],[-1.003,-151.042],[-88.541,-151.042],[-109.375,-130.209],[-109.375,130.209],[-88.541,151.042],[1.997,151.042],[88.541,151.042],[109.375,130.209],[109.375,-130.209]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('165-view-carousel-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"t":1,"s":[24],"h":1},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":3,"s":[24]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":7,"s":[17]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":8,"s":[15]},{"t":9,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"t":1,"s":[74],"h":1},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":3,"s":[74]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":7,"s":[83]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":8,"s":[85]},{"t":9,"s":[100]}],"ix":2},"o":{"a":1,"k":[{"t":1,"s":[122],"h":1},{"t":3,"s":[302],"h":1}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('165-view-carousel-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":29,"op":55,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[250.003,250.003,0],"to":[19.333,0,0],"ti":[-19.333,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0.333},"t":17.514,"s":[366.003,250.003,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.23,"y":1},"o":{"x":0.167,"y":0.167},"t":30.486,"s":[366.003,250.003,0],"to":[-4.167,0,0],"ti":[4.167,0,0]},{"t":47,"s":[341.003,250.003,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14,"s":[{"i":[[0,-11.506],[0,0]],"o":[[0,0],[0,11.505]],"v":[[71.259,-144.472],[71.259,140.945]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":17,"s":[{"i":[[0,-11.506],[0,0]],"o":[[0,0],[0,11.505]],"v":[[63.259,-151.942],[64.259,148.476]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[0,-11.506],[0,0]],"o":[[0,0],[0,11.505]],"v":[[63.259,-151.942],[61.259,148.476]],"c":false}]},{"t":28,"s":[{"i":[[0,-11.506],[0,0]],"o":[[0,0],[0,11.505]],"v":[[48.259,-151.881],[51.259,147.537]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('165-view-carousel-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('165-view-carousel-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":13,"op":29,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[250.003,250.003,0],"to":[19.333,0,0],"ti":[-19.333,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0.333},"t":17.514,"s":[366.003,250.003,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.23,"y":1},"o":{"x":0.167,"y":0.167},"t":30.486,"s":[366.003,250.003,0],"to":[-4.167,0,0],"ti":[4.167,0,0]},{"t":47,"s":[341.003,250.003,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[11.506,0],[29.848,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[-30.18,0],[0,0],[0,11.505],[0,0]],"o":[[0,0],[-29.179,0],[-11.506,0],[0,0],[0,11.505],[0,0],[28.848,0],[11.506,0],[0,0],[0,-11.506]],"v":[[88.541,-151.042],[-1.003,-151.042],[-88.541,-151.042],[-109.375,-130.209],[-109.375,130.209],[-88.541,151.042],[1.997,151.042],[88.541,151.042],[109.375,130.209],[109.375,-130.209]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6.896,"s":[{"i":[[7.637,1.081],[38.698,3.349],[0,0],[0,-11.506],[0,0],[-11.133,-1.609],[-52.72,5.696],[0,0],[0,10.851],[0,0]],"o":[[0,0],[-52.765,-6.743],[-11.848,2.171],[0,0],[0,11.505],[0,0],[43.571,-5.821],[7.637,-1.238],[0,0],[0,-10.852]],"v":[[106.723,-142.676],[20.996,-159.271],[-79.97,-151.771],[-100.982,-130.223],[-100.982,130.195],[-80.327,151.385],[22.05,158.6],[106.723,142.233],[121.534,122.578],[121.534,-123.033]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":9.256,"s":[{"i":[[6.089,1.514],[42.238,4.688],[0,0],[0,-11.506],[0,0],[-10.984,-2.253],[-61.736,7.974],[0,0],[0,10.589],[0,0]],"o":[[0,0],[-62.199,-9.441],[-11.984,3.04],[0,0],[0,11.505],[0,0],[49.46,-8.15],[6.089,-1.733],[0,0],[0,-10.591]],"v":[[98.595,-139.037],[30.196,-155.562],[-76.541,-152.062],[-97.625,-130.229],[-97.625,130.189],[-77.041,151.523],[29.733,156.023],[98.595,139.003],[110.997,119.819],[110.997,-119.87]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15.154,"s":[{"i":[[-1.776,2.595],[23.373,8.037],[0,0],[0,-11.506],[0,0],[-10.611,-3.862],[-41.925,18.346],[0,0],[0,9.935],[0,0]],"o":[[0,0],[-44.2,-18.355],[-12.325,5.211],[0,0],[0,11.505],[0,0],[31.73,-13.971],[-1.776,-2.971],[0,0],[0,-9.936]],"v":[[58.47,-130.461],[59.054,-159.208],[-67.97,-152.791],[-89.232,-130.243],[-89.232,130.175],[-68.827,151.866],[56.422,159.163],[58.47,130.403],[56.711,112.398],[56.711,-112.485]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":17.514,"s":[{"i":[[-4.922,3.028],[15.827,9.377],[0,0],[0,-11.506],[0,0],[-10.461,-4.506],[-34,22.494],[0,0],[0,9.674],[0,0]],"o":[[0,0],[-37,-21.921],[-12.461,6.079],[0,0],[0,11.505],[0,0],[24.637,-16.3],[-4.922,-3.466],[0,0],[0,-9.675]],"v":[[42.419,-127.031],[57.997,-160.082],[-64.541,-153.082],[-85.875,-130.248],[-85.875,130.169],[-65.541,152.003],[54.497,161.003],[42.419,126.963],[34.997,109.43],[34.997,-109.531]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":21.051,"s":[{"i":[[-6.038,2.422],[8.3,17.524],[0,0],[0,-11.506],[0,0],[-6.249,-3.706],[-24.4,30.92],[0,0],[0,9.674],[0,0]],"o":[[0,0],[-28.7,-24.476],[-7.161,4.779],[0,0],[0,11.505],[0,0],[12.1,-19.58],[-5.938,-2.94],[0,0],[0,-9.675]],"v":[[25.735,-123.553],[61.197,-154.527],[-29.141,-161.911],[-41.9,-139.278],[-36.9,138.74],[-24.654,160.375],[60.897,155.077],[25.635,123.459],[15.497,105.977],[15.497,-106.084]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25.77,"s":[{"i":[[-7.525,1.615],[-1.633,25.617],[0,0],[0,-11.506],[0,0],[-6.935,3.293],[1.067,18.321],[0,0],[0,9.674],[0,0]],"o":[[0,0],[-9.023,-16.317],[-6.728,1.014],[0,0],[0,11.505],[0,0],[-2.6,-13.053],[-7.292,-2.239],[0,0],[0,-9.675]],"v":[[3.489,-118.917],[62.13,-154.62],[35.725,-168.517],[25.399,-156.15],[27.967,153.335],[44.432,164.704],[61.93,143.676],[3.256,118.787],[-10.503,101.373],[-10.503,-101.487]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30.486,"s":[{"i":[[-9.013,0.807],[-17.317,12.809],[0,0],[0,-11.506],[0,0],[11.116,10.214],[16.283,9.16],[0,0],[0,9.673],[0,0]],"o":[[0,0],[12.683,-9.289],[7.31,-4.881],[0,0],[0,11.505],[0,0],[-17.3,-6.527],[-8.646,-1.539],[0,0],[0,-9.675]],"v":[[-18.757,-114.28],[31.814,-130.714],[54.187,-154.122],[62.793,-136.84],[63.982,120.861],[54.382,155.784],[28.93,132.276],[-19.124,114.114],[-36.503,96.77],[-36.503,-96.891]],"c":true}]},{"i":{"x":0.833,"y":0.859},"o":{"x":0.167,"y":0.167},"t":32.846,"s":[{"i":[[-11.32,0.404],[-29.19,6.404],[0,0],[0,-11.506],[0,0],[12.597,4.854],[27.72,4.58],[0,0],[0,9.673],[0,0]],"o":[[0,0],[24.953,-4.645],[12.385,-2.651],[0,0],[0,11.505],[0,0],[-28.6,-3.263],[-10.817,-1.188],[0,0],[0,-9.675]],"v":[[-28.317,-111.962],[31.476,-120.26],[79.82,-131.925],[97.307,-112.367],[97.997,104.443],[80.768,132.323],[29.803,120.575],[-28.819,111.778],[-49.503,94.468],[-49.503,-94.593]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.141},"t":36.385,"s":[{"i":[[-10.707,0.102],[-33.475,1.617],[0,0],[0,-11.506],[0,0],[11.602,0.847],[31.916,1.156],[0,0],[0,9.674],[0,0]],"o":[[0,0],[30.272,-1.173],[14.232,-0.984],[0,0],[0,11.505],[0,0],[-32.538,-0.824],[-10.206,-0.926],[0,0],[0,-9.675]],"v":[[-32.567,-109.991],[23.211,-112.208],[83.578,-115.094],[105.014,-93.835],[105.196,92.407],[84.952,115.024],[22.77,112.067],[-33.068,110.27],[-53.988,92.985],[-53.988,-92.637]],"c":true}]},{"t":38.744,"s":[{"i":[[-10.5,0],[-33,0],[0,0],[0,-11.506],[0,0],[10.599,-0.506],[31.5,0],[0,0],[0,9.674],[0,0]],"o":[[0,0],[30.33,0],[14.039,-0.421],[0,0],[0,11.505],[0,0],[-32,0],[-10,-0.837],[0,0],[0,-9.675]],"v":[[-34.003,-109.325],[18.497,-109.489],[79.459,-109.409],[100.997,-87.576],[100.997,88.342],[80.898,109.181],[18.497,109.193],[-34.503,109.76],[-55.503,92.484],[-55.503,-91.976]],"c":true}],"h":1},{"t":39.923828125,"s":[{"i":[[11.506,0],[0,0],[0,0],[3.77,-3.77],[0,-5.753],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0]],"o":[[0,0],[0,0],[-5.753,0],[-3.77,3.77],[0,0],[0,11.505],[0,0],[11.506,0],[0,0],[0,-11.506]],"v":[[81.291,-108.915],[24,-108.915],[-33.291,-108.915],[-48.023,-102.813],[-54.125,-88.081],[-54.125,89.003],[-33.291,109.836],[81.291,109.836],[102.125,89.003],[102.125,-88.081]],"c":true}],"h":1}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('165-view-carousel-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"t":33,"s":[0],"h":1},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":34,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":39,"s":[5]},{"i":{"x":[0.569],"y":[0.704]},"o":{"x":[0.203],"y":[0.107]},"t":40,"s":[5]},{"i":{"x":[0.703],"y":[1]},"o":{"x":[0.317],"y":[0.743]},"t":45,"s":[8.155]},{"t":55,"s":[10]}],"ix":1},"e":{"a":1,"k":[{"t":33,"s":[100],"h":1},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":34,"s":[68]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":39,"s":[62]},{"i":{"x":[0.569],"y":[0.83]},"o":{"x":[0.203],"y":[0.062]},"t":40,"s":[62]},{"i":{"x":[0.703],"y":[1]},"o":{"x":[0.317],"y":[-2.808]},"t":45,"s":[58.707]},{"t":55,"s":[59]}],"ix":2},"o":{"a":1,"k":[{"t":39,"s":[0],"h":1},{"t":40,"s":[-182],"h":1}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('165-view-carousel-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":29,"op":55,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[250.003,250.003,0],"to":[19.333,0,0],"ti":[-19.333,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0.333},"t":17.514,"s":[366.003,250.003,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.23,"y":1},"o":{"x":0.167,"y":0.167},"t":30.486,"s":[366.003,250.003,0],"to":[-4.167,0,0],"ti":[4.167,0,0]},{"t":47,"s":[341.003,250.003,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[11.506,0],[29.848,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[-30.18,0],[0,0],[0,11.505],[0,0]],"o":[[0,0],[-29.179,0],[-11.506,0],[0,0],[0,11.505],[0,0],[28.848,0],[11.506,0],[0,0],[0,-11.506]],"v":[[88.541,-151.042],[-1.003,-151.042],[-88.541,-151.042],[-109.375,-130.209],[-109.375,130.209],[-88.541,151.042],[1.997,151.042],[88.541,151.042],[109.375,130.209],[109.375,-130.209]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6.896,"s":[{"i":[[7.637,1.081],[38.698,3.349],[0,0],[0,-11.506],[0,0],[-11.133,-1.609],[-52.72,5.696],[0,0],[0,10.851],[0,0]],"o":[[0,0],[-52.765,-6.743],[-11.848,2.171],[0,0],[0,11.505],[0,0],[43.571,-5.821],[7.637,-1.238],[0,0],[0,-10.852]],"v":[[106.723,-142.676],[20.996,-159.271],[-79.97,-151.771],[-100.982,-130.223],[-100.982,130.195],[-80.327,151.385],[22.05,158.6],[106.723,142.233],[121.534,122.578],[121.534,-123.033]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":9.256,"s":[{"i":[[6.089,1.514],[42.238,4.688],[0,0],[0,-11.506],[0,0],[-10.984,-2.253],[-61.736,7.974],[0,0],[0,10.589],[0,0]],"o":[[0,0],[-62.199,-9.441],[-11.984,3.04],[0,0],[0,11.505],[0,0],[49.46,-8.15],[6.089,-1.733],[0,0],[0,-10.591]],"v":[[98.595,-139.037],[30.196,-155.562],[-76.541,-152.062],[-97.625,-130.229],[-97.625,130.189],[-77.041,151.523],[29.733,156.023],[98.595,139.003],[110.997,119.819],[110.997,-119.87]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15.154,"s":[{"i":[[-1.776,2.595],[23.373,8.037],[0,0],[0,-11.506],[0,0],[-10.611,-3.862],[-41.925,18.346],[0,0],[0,9.935],[0,0]],"o":[[0,0],[-44.2,-18.355],[-12.325,5.211],[0,0],[0,11.505],[0,0],[31.73,-13.971],[-1.776,-2.971],[0,0],[0,-9.936]],"v":[[58.47,-130.461],[59.054,-159.208],[-67.97,-152.791],[-89.232,-130.243],[-89.232,130.175],[-68.827,151.866],[56.422,159.163],[58.47,130.403],[56.711,112.398],[56.711,-112.485]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":17.514,"s":[{"i":[[-4.922,3.028],[15.827,9.377],[0,0],[0,-11.506],[0,0],[-10.461,-4.506],[-34,22.494],[0,0],[0,9.674],[0,0]],"o":[[0,0],[-37,-21.921],[-12.461,6.079],[0,0],[0,11.505],[0,0],[24.637,-16.3],[-4.922,-3.466],[0,0],[0,-9.675]],"v":[[42.419,-127.031],[57.997,-160.082],[-64.541,-153.082],[-85.875,-130.248],[-85.875,130.169],[-65.541,152.003],[54.497,161.003],[42.419,126.963],[34.997,109.43],[34.997,-109.531]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":21.051,"s":[{"i":[[-6.038,2.422],[8.3,17.524],[0,0],[0,-11.506],[0,0],[-6.249,-3.706],[-24.4,30.92],[0,0],[0,9.674],[0,0]],"o":[[0,0],[-28.7,-24.476],[-7.161,4.779],[0,0],[0,11.505],[0,0],[12.1,-19.58],[-5.938,-2.94],[0,0],[0,-9.675]],"v":[[25.735,-123.553],[61.197,-154.527],[-29.141,-161.911],[-41.9,-139.278],[-36.9,138.74],[-24.654,160.375],[60.897,155.077],[25.635,123.459],[15.497,105.977],[15.497,-106.084]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25.77,"s":[{"i":[[-7.525,1.615],[-1.633,25.617],[0,0],[0,-11.506],[0,0],[-6.935,3.293],[1.067,18.321],[0,0],[0,9.674],[0,0]],"o":[[0,0],[-9.023,-16.317],[-6.728,1.014],[0,0],[0,11.505],[0,0],[-2.6,-13.053],[-7.292,-2.239],[0,0],[0,-9.675]],"v":[[3.489,-118.917],[62.13,-154.62],[35.725,-168.517],[25.399,-156.15],[27.967,153.335],[44.432,164.704],[61.93,143.676],[3.256,118.787],[-10.503,101.373],[-10.503,-101.487]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30.486,"s":[{"i":[[-9.013,0.807],[-17.317,12.809],[0,0],[0,-11.506],[0,0],[11.116,10.214],[16.283,9.16],[0,0],[0,9.673],[0,0]],"o":[[0,0],[12.683,-9.289],[7.31,-4.881],[0,0],[0,11.505],[0,0],[-17.3,-6.527],[-8.646,-1.539],[0,0],[0,-9.675]],"v":[[-18.757,-114.28],[31.814,-130.714],[54.187,-154.122],[62.793,-136.84],[63.982,120.861],[54.382,155.784],[28.93,132.276],[-19.124,114.114],[-36.503,96.77],[-36.503,-96.891]],"c":true}]},{"i":{"x":0.833,"y":0.859},"o":{"x":0.167,"y":0.167},"t":32.846,"s":[{"i":[[-11.32,0.404],[-29.19,6.404],[0,0],[0,-11.506],[0,0],[12.597,4.854],[27.72,4.58],[0,0],[0,9.673],[0,0]],"o":[[0,0],[24.953,-4.645],[12.385,-2.651],[0,0],[0,11.505],[0,0],[-28.6,-3.263],[-10.817,-1.188],[0,0],[0,-9.675]],"v":[[-28.317,-111.962],[31.476,-120.26],[79.82,-131.925],[97.307,-112.367],[97.997,104.443],[80.768,132.323],[29.803,120.575],[-28.819,111.778],[-49.503,94.468],[-49.503,-94.593]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.141},"t":36.385,"s":[{"i":[[-10.707,0.102],[-33.475,1.617],[0,0],[0,-11.506],[0,0],[11.602,0.847],[31.916,1.156],[0,0],[0,9.674],[0,0]],"o":[[0,0],[30.272,-1.173],[14.232,-0.984],[0,0],[0,11.505],[0,0],[-32.538,-0.824],[-10.206,-0.926],[0,0],[0,-9.675]],"v":[[-32.567,-109.991],[23.211,-112.208],[83.578,-115.094],[105.014,-93.835],[105.196,92.407],[84.952,115.024],[22.77,112.067],[-33.068,110.27],[-53.988,92.985],[-53.988,-92.637]],"c":true}]},{"t":38.744,"s":[{"i":[[-10.5,0],[-33,0],[0,0],[0,-11.506],[0,0],[10.599,-0.506],[31.5,0],[0,0],[0,9.674],[0,0]],"o":[[0,0],[30.33,0],[14.039,-0.421],[0,0],[0,11.505],[0,0],[-32,0],[-10,-0.837],[0,0],[0,-9.675]],"v":[[-34.003,-109.325],[18.497,-109.489],[79.459,-109.409],[100.997,-87.576],[100.997,88.342],[80.898,109.181],[18.497,109.193],[-34.503,109.76],[-55.503,92.484],[-55.503,-91.976]],"c":true}],"h":1},{"t":39.923828125,"s":[{"i":[[11.506,0],[0,0],[0,0],[3.77,-3.77],[0,-5.753],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0]],"o":[[0,0],[0,0],[-5.753,0],[-3.77,3.77],[0,0],[0,11.505],[0,0],[11.506,0],[0,0],[0,-11.506]],"v":[[81.291,-108.915],[24,-108.915],[-33.291,-108.915],[-48.023,-102.813],[-54.125,-88.081],[-54.125,89.003],[-33.291,109.836],[81.291,109.836],[102.125,89.003],[102.125,-88.081]],"c":true}],"h":1}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('165-view-carousel-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":12,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":13,"s":[2]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[2]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":21,"s":[5]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[8]},{"t":26,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":12,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":13,"s":[69]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[69]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":21,"s":[65]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[59]},{"t":26,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('165-view-carousel-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":29,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.77,"y":0},"t":1,"s":[158.997,250.003,0],"to":[-11.333,0,0],"ti":[-15.167,0,0]},{"i":{"x":0.159,"y":1},"o":{"x":0.407,"y":0},"t":17,"s":[90.997,250.003,0],"to":[7.313,0,0],"ti":[-13.722,0,0]},{"t":55,"s":[249.997,250.003,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[60.705,-147.133],[60.712,150.416]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":21,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[61.705,-148.133],[62.712,149.416]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[70.455,-148.133],[71.462,142.666]],"c":false}]},{"t":25,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[70.705,-148.133],[71.712,140.416]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('165-view-carousel-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('165-view-carousel-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":13,"op":26,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.77,"y":0},"t":1,"s":[158.997,250.003,0],"to":[-11.333,0,0],"ti":[-15.167,0,0]},{"i":{"x":0.159,"y":1},"o":{"x":0.407,"y":0},"t":17,"s":[90.997,250.003,0],"to":[7.313,0,0],"ti":[-13.722,0,0]},{"t":55,"s":[249.997,250.003,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"t":1,"s":[{"i":[[11.506,0],[0,0],[0,0],[3.77,-3.77],[0,-5.753],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0]],"o":[[0,0],[0,0],[-5.753,0],[-3.77,3.77],[0,0],[0,11.505],[0,0],[11.506,0],[0,0],[0,-11.506]],"v":[[81.291,-108.915],[24,-108.915],[-33.291,-108.915],[-48.023,-102.813],[-54.125,-88.081],[-54.125,89.003],[-33.291,109.836],[81.291,109.836],[102.125,89.003],[102.125,-88.081]],"c":true}],"h":1},{"i":{"x":0.833,"y":0.859},"o":{"x":0.333,"y":0},"t":2.18,"s":[{"i":[[-10.5,0],[-33,0],[0,0],[0,-11.506],[0,0],[10.599,-0.506],[31.5,0],[0,0],[0,9.674],[0,0]],"o":[[0,0],[30.33,0],[14.039,-0.421],[0,0],[0,11.505],[0,0],[-32,0],[-10,-0.837],[0,0],[0,-9.675]],"v":[[-34.003,-109.325],[18.497,-109.489],[79.459,-109.409],[100.997,-87.576],[100.997,88.342],[80.898,109.181],[18.497,109.193],[-34.503,109.76],[-55.503,92.484],[-55.503,-91.976]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.141},"t":4.539,"s":[{"i":[[-10.707,0.102],[-33.475,1.617],[0,0],[0,-11.506],[0,0],[11.602,0.847],[31.916,1.156],[0,0],[0,9.674],[0,0]],"o":[[0,0],[30.272,-1.173],[14.232,-0.984],[0,0],[0,11.505],[0,0],[-32.538,-0.824],[-10.206,-0.926],[0,0],[0,-9.675]],"v":[[-32.567,-109.991],[23.211,-112.208],[83.578,-115.094],[105.014,-93.835],[105.196,92.407],[84.952,115.024],[22.77,112.067],[-33.068,110.27],[-53.988,92.985],[-53.988,-92.637]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-11.32,0.404],[-29.19,6.404],[0,0],[0,-11.506],[0,0],[12.597,4.854],[27.72,4.58],[0,0],[0,9.673],[0,0]],"o":[[0,0],[24.953,-4.645],[12.385,-2.651],[0,0],[0,11.505],[0,0],[-28.6,-3.263],[-10.817,-1.188],[0,0],[0,-9.675]],"v":[[-28.317,-111.962],[31.476,-120.26],[79.82,-131.925],[97.307,-112.367],[97.997,104.443],[80.768,132.323],[29.803,120.575],[-28.819,111.778],[-49.503,94.468],[-49.503,-94.593]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[-9.013,0.807],[-17.317,12.809],[0,0],[0,-11.506],[0,0],[11.116,10.214],[16.283,9.16],[0,0],[0,9.673],[0,0]],"o":[[0,0],[12.683,-9.289],[7.31,-4.881],[0,0],[0,11.505],[0,0],[-17.3,-6.527],[-8.646,-1.539],[0,0],[0,-9.675]],"v":[[7.243,-114.557],[57.814,-130.99],[80.187,-154.399],[88.793,-137.117],[89.982,120.584],[80.382,155.507],[54.93,131.999],[6.876,113.837],[-10.503,96.493],[-10.503,-97.168]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[-7.525,1.615],[-1.633,25.617],[0,0],[0,-11.506],[0,0],[-6.935,3.293],[1.067,18.321],[0,0],[0,9.674],[0,0]],"o":[[0,0],[-9.023,-16.317],[-6.728,1.014],[0,0],[0,11.505],[0,0],[-2.6,-13.053],[-7.292,-2.239],[0,0],[0,-9.675]],"v":[[0.489,-118.579],[59.13,-154.283],[32.725,-168.179],[22.399,-155.812],[24.967,153.672],[41.432,165.042],[58.93,144.014],[0.256,119.124],[-13.503,101.711],[-13.503,-101.15]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":19.873,"s":[{"i":[[-6.038,2.422],[8.3,17.524],[0,0],[0,-11.506],[0,0],[-6.249,-3.706],[-24.4,30.92],[0,0],[0,9.674],[0,0]],"o":[[0,0],[-28.7,-24.476],[-7.161,4.779],[0,0],[0,11.505],[0,0],[12.1,-19.58],[-5.938,-2.94],[0,0],[0,-9.675]],"v":[[25.735,-123.553],[61.197,-154.527],[-29.141,-161.911],[-41.9,-139.278],[-36.9,138.74],[-24.654,160.375],[60.897,155.077],[25.635,123.459],[15.497,105.977],[15.497,-106.084]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[-4.922,3.028],[15.827,9.377],[0,0],[0,-11.506],[0,0],[-10.461,-4.506],[-34,22.494],[0,0],[0,9.674],[0,0]],"o":[[0,0],[-37,-21.921],[-12.461,6.079],[0,0],[0,11.505],[0,0],[24.637,-16.3],[-4.922,-3.466],[0,0],[0,-9.675]],"v":[[42.419,-127.031],[57.997,-160.082],[-64.541,-153.082],[-85.875,-130.248],[-85.875,130.169],[-65.541,152.003],[54.497,161.003],[42.419,126.963],[34.997,109.43],[34.997,-109.531]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-1.776,2.595],[23.373,8.037],[0,0],[0,-11.506],[0,0],[-10.611,-3.862],[-41.925,18.346],[0,0],[0,9.935],[0,0]],"o":[[0,0],[-44.2,-18.355],[-12.325,5.211],[0,0],[0,11.505],[0,0],[31.73,-13.971],[-1.776,-2.971],[0,0],[0,-9.936]],"v":[[58.47,-130.461],[59.054,-159.208],[-67.97,-152.791],[-89.232,-130.243],[-89.232,130.175],[-68.827,151.866],[56.422,159.163],[58.47,130.403],[56.711,112.398],[56.711,-112.485]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":31,"s":[{"i":[[6.089,1.514],[42.238,4.688],[0,0],[0,-11.506],[0,0],[-10.984,-2.253],[-61.736,7.974],[0,0],[0,10.589],[0,0]],"o":[[0,0],[-62.199,-9.441],[-11.984,3.04],[0,0],[0,11.505],[0,0],[49.46,-8.15],[6.089,-1.733],[0,0],[0,-10.591]],"v":[[98.595,-139.037],[30.196,-155.562],[-76.541,-152.062],[-97.625,-130.229],[-97.625,130.189],[-77.041,151.523],[29.733,156.023],[98.595,139.003],[110.997,119.819],[110.997,-119.87]],"c":true}]},{"i":{"x":0.25,"y":1},"o":{"x":0.167,"y":0.167},"t":34.428,"s":[{"i":[[8.754,0.899],[38.858,0.619],[0,0],[0,-11.506],[0,0],[-11.208,-1.286],[-49.166,1.112],[0,0],[0,11.425],[0,0]],"o":[[0,0],[-47.257,-0.753],[-11.779,1.735],[0,0],[0,11.505],[0,0],[44.834,-0.888],[8.754,-1.029],[0,0],[0,-11.427]],"v":[[93.957,-150.003],[16.803,-153.622],[-81.693,-151.624],[-102.669,-130.22],[-102.669,130.198],[-81.978,151.316],[17.827,153.885],[93.957,149.984],[110.624,129.29],[110.624,-129.319]],"c":true}]},{"t":47.28515625,"s":[{"i":[[11.506,0],[29.848,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[-30.18,0],[0,0],[0,11.505],[0,0]],"o":[[0,0],[-29.179,0],[-11.506,0],[0,0],[0,11.505],[0,0],[28.848,0],[11.506,0],[0,0],[0,-11.506]],"v":[[88.541,-151.042],[-1.003,-151.042],[-88.541,-151.042],[-109.375,-130.209],[-109.375,130.209],[-88.541,151.042],[1.997,151.042],[88.541,151.042],[109.375,130.209],[109.375,-130.209]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('165-view-carousel-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":13,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":14,"s":[22]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":22,"s":[20]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[18]},{"t":26,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":13,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":14,"s":[75]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[75]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":22,"s":[84]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[86]},{"t":26,"s":[100]}],"ix":2},"o":{"a":0,"k":302,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('165-view-carousel-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":12,"op":29,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.77,"y":0},"t":1,"s":[158.997,250.003,0],"to":[-11.333,0,0],"ti":[-15.167,0,0]},{"i":{"x":0.159,"y":1},"o":{"x":0.407,"y":0},"t":17,"s":[90.997,250.003,0],"to":[7.313,0,0],"ti":[-13.722,0,0]},{"t":55,"s":[249.997,250.003,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"t":1,"s":[{"i":[[11.506,0],[0,0],[0,0],[3.77,-3.77],[0,-5.753],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0]],"o":[[0,0],[0,0],[-5.753,0],[-3.77,3.77],[0,0],[0,11.505],[0,0],[11.506,0],[0,0],[0,-11.506]],"v":[[81.291,-108.915],[24,-108.915],[-33.291,-108.915],[-48.023,-102.813],[-54.125,-88.081],[-54.125,89.003],[-33.291,109.836],[81.291,109.836],[102.125,89.003],[102.125,-88.081]],"c":true}],"h":1},{"i":{"x":0.833,"y":0.859},"o":{"x":0.333,"y":0},"t":2.18,"s":[{"i":[[-10.5,0],[-33,0],[0,0],[0,-11.506],[0,0],[10.599,-0.506],[31.5,0],[0,0],[0,9.674],[0,0]],"o":[[0,0],[30.33,0],[14.039,-0.421],[0,0],[0,11.505],[0,0],[-32,0],[-10,-0.837],[0,0],[0,-9.675]],"v":[[-34.003,-109.325],[18.497,-109.489],[79.459,-109.409],[100.997,-87.576],[100.997,88.342],[80.898,109.181],[18.497,109.193],[-34.503,109.76],[-55.503,92.484],[-55.503,-91.976]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.141},"t":4.539,"s":[{"i":[[-10.707,0.102],[-33.475,1.617],[0,0],[0,-11.506],[0,0],[11.602,0.847],[31.916,1.156],[0,0],[0,9.674],[0,0]],"o":[[0,0],[30.272,-1.173],[14.232,-0.984],[0,0],[0,11.505],[0,0],[-32.538,-0.824],[-10.206,-0.926],[0,0],[0,-9.675]],"v":[[-32.567,-109.991],[23.211,-112.208],[83.578,-115.094],[105.014,-93.835],[105.196,92.407],[84.952,115.024],[22.77,112.067],[-33.068,110.27],[-53.988,92.985],[-53.988,-92.637]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-11.32,0.404],[-29.19,6.404],[0,0],[0,-11.506],[0,0],[12.597,4.854],[27.72,4.58],[0,0],[0,9.673],[0,0]],"o":[[0,0],[24.953,-4.645],[12.385,-2.651],[0,0],[0,11.505],[0,0],[-28.6,-3.263],[-10.817,-1.188],[0,0],[0,-9.675]],"v":[[-28.317,-111.962],[31.476,-120.26],[79.82,-131.925],[97.307,-112.367],[97.997,104.443],[80.768,132.323],[29.803,120.575],[-28.819,111.778],[-49.503,94.468],[-49.503,-94.593]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[-9.013,0.807],[-17.317,12.809],[0,0],[0,-11.506],[0,0],[11.116,10.214],[16.283,9.16],[0,0],[0,9.673],[0,0]],"o":[[0,0],[12.683,-9.289],[7.31,-4.881],[0,0],[0,11.505],[0,0],[-17.3,-6.527],[-8.646,-1.539],[0,0],[0,-9.675]],"v":[[7.243,-114.557],[57.814,-130.99],[80.187,-154.399],[88.793,-137.117],[89.982,120.584],[80.382,155.507],[54.93,131.999],[6.876,113.837],[-10.503,96.493],[-10.503,-97.168]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[-7.525,1.615],[-1.633,25.617],[0,0],[0,-11.506],[0,0],[-6.935,3.293],[1.067,18.321],[0,0],[0,9.674],[0,0]],"o":[[0,0],[-9.023,-16.317],[-6.728,1.014],[0,0],[0,11.505],[0,0],[-2.6,-13.053],[-7.292,-2.239],[0,0],[0,-9.675]],"v":[[0.489,-118.579],[59.13,-154.283],[32.725,-168.179],[22.399,-155.812],[24.967,153.672],[41.432,165.042],[58.93,144.014],[0.256,119.124],[-13.503,101.711],[-13.503,-101.15]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":19.873,"s":[{"i":[[-6.038,2.422],[8.3,17.524],[0,0],[0,-11.506],[0,0],[-6.249,-3.706],[-24.4,30.92],[0,0],[0,9.674],[0,0]],"o":[[0,0],[-28.7,-24.476],[-7.161,4.779],[0,0],[0,11.505],[0,0],[12.1,-19.58],[-5.938,-2.94],[0,0],[0,-9.675]],"v":[[25.735,-123.553],[61.197,-154.527],[-29.141,-161.911],[-41.9,-139.278],[-36.9,138.74],[-24.654,160.375],[60.897,155.077],[25.635,123.459],[15.497,105.977],[15.497,-106.084]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[-4.922,3.028],[15.827,9.377],[0,0],[0,-11.506],[0,0],[-10.461,-4.506],[-34,22.494],[0,0],[0,9.674],[0,0]],"o":[[0,0],[-37,-21.921],[-12.461,6.079],[0,0],[0,11.505],[0,0],[24.637,-16.3],[-4.922,-3.466],[0,0],[0,-9.675]],"v":[[42.419,-127.031],[57.997,-160.082],[-64.541,-153.082],[-85.875,-130.248],[-85.875,130.169],[-65.541,152.003],[54.497,161.003],[42.419,126.963],[34.997,109.43],[34.997,-109.531]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[-1.776,2.595],[23.373,8.037],[0,0],[0,-11.506],[0,0],[-10.611,-3.862],[-41.925,18.346],[0,0],[0,9.935],[0,0]],"o":[[0,0],[-44.2,-18.355],[-12.325,5.211],[0,0],[0,11.505],[0,0],[31.73,-13.971],[-1.776,-2.971],[0,0],[0,-9.936]],"v":[[58.47,-130.461],[59.054,-159.208],[-67.97,-152.791],[-89.232,-130.243],[-89.232,130.175],[-68.827,151.866],[56.422,159.163],[58.47,130.403],[56.711,112.398],[56.711,-112.485]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":31,"s":[{"i":[[6.089,1.514],[42.238,4.688],[0,0],[0,-11.506],[0,0],[-10.984,-2.253],[-61.736,7.974],[0,0],[0,10.589],[0,0]],"o":[[0,0],[-62.199,-9.441],[-11.984,3.04],[0,0],[0,11.505],[0,0],[49.46,-8.15],[6.089,-1.733],[0,0],[0,-10.591]],"v":[[98.595,-139.037],[30.196,-155.562],[-76.541,-152.062],[-97.625,-130.229],[-97.625,130.189],[-77.041,151.523],[29.733,156.023],[98.595,139.003],[110.997,119.819],[110.997,-119.87]],"c":true}]},{"i":{"x":0.25,"y":1},"o":{"x":0.167,"y":0.167},"t":34.428,"s":[{"i":[[8.754,0.899],[38.858,0.619],[0,0],[0,-11.506],[0,0],[-11.208,-1.286],[-49.166,1.112],[0,0],[0,11.425],[0,0]],"o":[[0,0],[-47.257,-0.753],[-11.779,1.735],[0,0],[0,11.505],[0,0],[44.834,-0.888],[8.754,-1.029],[0,0],[0,-11.427]],"v":[[93.957,-150.003],[16.803,-153.622],[-81.693,-151.624],[-102.669,-130.22],[-102.669,130.198],[-81.978,151.316],[17.827,153.885],[93.957,149.984],[110.624,129.29],[110.624,-129.319]],"c":true}]},{"t":47.28515625,"s":[{"i":[[11.506,0],[29.848,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[-30.18,0],[0,0],[0,11.505],[0,0]],"o":[[0,0],[-29.179,0],[-11.506,0],[0,0],[0,11.505],[0,0],[28.848,0],[11.506,0],[0,0],[0,-11.506]],"v":[[88.541,-151.042],[-1.003,-151.042],[-88.541,-151.042],[-109.375,-130.209],[-109.375,130.209],[-88.541,151.042],[1.997,151.042],[88.541,151.042],[109.375,130.209],[109.375,-130.209]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('165-view-carousel-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"t":1,"s":[24],"h":1},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":3,"s":[24]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":7,"s":[17]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":8,"s":[15]},{"t":9,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"t":1,"s":[74],"h":1},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":3,"s":[74]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":7,"s":[83]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":8,"s":[85]},{"t":9,"s":[100]}],"ix":2},"o":{"a":1,"k":[{"t":1,"s":[122],"h":1},{"t":3,"s":[302],"h":1}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('165-view-carousel-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":12,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.25,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[364.587,250.003,0],"to":[-38.167,0,0],"ti":[38.167,0,0]},{"t":40,"s":[135.587,250.003,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[11.506,0],[0,0],[0,-11.506],[0,0],[-11.506,0],[0,0],[0,11.505],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,11.505],[0,0],[11.506,0],[0,0],[0,-11.506]],"v":[[57.291,-109.376],[-57.291,-109.376],[-78.125,-88.542],[-78.125,88.542],[-57.291,109.376],[57.291,109.376],[78.125,88.542],[78.125,-88.542]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('165-view-carousel-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":1,"s":[26]},{"t":7,"s":[34],"h":1},{"t":8,"s":[50],"h":1},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":11,"s":[65]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[77]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":18,"s":[83]},{"t":19,"s":[100],"h":1},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[16]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":22,"s":[27.5]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":24,"s":[29]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":27,"s":[36]},{"t":28,"s":[50],"h":1},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":31,"s":[63]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":35,"s":[68]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[70]},{"t":55,"s":[73]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":1,"s":[74]},{"t":7,"s":[65],"h":1},{"t":8,"s":[50],"h":1},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":11,"s":[35]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[21]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":18,"s":[14]},{"t":19,"s":[0],"h":1},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[81]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":22,"s":[73.5]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":24,"s":[70]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":27,"s":[63]},{"t":28,"s":[50],"h":1},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":31,"s":[35]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":35,"s":[32]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[27]},{"t":55,"s":[26]}],"ix":2},"o":{"a":1,"k":[{"t":1,"s":[121],"h":1},{"t":11,"s":[301],"h":1},{"t":20,"s":[121],"h":1},{"t":31,"s":[301],"h":1}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('165-view-carousel-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":55,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"lordicon.com Outlines","cl":"com","sr":1,"ks":{"o":{"a":0,"k":20,"ix":11,"x":"var $bm_rt;\nvar checkbox = thisComp.layer('02092020').effect('02092020002')('Checkbox');\nif (checkbox == 1) {\n $bm_rt = 20;\n} else {\n $bm_rt = 0;\n}\n;"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.934,481.369,0],"ix":2,"l":2},"a":{"a":0,"k":[79.934,0.369,0],"ix":1,"l":2},"s":{"a":0,"k":[265.159,265.159,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[1.415,0],[11.014,0],[11.014,-2.523],[4.656,-2.523],[4.656,-14.809],[1.415,-14.809]],"c":true},"ix":2},"nm":"l","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"l","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[11.167,-7.199],[12.992,-1.661],[18.243,0.369],[23.514,-1.743],[25.381,-7.548],[23.494,-13.127],[18.284,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[14.49,-7.302],[15.577,-11.609],[18.305,-12.86],[21.689,-10.235],[22.058,-7.589],[21.053,-3.343],[18.284,-1.969],[15.597,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[-0.287,-0.841],[-0.144,-0.82],[0,0],[0.164,0.656],[0.226,1.743],[2.236,0.205],[0,2.769],[0.923,0.8],[1.641,-0.021],[0,0]],"o":[[0,0],[0,0],[0,0],[0.533,0],[0.205,0.574],[0,0],[-0.164,-0.246],[-0.103,-0.41],[-0.267,-1.928],[0.718,-0.205],[0,-0.964],[-1.19,-1.026],[0,0],[0,0]],"v":[[27.381,0],[30.622,0],[30.622,-5.989],[33.411,-5.989],[35.011,-5.148],[35.811,0],[39.318,0],[38.867,-1.067],[38.416,-3.938],[35.749,-7.343],[38.847,-10.973],[37.554,-13.824],[33.063,-14.829],[27.381,-14.829]],"c":true},"ix":2},"nm":"r","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-0.492,-0.349],[0,-1.005],[0.226,-0.164],[0.369,0],[0,0]],"o":[[0,0],[1.005,0],[0.287,0.185],[0,1.046],[-0.513,0.41],[0,0],[0,0]],"v":[[30.519,-12.491],[32.652,-12.491],[34.744,-12.142],[35.524,-10.481],[34.703,-8.758],[33.083,-8.348],[30.519,-8.348]],"c":true},"ix":2},"nm":"r","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"r","np":5,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-0.554,0.103],[0,4.553],[1.866,1.374],[0.82,0],[0,0]],"o":[[0,0],[1.497,0],[2.81,-0.513],[0,-2.113],[-1.784,-1.313],[0,0],[0,0]],"v":[[41.068,0],[45.683,0],[48.349,-0.164],[53.6,-7.609],[51.077,-13.434],[45.97,-14.768],[41.068,-14.788]],"c":true},"ix":2},"nm":"d","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-0.656,-0.185],[0,-2.092],[1.251,-1.251],[1.354,0],[0.349,0.021]],"o":[[1.825,-0.082],[1.99,0.554],[0,0.718],[-0.923,0.923],[-0.369,0],[0,0]],"v":[[44.288,-12.388],[47.611,-12.183],[50.318,-7.609],[48.985,-3.425],[45.539,-2.4],[44.288,-2.441]],"c":true},"ix":2},"nm":"d","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"d","np":5,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[55.669,0],[58.849,0],[58.849,-14.87],[55.669,-14.87]],"c":true},"ix":2},"nm":"i","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"i","np":3,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[3.241,0],[0,-4.697],[-5.107,0],[-1.313,1.354],[-0.062,0.882],[0,0],[1.333,0],[0,0.882],[-2.359,0],[-0.062,-0.513]],"o":[[0,-2.954],[-4.164,0],[0,3.671],[1.354,0],[1.19,-1.231],[0,0],[-0.062,1.969],[-3.097,0],[0,-3.056],[2.154,0],[0,0]],"v":[[73.104,-9.989],[67.587,-14.911],[60.798,-7.097],[67.566,0.349],[71.894,-1.313],[73.227,-4.799],[69.884,-4.799],[67.218,-1.99],[64.121,-7.076],[67.464,-12.593],[69.864,-9.989]],"c":true},"ix":2},"nm":"c","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"c","np":3,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[74.546,-7.199],[76.372,-1.661],[81.622,0.369],[86.894,-1.743],[88.76,-7.548],[86.873,-13.127],[81.663,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[77.869,-7.302],[78.956,-11.609],[81.684,-12.86],[85.068,-10.235],[85.437,-7.589],[84.432,-3.343],[81.663,-1.969],[78.977,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[91.007,0],[94.001,0],[94.001,-12.306],[99.744,0],[104.113,0],[104.113,-14.829],[101.159,-14.829],[101.159,-3.159],[95.601,-14.829],[91.007,-14.829]],"c":true},"ix":2},"nm":"n","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"n","np":3,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[106.893,0],[109.497,0],[109.497,-2.728],[106.893,-2.728]],"c":true},"ix":2},"nm":".","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":".","np":3,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[3.241,0],[0,-4.697],[-5.107,0],[-1.313,1.354],[-0.062,0.882],[0,0],[1.333,0],[0,0.882],[-2.359,0],[-0.062,-0.513]],"o":[[0,-2.954],[-4.164,0],[0,3.671],[1.354,0],[1.19,-1.231],[0,0],[-0.062,1.969],[-3.097,0],[0,-3.056],[2.154,0],[0,0]],"v":[[124.04,-9.989],[118.523,-14.911],[111.734,-7.097],[118.502,0.349],[122.83,-1.313],[124.163,-4.799],[120.82,-4.799],[118.154,-1.99],[115.057,-7.076],[118.4,-12.593],[120.8,-9.989]],"c":true},"ix":2},"nm":"c","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"c","np":3,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[125.482,-7.199],[127.308,-1.661],[132.558,0.369],[137.829,-1.743],[139.696,-7.548],[137.809,-13.127],[132.599,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[128.805,-7.302],[129.892,-11.609],[132.62,-12.86],[136.004,-10.235],[136.373,-7.589],[135.368,-3.343],[132.599,-1.969],[129.912,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[141.696,0],[144.67,0],[144.67,-12.716],[148.629,0],[151.254,0],[155.295,-12.716],[155.295,0],[158.453,0],[158.453,-14.829],[153.408,-14.829],[150.024,-4.041],[146.885,-14.829],[141.696,-14.829]],"c":true},"ix":2},"nm":"m","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"m","np":3,"cix":2,"bm":0,"ix":12,"mn":"ADBE Vector Group","hd":false}],"ip":2.5,"op":25,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":3,"nm":"02092020","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-105,15,0],"ix":2,"l":2},"a":{"a":0,"k":[60,60,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"02092020002","np":3,"mn":"ADBE Checkbox Control","ix":1,"en":1,"ef":[{"ty":7,"nm":"Checkbox","mn":"ADBE Checkbox Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"Color & Stroke Change","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-55,-102,0],"ix":2,"l":2,"x":"var $bm_rt;\n$bm_rt = effect('Axis')('Point');"},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2,"x":"var $bm_rt;\nvar temp;\ntemp = effect('Scale')('Slider');\n$bm_rt = [\n temp,\n temp\n];"}},"ao":0,"ef":[{"ty":5,"nm":"Primary","np":3,"mn":"ADBE Color Control","ix":1,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]},{"ty":5,"nm":"Axis","np":3,"mn":"ADBE Point Control","ix":2,"en":1,"ef":[{"ty":3,"nm":"Point","mn":"ADBE Point Control-0001","ix":1,"v":{"a":0,"k":[250,250],"ix":1}}]},{"ty":5,"nm":"Scale","np":3,"mn":"ADBE Slider Control","ix":3,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":100,"ix":1}}]},{"ty":5,"nm":"State-Intro","np":3,"mn":"ADBE Slider Control","ix":4,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]},{"ty":5,"nm":"State-Hover","np":3,"mn":"ADBE Slider Control","ix":5,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":1,"ix":1}}]}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"in-carousel","parent":3,"refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Intro')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":71,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"hover-carousel","parent":3,"refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Hover')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":71,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/frontend/public/lotties/system-outline-168-view-headline.json b/frontend/public/lotties/system-outline-168-view-headline.json deleted file mode 100644 index 3d511470b..000000000 --- a/frontend/public/lotties/system-outline-168-view-headline.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.8.1","fr":60,"ip":0,"op":61,"w":500,"h":500,"nm":"168-view-headline-outline","ddd":0,"assets":[{"id":"comp_0","nm":"in-headline","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.004,250.003,0],"ix":2,"l":2},"a":{"a":0,"k":[250.004,250.003,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[156.226,20.808],[-156.226,20.808],[-156.226,-20.808],[156.226,-20.808]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,-8.643],[0,0],[-8.644,0],[0,0],[0,8.643],[0,0]],"o":[[0,0],[-8.644,0],[0,0],[0,8.643],[0,0],[8.644,0],[0,0],[0,-8.643]],"v":[[171.876,-52.108],[-171.876,-52.108],[-187.526,-36.458],[-187.526,36.458],[-171.876,52.108],[171.876,52.108],[187.526,36.458],[187.526,-36.458]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,385.421],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[156.226,20.808],[-156.226,20.808],[-156.226,-20.808],[156.226,-20.808]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,-8.643],[0,0],[-8.644,0],[0,0],[0,8.643],[0,0]],"o":[[0,0],[-8.644,0],[0,0],[0,8.643],[0,0],[8.644,0],[0,0],[0,-8.643]],"v":[[171.876,-52.108],[-171.876,-52.108],[-187.526,-36.458],[-187.526,36.458],[-171.876,52.108],[171.876,52.108],[187.526,36.458],[187.526,-36.458]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[156.226,20.808],[-156.226,20.808],[-156.226,-20.808],[156.226,-20.808]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,-8.643],[0,0],[-8.644,0],[0,0],[0,8.643],[0,0]],"o":[[0,0],[-8.644,0],[0,0],[0,8.643],[0,0],[8.644,0],[0,0],[0,-8.643]],"v":[[171.876,-52.108],[-171.876,-52.108],[-187.526,-36.458],[-187.526,36.458],[-171.876,52.108],[171.876,52.108],[187.526,36.458],[187.526,-36.458]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,114.586],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":3,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":56,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":1,"s":[250.004,566.586,0],"to":[0,-75.5,0],"ti":[0,75.5,0]},{"t":42,"s":[250.004,113.586,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[43.004,36.458],[-43.004,36.458],[-43.004,-36.458],[43.004,-36.458]],"c":true}]},{"t":42,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[171.876,36.458],[-171.876,36.458],[-171.876,-36.458],[171.876,-36.458]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":56,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":10,"s":[250.004,702.003,0],"to":[0,-75.5,0],"ti":[0,75.5,0]},{"t":49,"s":[250.004,249.003,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[43.004,36.458],[-43.004,36.458],[-43.004,-36.458],[43.004,-36.458]],"c":true}]},{"t":49,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[171.876,36.458],[-171.876,36.458],[-171.876,-36.458],[171.876,-36.458]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":56,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":20,"s":[250.004,837.42,0],"to":[0,-75.5,0],"ti":[0,75.5,0]},{"t":56,"s":[250.004,384.42,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":25,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[43.004,36.458],[-43.004,36.458],[-43.004,-36.458],[43.004,-36.458]],"c":true}]},{"t":56,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[171.876,36.458],[-171.876,36.458],[-171.876,-36.458],[171.876,-36.458]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":56,"st":0,"bm":0}]},{"id":"comp_1","nm":"hover-headline","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.004,250.003,0],"ix":2,"l":2},"a":{"a":0,"k":[250.004,250.003,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[156.226,20.808],[-156.226,20.808],[-156.226,-20.808],[156.226,-20.808]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,-8.643],[0,0],[-8.644,0],[0,0],[0,8.643],[0,0]],"o":[[0,0],[-8.644,0],[0,0],[0,8.643],[0,0],[8.644,0],[0,0],[0,-8.643]],"v":[[171.876,-52.108],[-171.876,-52.108],[-187.526,-36.458],[-187.526,36.458],[-171.876,52.108],[171.876,52.108],[187.526,36.458],[187.526,-36.458]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,385.421],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[156.226,20.808],[-156.226,20.808],[-156.226,-20.808],[156.226,-20.808]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,-8.643],[0,0],[-8.644,0],[0,0],[0,8.643],[0,0]],"o":[[0,0],[-8.644,0],[0,0],[0,8.643],[0,0],[8.644,0],[0,0],[0,-8.643]],"v":[[171.876,-52.108],[-171.876,-52.108],[-187.526,-36.458],[-187.526,36.458],[-171.876,52.108],[171.876,52.108],[187.526,36.458],[187.526,-36.458]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[156.226,20.808],[-156.226,20.808],[-156.226,-20.808],[156.226,-20.808]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,-8.643],[0,0],[-8.644,0],[0,0],[0,8.643],[0,0]],"o":[[0,0],[-8.644,0],[0,0],[0,8.643],[0,0],[8.644,0],[0,0],[0,-8.643]],"v":[[171.876,-52.108],[-171.876,-52.108],[-187.526,-36.458],[-187.526,36.458],[-171.876,52.108],[171.876,52.108],[187.526,36.458],[187.526,-36.458]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,114.586],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":3,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":1,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":59,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.004,250.003,0],"ix":2,"l":2},"a":{"a":0,"k":[250.004,250.003,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[156.226,20.808],[-156.226,20.808],[-156.226,-20.808],[156.226,-20.808]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,-8.643],[0,0],[-8.644,0],[0,0],[0,8.643],[0,0]],"o":[[0,0],[-8.644,0],[0,0],[0,8.643],[0,0],[8.644,0],[0,0],[0,-8.643]],"v":[[171.876,-52.108],[-171.876,-52.108],[-187.526,-36.458],[-187.526,36.458],[-171.876,52.108],[171.876,52.108],[187.526,36.458],[187.526,-36.458]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,385.421],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[156.226,20.808],[-156.226,20.808],[-156.226,-20.808],[156.226,-20.808]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,-8.643],[0,0],[-8.644,0],[0,0],[0,8.643],[0,0]],"o":[[0,0],[-8.644,0],[0,0],[0,8.643],[0,0],[8.644,0],[0,0],[0,-8.643]],"v":[[171.876,-52.108],[-171.876,-52.108],[-187.526,-36.458],[-187.526,36.458],[-171.876,52.108],[171.876,52.108],[187.526,36.458],[187.526,-36.458]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[156.226,20.808],[-156.226,20.808],[-156.226,-20.808],[156.226,-20.808]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,-8.643],[0,0],[-8.644,0],[0,0],[0,8.643],[0,0]],"o":[[0,0],[-8.644,0],[0,0],[0,8.643],[0,0],[8.644,0],[0,0],[0,-8.643]],"v":[[171.876,-52.108],[-171.876,-52.108],[-187.526,-36.458],[-187.526,36.458],[-171.876,52.108],[171.876,52.108],[187.526,36.458],[187.526,-36.458]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,114.586],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":3,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":1,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":1,"s":[250.004,114.586,0],"to":[0,-75.5,0],"ti":[0,75.5,0]},{"t":21,"s":[250.004,-338.414,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[171.876,36.458],[-171.876,36.458],[-171.876,-36.458],[171.876,-36.458]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":59,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":3,"s":[250.004,250.003,0],"to":[0,-75.5,0],"ti":[0,75.5,0]},{"t":29,"s":[250.004,-202.997,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[171.876,36.458],[-171.876,36.458],[-171.876,-36.458],[171.876,-36.458]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":59,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":5,"s":[250.004,385.42,0],"to":[0,-75.5,0],"ti":[0,75.5,0]},{"t":37,"s":[250.004,-67.58,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[171.876,36.458],[-171.876,36.458],[-171.876,-36.458],[171.876,-36.458]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":59,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.333,"y":0},"t":15,"s":[250.004,566.586,0],"to":[0,-75.5,0],"ti":[0,75.5,0]},{"t":51,"s":[250.004,113.586,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[171.876,36.458],[-171.876,36.458],[-171.876,-36.458],[171.876,-36.458]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":59,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.333,"y":0},"t":21,"s":[250.004,702.003,0],"to":[0,-75.5,0],"ti":[0,75.5,0]},{"t":55,"s":[250.004,249.003,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[171.876,36.458],[-171.876,36.458],[-171.876,-36.458],[171.876,-36.458]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":59,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.333,"y":0},"t":28,"s":[250.004,837.42,0],"to":[0,-75.5,0],"ti":[0,75.5,0]},{"t":59,"s":[250.004,384.42,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[171.876,36.458],[-171.876,36.458],[-171.876,-36.458],[171.876,-36.458]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('168-view-headline-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":59,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"lordicon.com Outlines","cl":"com","sr":1,"ks":{"o":{"a":0,"k":20,"ix":11,"x":"var $bm_rt;\nvar checkbox = thisComp.layer('02092020').effect('02092020002')('Checkbox');\nif (checkbox == 1) {\n $bm_rt = 20;\n} else {\n $bm_rt = 0;\n}\n;"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.934,481.369,0],"ix":2,"l":2},"a":{"a":0,"k":[79.934,0.369,0],"ix":1,"l":2},"s":{"a":0,"k":[265.159,265.159,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[1.415,0],[11.014,0],[11.014,-2.523],[4.656,-2.523],[4.656,-14.809],[1.415,-14.809]],"c":true},"ix":2},"nm":"l","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"l","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[11.167,-7.199],[12.992,-1.661],[18.243,0.369],[23.514,-1.743],[25.381,-7.548],[23.494,-13.127],[18.284,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[14.49,-7.302],[15.577,-11.609],[18.305,-12.86],[21.689,-10.235],[22.058,-7.589],[21.053,-3.343],[18.284,-1.969],[15.597,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[-0.287,-0.841],[-0.144,-0.82],[0,0],[0.164,0.656],[0.226,1.743],[2.236,0.205],[0,2.769],[0.923,0.8],[1.641,-0.021],[0,0]],"o":[[0,0],[0,0],[0,0],[0.533,0],[0.205,0.574],[0,0],[-0.164,-0.246],[-0.103,-0.41],[-0.267,-1.928],[0.718,-0.205],[0,-0.964],[-1.19,-1.026],[0,0],[0,0]],"v":[[27.381,0],[30.622,0],[30.622,-5.989],[33.411,-5.989],[35.011,-5.148],[35.811,0],[39.318,0],[38.867,-1.067],[38.416,-3.938],[35.749,-7.343],[38.847,-10.973],[37.554,-13.824],[33.063,-14.829],[27.381,-14.829]],"c":true},"ix":2},"nm":"r","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-0.492,-0.349],[0,-1.005],[0.226,-0.164],[0.369,0],[0,0]],"o":[[0,0],[1.005,0],[0.287,0.185],[0,1.046],[-0.513,0.41],[0,0],[0,0]],"v":[[30.519,-12.491],[32.652,-12.491],[34.744,-12.142],[35.524,-10.481],[34.703,-8.758],[33.083,-8.348],[30.519,-8.348]],"c":true},"ix":2},"nm":"r","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"r","np":5,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-0.554,0.103],[0,4.553],[1.866,1.374],[0.82,0],[0,0]],"o":[[0,0],[1.497,0],[2.81,-0.513],[0,-2.113],[-1.784,-1.313],[0,0],[0,0]],"v":[[41.068,0],[45.683,0],[48.349,-0.164],[53.6,-7.609],[51.077,-13.434],[45.97,-14.768],[41.068,-14.788]],"c":true},"ix":2},"nm":"d","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-0.656,-0.185],[0,-2.092],[1.251,-1.251],[1.354,0],[0.349,0.021]],"o":[[1.825,-0.082],[1.99,0.554],[0,0.718],[-0.923,0.923],[-0.369,0],[0,0]],"v":[[44.288,-12.388],[47.611,-12.183],[50.318,-7.609],[48.985,-3.425],[45.539,-2.4],[44.288,-2.441]],"c":true},"ix":2},"nm":"d","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"d","np":5,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[55.669,0],[58.849,0],[58.849,-14.87],[55.669,-14.87]],"c":true},"ix":2},"nm":"i","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"i","np":3,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[3.241,0],[0,-4.697],[-5.107,0],[-1.313,1.354],[-0.062,0.882],[0,0],[1.333,0],[0,0.882],[-2.359,0],[-0.062,-0.513]],"o":[[0,-2.954],[-4.164,0],[0,3.671],[1.354,0],[1.19,-1.231],[0,0],[-0.062,1.969],[-3.097,0],[0,-3.056],[2.154,0],[0,0]],"v":[[73.104,-9.989],[67.587,-14.911],[60.798,-7.097],[67.566,0.349],[71.894,-1.313],[73.227,-4.799],[69.884,-4.799],[67.218,-1.99],[64.121,-7.076],[67.464,-12.593],[69.864,-9.989]],"c":true},"ix":2},"nm":"c","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"c","np":3,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[74.546,-7.199],[76.372,-1.661],[81.622,0.369],[86.894,-1.743],[88.76,-7.548],[86.873,-13.127],[81.663,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[77.869,-7.302],[78.956,-11.609],[81.684,-12.86],[85.068,-10.235],[85.437,-7.589],[84.432,-3.343],[81.663,-1.969],[78.977,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[91.007,0],[94.001,0],[94.001,-12.306],[99.744,0],[104.113,0],[104.113,-14.829],[101.159,-14.829],[101.159,-3.159],[95.601,-14.829],[91.007,-14.829]],"c":true},"ix":2},"nm":"n","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"n","np":3,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[106.893,0],[109.497,0],[109.497,-2.728],[106.893,-2.728]],"c":true},"ix":2},"nm":".","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":".","np":3,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[3.241,0],[0,-4.697],[-5.107,0],[-1.313,1.354],[-0.062,0.882],[0,0],[1.333,0],[0,0.882],[-2.359,0],[-0.062,-0.513]],"o":[[0,-2.954],[-4.164,0],[0,3.671],[1.354,0],[1.19,-1.231],[0,0],[-0.062,1.969],[-3.097,0],[0,-3.056],[2.154,0],[0,0]],"v":[[124.04,-9.989],[118.523,-14.911],[111.734,-7.097],[118.502,0.349],[122.83,-1.313],[124.163,-4.799],[120.82,-4.799],[118.154,-1.99],[115.057,-7.076],[118.4,-12.593],[120.8,-9.989]],"c":true},"ix":2},"nm":"c","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"c","np":3,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[125.482,-7.199],[127.308,-1.661],[132.558,0.369],[137.829,-1.743],[139.696,-7.548],[137.809,-13.127],[132.599,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[128.805,-7.302],[129.892,-11.609],[132.62,-12.86],[136.004,-10.235],[136.373,-7.589],[135.368,-3.343],[132.599,-1.969],[129.912,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[141.696,0],[144.67,0],[144.67,-12.716],[148.629,0],[151.254,0],[155.295,-12.716],[155.295,0],[158.453,0],[158.453,-14.829],[153.408,-14.829],[150.024,-4.041],[146.885,-14.829],[141.696,-14.829]],"c":true},"ix":2},"nm":"m","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"m","np":3,"cix":2,"bm":0,"ix":12,"mn":"ADBE Vector Group","hd":false}],"ip":2.5,"op":25,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":3,"nm":"02092020","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-105,15,0],"ix":2,"l":2},"a":{"a":0,"k":[60,60,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"02092020002","np":3,"mn":"ADBE Checkbox Control","ix":1,"en":1,"ef":[{"ty":7,"nm":"Checkbox","mn":"ADBE Checkbox Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"Color & Stroke Change","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-55,-102,0],"ix":2,"l":2,"x":"var $bm_rt;\n$bm_rt = effect('Axis')('Point');"},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2,"x":"var $bm_rt;\nvar temp;\ntemp = effect('Scale')('Slider');\n$bm_rt = [\n temp,\n temp\n];"}},"ao":0,"ef":[{"ty":5,"nm":"Primary","np":3,"mn":"ADBE Color Control","ix":1,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]},{"ty":5,"nm":"Axis","np":3,"mn":"ADBE Point Control","ix":2,"en":1,"ef":[{"ty":3,"nm":"Point","mn":"ADBE Point Control-0001","ix":1,"v":{"a":0,"k":[250,250],"ix":1}}]},{"ty":5,"nm":"Scale","np":3,"mn":"ADBE Slider Control","ix":3,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":100,"ix":1}}]},{"ty":5,"nm":"State-Intro","np":3,"mn":"ADBE Slider Control","ix":4,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]},{"ty":5,"nm":"State-Hover","np":3,"mn":"ADBE Slider Control","ix":5,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":1,"ix":1}}]}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"in-headline","parent":3,"refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Intro')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":71,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"hover-headline","parent":3,"refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Hover')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":71,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/frontend/public/lotties/system-outline-69-document-scan.json b/frontend/public/lotties/system-outline-69-document-scan.json deleted file mode 100644 index 03664451e..000000000 --- a/frontend/public/lotties/system-outline-69-document-scan.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.8.1","fr":60,"ip":0,"op":91,"w":500,"h":500,"nm":"69-document-scan-outline","ddd":0,"assets":[{"id":"comp_0","nm":"in-scan","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.003,250.003,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-8.643]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[8.644,0],[0,8.643]],"v":[[171.875,15.65],[-171.875,15.65],[-187.525,0],[-171.875,-15.65],[171.875,-15.65],[187.525,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.001,250.001,0],"ix":2,"l":2},"a":{"a":0,"k":[250.001,250.001,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,8.643],[0,0],[2.858,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-20.117],[0,0]],"o":[[-8.644,0],[0,0],[0,-2.858],[0,0],[-8.644,0],[0,-8.643],[0,0],[20.117,0],[0,0],[0,8.643]],"v":[[46.875,62.525],[31.225,46.875],[31.225,-26.041],[26.041,-31.225],[-46.875,-31.225],[-62.525,-46.875],[-46.875,-62.525],[26.041,-62.525],[62.525,-26.041],[62.525,46.875]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[374.998,125.001],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,20.117],[0,0],[-8.644,0],[0,-8.643],[0,0],[-2.857,0],[0,0],[0,-8.643]],"o":[[0,0],[-20.117,0],[0,0],[0,-8.643],[8.644,0],[0,0],[0,2.858],[0,0],[8.644,0],[0,8.643]],"v":[[46.875,62.524],[-26.042,62.524],[-62.525,26.041],[-62.525,-46.875],[-46.875,-62.524],[-31.224,-46.875],[-31.224,26.041],[-26.042,31.225],[46.875,31.225],[62.525,46.875]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[124.998,374.999],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,8.643],[0,0],[-20.117,0],[0,0],[0,-8.643],[8.644,0],[0,0],[0,-2.858],[0,0]],"o":[[-8.644,0],[0,0],[0,-20.117],[0,0],[8.644,0],[0,8.643],[0,0],[-2.857,0],[0,0],[0,8.643]],"v":[[-46.875,62.524],[-62.525,46.875],[-62.525,-26.042],[-26.042,-62.524],[46.875,-62.524],[62.525,-46.875],[46.875,-31.225],[-26.042,-31.225],[-31.224,-26.042],[-31.224,46.875]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[124.998,124.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[20.117,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,2.858],[0,0],[-8.644,0],[0,-8.643],[0,0]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[2.857,0],[0,0],[0,-8.643],[8.644,0],[0,0],[0,20.117]],"v":[[26.042,62.525],[-46.875,62.525],[-62.525,46.875],[-46.875,31.225],[26.042,31.225],[31.224,26.041],[31.224,-46.875],[46.875,-62.525],[62.525,-46.875],[62.525,26.041]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[375.004,375.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.004,250.003,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.004,0],[-0.004,0]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-171.875,0],[171.875,0]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":10,"op":60,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.001,250.001,0],"ix":2,"l":2},"a":{"a":0,"k":[250.001,250.001,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0],[0,-0.25],[0,0]],"o":[[0,0],[0.25,0],[0,0],[0,0]],"v":[[-125.038,123.001],[-123.451,123.001],[-122.998,123.454],[-122.998,125.04]],"c":false}]},{"i":{"x":0.04,"y":1},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[0,0],[0,-11.506],[0,0]],"o":[[0,0],[11.506,0],[0,0],[0,0]],"v":[[-126.875,33.125],[-53.959,33.125],[-33.125,53.959],[-33.125,126.875]],"c":false}]},{"t":45,"s":[{"i":[[0,0],[0,0],[0,-11.506],[0,0]],"o":[[0,0],[11.506,0],[0,0],[0,0]],"v":[[-46.875,-46.875],[26.041,-46.875],[46.875,-26.041],[46.875,46.875]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0],[0,0.25],[0,0]],"o":[[0,0],[-0.25,0],[0,0],[0,0]],"v":[[-124.956,126.998],[-126.543,126.998],[-126.996,126.545],[-126.996,124.959]],"c":false}]},{"i":{"x":0.04,"y":1},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[0,0],[0,11.506],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,0]],"v":[[-123.126,216.872],[-196.042,216.872],[-216.875,196.039],[-216.875,123.123]],"c":false}]},{"t":45,"s":[{"i":[[0,0],[0,0],[0,11.506],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,0]],"v":[[-203.126,296.872],[-276.042,296.872],[-296.875,276.039],[-296.875,203.123]],"c":false}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0],[-0.25,0],[0,0]],"o":[[0,0],[0,-0.25],[0,0],[0,0]],"v":[[-126.996,125.04],[-126.996,123.454],[-126.543,123.001],[-124.956,123.001]],"c":false}]},{"i":{"x":0.04,"y":1},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[0,0],[-11.506,0],[0,0]],"o":[[0,0],[0,-11.506],[0,0],[0,0]],"v":[[-216.875,126.872],[-216.875,53.956],[-196.042,33.122],[-123.126,33.122]],"c":false}]},{"t":45,"s":[{"i":[[0,0],[0,0],[-11.506,0],[0,0]],"o":[[0,0],[0,-11.506],[0,0],[0,0]],"v":[[-296.875,46.872],[-296.875,-26.044],[-276.042,-46.878],[-203.126,-46.878]],"c":false}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0],[0.25,0],[0,0]],"o":[[0,0],[0,0.25],[0,0],[0,0]],"v":[[-122.998,124.959],[-122.998,126.545],[-123.451,126.998],[-125.038,126.998]],"c":false}]},{"i":{"x":0.04,"y":1},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[0,0],[11.506,0],[0,0]],"o":[[0,0],[0,11.506],[0,0],[0,0]],"v":[[-33.119,123.127],[-33.119,196.043],[-53.952,216.877],[-126.868,216.877]],"c":false}]},{"t":45,"s":[{"i":[[0,0],[0,0],[11.506,0],[0,0]],"o":[[0,0],[0,11.506],[0,0],[0,0]],"v":[[46.881,203.127],[46.881,276.043],[26.048,296.877],[-46.868,296.877]],"c":false}]}],"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[374.998,125.001],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":0,"bm":0}]},{"id":"comp_1","nm":"hover-scan","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.003,250.003,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-8.643]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[8.644,0],[0,8.643]],"v":[[171.875,15.65],[-171.875,15.65],[-187.525,0],[-171.875,-15.65],[171.875,-15.65],[187.525,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":90,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.001,250.001,0],"ix":2,"l":2},"a":{"a":0,"k":[250.001,250.001,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,8.643],[0,0],[2.858,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-20.117],[0,0]],"o":[[-8.644,0],[0,0],[0,-2.858],[0,0],[-8.644,0],[0,-8.643],[0,0],[20.117,0],[0,0],[0,8.643]],"v":[[46.875,62.525],[31.225,46.875],[31.225,-26.041],[26.041,-31.225],[-46.875,-31.225],[-62.525,-46.875],[-46.875,-62.525],[26.041,-62.525],[62.525,-26.041],[62.525,46.875]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[374.998,125.001],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,20.117],[0,0],[-8.644,0],[0,-8.643],[0,0],[-2.857,0],[0,0],[0,-8.643]],"o":[[0,0],[-20.117,0],[0,0],[0,-8.643],[8.644,0],[0,0],[0,2.858],[0,0],[8.644,0],[0,8.643]],"v":[[46.875,62.524],[-26.042,62.524],[-62.525,26.041],[-62.525,-46.875],[-46.875,-62.524],[-31.224,-46.875],[-31.224,26.041],[-26.042,31.225],[46.875,31.225],[62.525,46.875]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[124.998,374.999],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,8.643],[0,0],[-20.117,0],[0,0],[0,-8.643],[8.644,0],[0,0],[0,-2.858],[0,0]],"o":[[-8.644,0],[0,0],[0,-20.117],[0,0],[8.644,0],[0,8.643],[0,0],[-2.857,0],[0,0],[0,8.643]],"v":[[-46.875,62.524],[-62.525,46.875],[-62.525,-26.042],[-26.042,-62.524],[46.875,-62.524],[62.525,-46.875],[46.875,-31.225],[-26.042,-31.225],[-31.224,-26.042],[-31.224,46.875]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[124.998,124.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[20.117,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,2.858],[0,0],[-8.644,0],[0,-8.643],[0,0]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[2.857,0],[0,0],[0,-8.643],[8.644,0],[0,0],[0,20.117]],"v":[[26.042,62.525],[-46.875,62.525],[-62.525,46.875],[-46.875,31.225],[26.042,31.225],[31.224,26.041],[31.224,-46.875],[46.875,-62.525],[62.525,-46.875],[62.525,26.041]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[375.004,375.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":90,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.003,250.003,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-8.643]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[8.644,0],[0,8.643]],"v":[[171.875,15.65],[-171.875,15.65],[-187.525,0],[-171.875,-15.65],[171.875,-15.65],[187.525,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.001,250.001,0],"ix":2,"l":2},"a":{"a":0,"k":[250.001,250.001,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,8.643],[0,0],[2.858,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-20.117],[0,0]],"o":[[-8.644,0],[0,0],[0,-2.858],[0,0],[-8.644,0],[0,-8.643],[0,0],[20.117,0],[0,0],[0,8.643]],"v":[[46.875,62.525],[31.225,46.875],[31.225,-26.041],[26.041,-31.225],[-46.875,-31.225],[-62.525,-46.875],[-46.875,-62.525],[26.041,-62.525],[62.525,-26.041],[62.525,46.875]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[374.998,125.001],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,20.117],[0,0],[-8.644,0],[0,-8.643],[0,0],[-2.857,0],[0,0],[0,-8.643]],"o":[[0,0],[-20.117,0],[0,0],[0,-8.643],[8.644,0],[0,0],[0,2.858],[0,0],[8.644,0],[0,8.643]],"v":[[46.875,62.524],[-26.042,62.524],[-62.525,26.041],[-62.525,-46.875],[-46.875,-62.524],[-31.224,-46.875],[-31.224,26.041],[-26.042,31.225],[46.875,31.225],[62.525,46.875]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[124.998,374.999],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,8.643],[0,0],[-20.117,0],[0,0],[0,-8.643],[8.644,0],[0,0],[0,-2.858],[0,0]],"o":[[-8.644,0],[0,0],[0,-20.117],[0,0],[8.644,0],[0,8.643],[0,0],[-2.857,0],[0,0],[0,8.643]],"v":[[-46.875,62.524],[-62.525,46.875],[-62.525,-26.042],[-26.042,-62.524],[46.875,-62.524],[62.525,-46.875],[46.875,-31.225],[-26.042,-31.225],[-31.224,-26.042],[-31.224,46.875]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[124.998,124.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[20.117,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,2.858],[0,0],[-8.644,0],[0,-8.643],[0,0]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[2.857,0],[0,0],[0,-8.643],[8.644,0],[0,0],[0,20.117]],"v":[[26.042,62.525],[-46.875,62.525],[-62.525,46.875],[-46.875,31.225],[26.042,31.225],[31.224,26.041],[31.224,-46.875],[46.875,-62.525],[62.525,-46.875],[62.525,26.041]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[375.004,375.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.365,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[250.064,250.003,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.431,"y":1},"o":{"x":0.575,"y":0},"t":22,"s":[250.064,56.003,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.803,"y":0},"t":60,"s":[250.064,443.003,0],"to":[0,0,0],"ti":[0,0,0]},{"t":90,"s":[250.064,250.003,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[0.042,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":0,"s":[343,0]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0.346,0]},"t":7,"s":[352.076,52]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":22,"s":[406,0]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":41,"s":[406,119]},{"i":{"x":[0.667,0.667],"y":[0.639,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":60,"s":[406,0]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0.824,0]},"t":80,"s":[354.333,30]},{"t":90,"s":[343,0]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-0.504,-0.503],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":90,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.001,250.001,0],"ix":2,"l":2},"a":{"a":0,"k":[250.001,250.001,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,-11.506],[0,0]],"o":[[0,0],[11.506,0],[0,0],[0,0]],"v":[[-46.875,-46.875],[26.041,-46.875],[46.875,-26.041],[46.875,46.875]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":22,"s":[{"i":[[0,0],[0,0],[0,-11.506],[0,0]],"o":[[0,0],[11.506,0],[0,0],[0,0]],"v":[[-13.917,-79.832],[58.999,-79.832],[79.832,-58.999],[79.832,13.917]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":60,"s":[{"i":[[0,0],[0,0],[0,-11.506],[0,0]],"o":[[0,0],[11.506,0],[0,0],[0,0]],"v":[[-13.917,-79.832],[58.999,-79.832],[79.832,-58.999],[79.832,13.917]],"c":false}]},{"t":90,"s":[{"i":[[0,0],[0,0],[0,-11.506],[0,0]],"o":[[0,0],[11.506,0],[0,0],[0,0]],"v":[[-46.875,-46.875],[26.041,-46.875],[46.875,-26.041],[46.875,46.875]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,11.506],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,0]],"v":[[-203.126,296.872],[-276.042,296.872],[-296.875,276.039],[-296.875,203.123]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":22,"s":[{"i":[[0,0],[0,0],[0,11.506],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,0]],"v":[[-236.084,329.83],[-309,329.83],[-329.833,308.997],[-329.833,236.081]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":60,"s":[{"i":[[0,0],[0,0],[0,11.506],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,0]],"v":[[-236.084,329.83],[-309,329.83],[-329.833,308.997],[-329.833,236.081]],"c":false}]},{"t":90,"s":[{"i":[[0,0],[0,0],[0,11.506],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,0]],"v":[[-203.126,296.872],[-276.042,296.872],[-296.875,276.039],[-296.875,203.123]],"c":false}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[-11.506,0],[0,0]],"o":[[0,0],[0,-11.506],[0,0],[0,0]],"v":[[-296.875,46.872],[-296.875,-26.044],[-276.042,-46.878],[-203.126,-46.878]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":22,"s":[{"i":[[0,0],[0,0],[-11.506,0],[0,0]],"o":[[0,0],[0,-11.506],[0,0],[0,0]],"v":[[-329.833,13.914],[-329.833,-59.002],[-309,-79.835],[-236.084,-79.835]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":60,"s":[{"i":[[0,0],[0,0],[-11.506,0],[0,0]],"o":[[0,0],[0,-11.506],[0,0],[0,0]],"v":[[-329.833,13.914],[-329.833,-59.002],[-309,-79.835],[-236.084,-79.835]],"c":false}]},{"t":90,"s":[{"i":[[0,0],[0,0],[-11.506,0],[0,0]],"o":[[0,0],[0,-11.506],[0,0],[0,0]],"v":[[-296.875,46.872],[-296.875,-26.044],[-276.042,-46.878],[-203.126,-46.878]],"c":false}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[11.506,0],[0,0]],"o":[[0,0],[0,11.506],[0,0],[0,0]],"v":[[46.881,203.127],[46.881,276.043],[26.048,296.877],[-46.868,296.877]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":22,"s":[{"i":[[0,0],[0,0],[11.506,0],[0,0]],"o":[[0,0],[0,11.506],[0,0],[0,0]],"v":[[79.839,236.085],[79.839,309.001],[59.006,329.834],[-13.91,329.834]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":60,"s":[{"i":[[0,0],[0,0],[11.506,0],[0,0]],"o":[[0,0],[0,11.506],[0,0],[0,0]],"v":[[79.839,236.085],[79.839,309.001],[59.006,329.834],[-13.91,329.834]],"c":false}]},{"t":90,"s":[{"i":[[0,0],[0,0],[11.506,0],[0,0]],"o":[[0,0],[0,11.506],[0,0],[0,0]],"v":[[46.881,203.127],[46.881,276.043],[26.048,296.877],[-46.868,296.877]],"c":false}]}],"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[374.998,125.001],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":90,"st":0,"bm":0}]},{"id":"comp_2","nm":"morph-scan-OK","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[253.419,260.347,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[149.956,-122.947],[-31.321,57.362],[-83.54,5.208]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":100,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[253.419,260.347,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":60,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[197.252,182.153],[-31.025,182.263],[-200.744,182.309]],"c":false}]},{"t":90,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[149.956,-122.947],[-31.321,57.362],[-83.54,5.208]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":100,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":60,"s":[0]},{"t":90,"s":[28.5]}],"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[0.418]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[0]},{"i":{"x":[0.37],"y":[1]},"o":{"x":[0.333],"y":[-1.887]},"t":67,"s":[-2.777]},{"t":90,"s":[0]}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":90,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,249.974,0],"ix":2,"l":2},"a":{"a":0,"k":[250,249.999,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.57,-1.64],[2.27,-0.05],[1.65,1.56],[0.05,2.27],[-4.68,0.11],[-0.07,0],[-1.6,-1.52],[-0.05,-2.27]],"o":[[-1.57,1.64],[-2.28,0.06],[-1.65,-1.56],[-0.11,-4.69],[0.07,0],[2.19,0],[1.64,1.57],[0.06,2.26]],"v":[[6.15,5.861],[0.2,8.491],[-5.87,6.151],[-8.5,0.201],[-0.21,-8.499],[0,-8.499],[5.86,-6.149],[8.49,-0.199]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[2.67,-0.06],[-0.13,-5.51],[-1.93,-1.84],[-2.58,0],[-0.08,0],[-1.84,1.93],[0.06,2.67],[1.93,1.84]],"o":[[-5.51,0.14],[0.06,2.67],[1.88,1.79],[0.08,0],[2.67,-0.06],[1.84,-1.93],[-0.06,-2.67],[-1.94,-1.84]],"v":[[-0.24,-9.999],[-10,0.241],[-6.9,7.241],[-0.01,10.001],[0.24,10.001],[7.24,6.901],[10,-0.239],[6.9,-7.239]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250,249.999],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.3,-0.29],[0,0],[0,0],[0.29,-0.29],[-0.29,-0.29],[0,0],[-0.19,0],[-0.15,0.15],[0,0],[0.3,0.3]],"o":[[0,0],[0,0],[-0.29,-0.29],[-0.29,0.29],[0,0],[0.15,0.15],[0.19,0],[0,0],[0.3,-0.29],[-0.29,-0.29]],"v":[[3.476,-3.286],[-1.504,1.694],[-3.484,-0.276],[-4.544,-0.276],[-4.544,0.784],[-2.034,3.284],[-1.504,3.504],[-0.974,3.284],[4.536,-2.226],[4.536,-3.286]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.164,250.496],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":90,"op":300,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.003,250.003,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-8.643]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[8.644,0],[0,8.643]],"v":[[171.875,15.65],[-171.875,15.65],[-187.525,0],[-171.875,-15.65],[171.875,-15.65],[187.525,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.001,250.001,0],"ix":2,"l":2},"a":{"a":0,"k":[250.001,250.001,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,8.643],[0,0],[2.858,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-20.117],[0,0]],"o":[[-8.644,0],[0,0],[0,-2.858],[0,0],[-8.644,0],[0,-8.643],[0,0],[20.117,0],[0,0],[0,8.643]],"v":[[46.875,62.525],[31.225,46.875],[31.225,-26.041],[26.041,-31.225],[-46.875,-31.225],[-62.525,-46.875],[-46.875,-62.525],[26.041,-62.525],[62.525,-26.041],[62.525,46.875]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[374.998,125.001],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,20.117],[0,0],[-8.644,0],[0,-8.643],[0,0],[-2.857,0],[0,0],[0,-8.643]],"o":[[0,0],[-20.117,0],[0,0],[0,-8.643],[8.644,0],[0,0],[0,2.858],[0,0],[8.644,0],[0,8.643]],"v":[[46.875,62.524],[-26.042,62.524],[-62.525,26.041],[-62.525,-46.875],[-46.875,-62.524],[-31.224,-46.875],[-31.224,26.041],[-26.042,31.225],[46.875,31.225],[62.525,46.875]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[124.998,374.999],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,8.643],[0,0],[-20.117,0],[0,0],[0,-8.643],[8.644,0],[0,0],[0,-2.858],[0,0]],"o":[[-8.644,0],[0,0],[0,-20.117],[0,0],[8.644,0],[0,8.643],[0,0],[-2.857,0],[0,0],[0,8.643]],"v":[[-46.875,62.524],[-62.525,46.875],[-62.525,-26.042],[-26.042,-62.524],[46.875,-62.524],[62.525,-46.875],[46.875,-31.225],[-26.042,-31.225],[-31.224,-26.042],[-31.224,46.875]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[124.998,124.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[20.117,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,2.858],[0,0],[-8.644,0],[0,-8.643],[0,0]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[2.857,0],[0,0],[0,-8.643],[8.644,0],[0,0],[0,20.117]],"v":[[26.042,62.525],[-46.875,62.525],[-62.525,46.875],[-46.875,31.225],[26.042,31.225],[31.224,26.041],[31.224,-46.875],[46.875,-62.525],[62.525,-46.875],[62.525,26.041]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[375.004,375.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.365,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[250.064,250.003,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.431,"y":1},"o":{"x":0.575,"y":0},"t":22,"s":[250.064,56.003,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.803,"y":0},"t":60,"s":[250.064,443.003,0],"to":[0,0,0],"ti":[0,0,0]},{"t":90,"s":[250.064,250.003,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[0.042,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":0,"s":[343,0]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0.346,0]},"t":7,"s":[352.076,52]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":22,"s":[406,0]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":41,"s":[406,119]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":60,"s":[406,0]},{"t":90,"s":[343,0]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-0.504,-0.503],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.001,250.001,0],"ix":2,"l":2},"a":{"a":0,"k":[250.001,250.001,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,-11.506],[0,0]],"o":[[0,0],[11.506,0],[0,0],[0,0]],"v":[[-46.875,-46.875],[26.041,-46.875],[46.875,-26.041],[46.875,46.875]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":22,"s":[{"i":[[0,0],[0,0],[0,-11.506],[0,0]],"o":[[0,0],[11.506,0],[0,0],[0,0]],"v":[[-13.917,-79.832],[58.999,-79.832],[79.832,-58.999],[79.832,13.917]],"c":false}]},{"i":{"x":0.221,"y":1},"o":{"x":0.6,"y":0},"t":60,"s":[{"i":[[0,0],[0,0],[0,-11.506],[0,0]],"o":[[0,0],[11.506,0],[0,0],[0,0]],"v":[[-13.917,-79.832],[58.999,-79.832],[79.832,-58.999],[79.832,13.917]],"c":false}]},{"t":90,"s":[{"i":[[0,0],[0,0],[0,-105.64],[0,0]],"o":[[0,0],[105.64,0],[0,0],[0,0]],"v":[[-124.924,-67.612],[-123.4,-67.615],[67.878,123.663],[67.875,124.613]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,11.506],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,0]],"v":[[-203.126,296.872],[-276.042,296.872],[-296.875,276.039],[-296.875,203.123]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":22,"s":[{"i":[[0,0],[0,0],[0,11.506],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,0]],"v":[[-236.084,329.83],[-309,329.83],[-329.833,308.997],[-329.833,236.081]],"c":false}]},{"i":{"x":0.221,"y":1},"o":{"x":0.6,"y":0},"t":60,"s":[{"i":[[0,0],[0,0],[0,11.506],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,0]],"v":[[-236.084,329.83],[-309,329.83],[-329.833,308.997],[-329.833,236.081]],"c":false}]},{"t":90,"s":[{"i":[[0,0],[0,0],[0,105.64],[0,0]],"o":[[0,0],[-105.64,0],[0,0],[0,0]],"v":[[-124.928,316.796],[-126.451,315.673],[-317.73,124.395],[-316.584,120.576]],"c":false}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[-11.506,0],[0,0]],"o":[[0,0],[0,-11.506],[0,0],[0,0]],"v":[[-296.875,46.872],[-296.875,-26.044],[-276.042,-46.878],[-203.126,-46.878]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":22,"s":[{"i":[[0,0],[0,0],[-11.506,0],[0,0]],"o":[[0,0],[0,-11.506],[0,0],[0,0]],"v":[[-329.833,13.914],[-329.833,-59.002],[-309,-79.835],[-236.084,-79.835]],"c":false}]},{"i":{"x":0.221,"y":1},"o":{"x":0.6,"y":0},"t":60,"s":[{"i":[[0,0],[0,0],[-11.506,0],[0,0]],"o":[[0,0],[0,-11.506],[0,0],[0,0]],"v":[[-329.833,13.914],[-329.833,-59.002],[-309,-79.835],[-236.084,-79.835]],"c":false}]},{"t":90,"s":[{"i":[[0,0],[0,0],[-105.64,0],[0,0]],"o":[[0,0],[0,-105.64],[0,0],[0,0]],"v":[[-317.741,121.714],[-317.736,123.634],[-126.458,-67.644],[-124.934,-67.668]],"c":false}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[11.506,0],[0,0]],"o":[[0,0],[0,11.506],[0,0],[0,0]],"v":[[46.881,203.127],[46.881,276.043],[26.048,296.877],[-46.868,296.877]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":22,"s":[{"i":[[0,0],[0,0],[11.506,0],[0,0]],"o":[[0,0],[0,11.506],[0,0],[0,0]],"v":[[79.839,236.085],[79.839,309.001],[59.006,329.834],[-13.91,329.834]],"c":false}]},{"i":{"x":0.221,"y":1},"o":{"x":0.6,"y":0},"t":60,"s":[{"i":[[0,0],[0,0],[11.506,0],[0,0]],"o":[[0,0],[0,11.506],[0,0],[0,0]],"v":[[79.839,236.085],[79.839,309.001],[59.006,329.834],[-13.91,329.834]],"c":false}]},{"t":90,"s":[{"i":[[0,0],[0,0],[105.64,0],[0,0]],"o":[[0,0],[0,105.64],[0,0],[0,0]],"v":[[68.002,124.062],[67.944,124.438],[-123.334,315.716],[-123.136,315.157]],"c":false}]}],"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[374.998,125.001],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":90,"st":0,"bm":0}]},{"id":"comp_3","nm":"morph-scan-WRONG","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.236],"y":[1]},"o":{"x":[0.538],"y":[0]},"t":60,"s":[45]},{"t":90,"s":[90]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.236,"y":1},"o":{"x":0.538,"y":0},"t":60,"s":[150.004,439.003,0],"to":[16.667,-31.5,0],"ti":[-16.667,31.5,0]},{"t":90,"s":[250.004,250.003,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-67.709,67.709],[67.709,-67.709]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":60,"op":90,"st":30,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.236],"y":[1]},"o":{"x":[0.538],"y":[0]},"t":60,"s":[135]},{"t":90,"s":[90]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.236,"y":1},"o":{"x":0.538,"y":0},"t":60,"s":[350.004,439.003,0],"to":[-16.667,-31.5,0],"ti":[16.667,31.5,0]},{"t":90,"s":[250.004,250.003,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[67.709,67.709],[-67.709,-67.709]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":60,"op":90,"st":30,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.002,249.974,0],"ix":2,"l":2},"a":{"a":0,"k":[250,249.999,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.57,-1.64],[2.27,-0.05],[1.64,1.56],[0.05,2.27],[-1.57,1.64],[-2.27,0.05],[-0.07,0],[-1.6,-1.52],[-0.05,-2.27]],"o":[[-1.57,1.64],[-2.26,0.06],[-1.64,-1.57],[-0.05,-2.26],[1.57,-1.64],[0.07,0],[2.19,0],[1.64,1.57],[0.05,2.25]],"v":[[6.15,5.861],[0.2,8.491],[-5.87,6.151],[-8.5,0.201],[-6.15,-5.859],[-0.2,-8.489],[0.01,-8.489],[5.87,-6.139],[8.5,-0.189]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[2.66,-0.06],[1.84,-1.93],[-0.06,-2.67],[-1.93,-1.84],[-2.58,0],[-0.08,0],[-1.84,1.93],[0.06,2.67],[1.93,1.84]],"o":[[-2.67,0.06],[-1.84,1.94],[0.06,2.67],[1.88,1.79],[0.08,0],[2.67,-0.06],[1.84,-1.93],[-0.06,-2.67],[-1.94,-1.84]],"v":[[-0.24,-9.999],[-7.24,-6.899],[-10,0.241],[-6.9,7.241],[-0.01,10.001],[0.24,10.001],[7.24,6.901],[10,-0.239],[6.9,-7.239]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250,249.999],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.29,0.29],[0.29,-0.29],[0,0],[0,0],[0.29,-0.29],[-0.29,-0.29],[0,0],[0,0],[-0.29,-0.29],[-0.19,0],[-0.15,0.15],[0,0],[0,0],[-0.19,0],[-0.15,0.15],[0.29,0.29],[0,0],[0,0]],"o":[[-0.29,-0.29],[0,0],[0,0],[-0.29,-0.29],[-0.29,0.29],[0,0],[0,0],[-0.29,0.29],[0.15,0.15],[0.19,0],[0,0],[0,0],[0.15,0.15],[0.19,0],[0.29,-0.29],[0,0],[0,0],[0.29,-0.29]],"v":[[3.78,-3.781],[2.72,-3.781],[0,-1.061],[-2.72,-3.781],[-3.78,-3.781],[-3.78,-2.721],[-1.06,-0.001],[-3.78,2.719],[-3.78,3.779],[-3.25,3.999],[-2.72,3.779],[0,1.059],[2.72,3.779],[3.25,3.999],[3.78,3.779],[3.78,2.719],[1.06,-0.001],[3.78,-2.721]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250,250.001],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":90,"op":330,"st":30,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[253.419,260.347,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[149.956,-122.947],[-31.321,57.362],[-83.54,5.208]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":100,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.003,250.003,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-8.643]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[8.644,0],[0,8.643]],"v":[[171.875,15.65],[-171.875,15.65],[-187.525,0],[-171.875,-15.65],[171.875,-15.65],[187.525,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.001,250.001,0],"ix":2,"l":2},"a":{"a":0,"k":[250.001,250.001,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,8.643],[0,0],[2.858,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-20.117],[0,0]],"o":[[-8.644,0],[0,0],[0,-2.858],[0,0],[-8.644,0],[0,-8.643],[0,0],[20.117,0],[0,0],[0,8.643]],"v":[[46.875,62.525],[31.225,46.875],[31.225,-26.041],[26.041,-31.225],[-46.875,-31.225],[-62.525,-46.875],[-46.875,-62.525],[26.041,-62.525],[62.525,-26.041],[62.525,46.875]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[374.998,125.001],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,20.117],[0,0],[-8.644,0],[0,-8.643],[0,0],[-2.857,0],[0,0],[0,-8.643]],"o":[[0,0],[-20.117,0],[0,0],[0,-8.643],[8.644,0],[0,0],[0,2.858],[0,0],[8.644,0],[0,8.643]],"v":[[46.875,62.524],[-26.042,62.524],[-62.525,26.041],[-62.525,-46.875],[-46.875,-62.524],[-31.224,-46.875],[-31.224,26.041],[-26.042,31.225],[46.875,31.225],[62.525,46.875]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[124.998,374.999],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,8.643],[0,0],[-20.117,0],[0,0],[0,-8.643],[8.644,0],[0,0],[0,-2.858],[0,0]],"o":[[-8.644,0],[0,0],[0,-20.117],[0,0],[8.644,0],[0,8.643],[0,0],[-2.857,0],[0,0],[0,8.643]],"v":[[-46.875,62.524],[-62.525,46.875],[-62.525,-26.042],[-26.042,-62.524],[46.875,-62.524],[62.525,-46.875],[46.875,-31.225],[-26.042,-31.225],[-31.224,-26.042],[-31.224,46.875]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[124.998,124.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[20.117,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,2.858],[0,0],[-8.644,0],[0,-8.643],[0,0]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[2.857,0],[0,0],[0,-8.643],[8.644,0],[0,0],[0,20.117]],"v":[[26.042,62.525],[-46.875,62.525],[-62.525,46.875],[-46.875,31.225],[26.042,31.225],[31.224,26.041],[31.224,-46.875],[46.875,-62.525],[62.525,-46.875],[62.525,26.041]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[375.004,375.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.365,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[250.064,250.003,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.431,"y":1},"o":{"x":0.575,"y":0},"t":22,"s":[250.064,56.003,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.803,"y":0},"t":60,"s":[250.064,443.003,0],"to":[0,0,0],"ti":[0,0,0]},{"t":90,"s":[250.064,250.003,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[0.042,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":0,"s":[343,0]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0.346,0]},"t":7,"s":[352.076,52]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":22,"s":[406,0]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":41,"s":[406,119]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":60,"s":[406,0]},{"t":90,"s":[343,0]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-0.504,-0.503],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.001,250.001,0],"ix":2,"l":2},"a":{"a":0,"k":[250.001,250.001,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,-11.506],[0,0]],"o":[[0,0],[11.506,0],[0,0],[0,0]],"v":[[-46.875,-46.875],[26.041,-46.875],[46.875,-26.041],[46.875,46.875]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":22,"s":[{"i":[[0,0],[0,0],[0,-11.506],[0,0]],"o":[[0,0],[11.506,0],[0,0],[0,0]],"v":[[-13.917,-79.832],[58.999,-79.832],[79.832,-58.999],[79.832,13.917]],"c":false}]},{"i":{"x":0.221,"y":1},"o":{"x":0.6,"y":0},"t":60,"s":[{"i":[[0,0],[0,0],[0,-11.506],[0,0]],"o":[[0,0],[11.506,0],[0,0],[0,0]],"v":[[-13.917,-79.832],[58.999,-79.832],[79.832,-58.999],[79.832,13.917]],"c":false}]},{"t":90,"s":[{"i":[[0,0],[0,0],[0,-105.64],[0,0]],"o":[[0,0],[105.64,0],[0,0],[0,0]],"v":[[-124.924,-67.612],[-123.4,-67.615],[67.878,123.663],[67.875,124.613]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,11.506],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,0]],"v":[[-203.126,296.872],[-276.042,296.872],[-296.875,276.039],[-296.875,203.123]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":22,"s":[{"i":[[0,0],[0,0],[0,11.506],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,0]],"v":[[-236.084,329.83],[-309,329.83],[-329.833,308.997],[-329.833,236.081]],"c":false}]},{"i":{"x":0.221,"y":1},"o":{"x":0.6,"y":0},"t":60,"s":[{"i":[[0,0],[0,0],[0,11.506],[0,0]],"o":[[0,0],[-11.506,0],[0,0],[0,0]],"v":[[-236.084,329.83],[-309,329.83],[-329.833,308.997],[-329.833,236.081]],"c":false}]},{"t":90,"s":[{"i":[[0,0],[0,0],[0,105.64],[0,0]],"o":[[0,0],[-105.64,0],[0,0],[0,0]],"v":[[-124.928,316.796],[-126.451,315.673],[-317.73,124.395],[-316.584,120.576]],"c":false}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[-11.506,0],[0,0]],"o":[[0,0],[0,-11.506],[0,0],[0,0]],"v":[[-296.875,46.872],[-296.875,-26.044],[-276.042,-46.878],[-203.126,-46.878]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":22,"s":[{"i":[[0,0],[0,0],[-11.506,0],[0,0]],"o":[[0,0],[0,-11.506],[0,0],[0,0]],"v":[[-329.833,13.914],[-329.833,-59.002],[-309,-79.835],[-236.084,-79.835]],"c":false}]},{"i":{"x":0.221,"y":1},"o":{"x":0.6,"y":0},"t":60,"s":[{"i":[[0,0],[0,0],[-11.506,0],[0,0]],"o":[[0,0],[0,-11.506],[0,0],[0,0]],"v":[[-329.833,13.914],[-329.833,-59.002],[-309,-79.835],[-236.084,-79.835]],"c":false}]},{"t":90,"s":[{"i":[[0,0],[0,0],[-105.64,0],[0,0]],"o":[[0,0],[0,-105.64],[0,0],[0,0]],"v":[[-317.741,121.714],[-317.736,123.634],[-126.458,-67.644],[-124.934,-67.668]],"c":false}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[11.506,0],[0,0]],"o":[[0,0],[0,11.506],[0,0],[0,0]],"v":[[46.881,203.127],[46.881,276.043],[26.048,296.877],[-46.868,296.877]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":22,"s":[{"i":[[0,0],[0,0],[11.506,0],[0,0]],"o":[[0,0],[0,11.506],[0,0],[0,0]],"v":[[79.839,236.085],[79.839,309.001],[59.006,329.834],[-13.91,329.834]],"c":false}]},{"i":{"x":0.221,"y":1},"o":{"x":0.6,"y":0},"t":60,"s":[{"i":[[0,0],[0,0],[11.506,0],[0,0]],"o":[[0,0],[0,11.506],[0,0],[0,0]],"v":[[79.839,236.085],[79.839,309.001],[59.006,329.834],[-13.91,329.834]],"c":false}]},{"t":90,"s":[{"i":[[0,0],[0,0],[105.64,0],[0,0]],"o":[[0,0],[0,105.64],[0,0],[0,0]],"v":[[68.002,124.062],[67.944,124.438],[-123.334,315.716],[-123.136,315.157]],"c":false}]}],"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[374.998,125.001],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":90,"st":0,"bm":0}]},{"id":"comp_4","nm":"loop-scan","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.001,250.001,0],"ix":2,"l":2},"a":{"a":0,"k":[250.001,250.001,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,8.643],[0,0],[2.858,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-20.117],[0,0]],"o":[[-8.644,0],[0,0],[0,-2.858],[0,0],[-8.644,0],[0,-8.643],[0,0],[20.117,0],[0,0],[0,8.643]],"v":[[46.875,62.525],[31.225,46.875],[31.225,-26.041],[26.041,-31.225],[-46.875,-31.225],[-62.525,-46.875],[-46.875,-62.525],[26.041,-62.525],[62.525,-26.041],[62.525,46.875]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[374.998,125.001],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,20.117],[0,0],[-8.644,0],[0,-8.643],[0,0],[-2.857,0],[0,0],[0,-8.643]],"o":[[0,0],[-20.117,0],[0,0],[0,-8.643],[8.644,0],[0,0],[0,2.858],[0,0],[8.644,0],[0,8.643]],"v":[[46.875,62.524],[-26.042,62.524],[-62.525,26.041],[-62.525,-46.875],[-46.875,-62.524],[-31.224,-46.875],[-31.224,26.041],[-26.042,31.225],[46.875,31.225],[62.525,46.875]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[124.998,374.999],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,8.643],[0,0],[-20.117,0],[0,0],[0,-8.643],[8.644,0],[0,0],[0,-2.858],[0,0]],"o":[[-8.644,0],[0,0],[0,-20.117],[0,0],[8.644,0],[0,8.643],[0,0],[-2.857,0],[0,0],[0,8.643]],"v":[[-46.875,62.524],[-62.525,46.875],[-62.525,-26.042],[-26.042,-62.524],[46.875,-62.524],[62.525,-46.875],[46.875,-31.225],[-26.042,-31.225],[-31.224,-26.042],[-31.224,46.875]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[124.998,124.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[20.117,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,2.858],[0,0],[-8.644,0],[0,-8.643],[0,0]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[2.857,0],[0,0],[0,-8.643],[8.644,0],[0,0],[0,20.117]],"v":[[26.042,62.525],[-46.875,62.525],[-62.525,46.875],[-46.875,31.225],[26.042,31.225],[31.224,26.041],[31.224,-46.875],[46.875,-62.525],[62.525,-46.875],[62.525,26.041]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[375.004,375.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[250.064,250.003,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":25,"s":[250.064,86.003,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":65,"s":[250.064,416.003,0],"to":[0,0,0],"ti":[0,0,0]},{"t":90,"s":[250.064,250.003,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.569,0.569],"y":[3.925,-0.646]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[343,78]},{"i":{"x":[0.703,0.703],"y":[1,1]},"o":{"x":[0.321,0.321],"y":[0.247,0.493]},"t":9,"s":[344.02,63.623]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":25,"s":[328,0]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":45,"s":[343,144]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":65,"s":[332,0]},{"t":90,"s":[343,78]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('69-document-scan-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-0.504,-0.503],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":91,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"lordicon.com Outlines","cl":"com","sr":1,"ks":{"o":{"a":0,"k":20,"ix":11,"x":"var $bm_rt;\nvar checkbox = thisComp.layer('02092020').effect('02092020002')('Checkbox');\nif (checkbox == 1) {\n $bm_rt = 20;\n} else {\n $bm_rt = 0;\n}\n;"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.934,481.369,0],"ix":2,"l":2},"a":{"a":0,"k":[79.934,0.369,0],"ix":1,"l":2},"s":{"a":0,"k":[265.159,265.159,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[1.415,0],[11.014,0],[11.014,-2.523],[4.656,-2.523],[4.656,-14.809],[1.415,-14.809]],"c":true},"ix":2},"nm":"l","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"l","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[11.167,-7.199],[12.992,-1.661],[18.243,0.369],[23.514,-1.743],[25.381,-7.548],[23.494,-13.127],[18.284,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[14.49,-7.302],[15.577,-11.609],[18.305,-12.86],[21.689,-10.235],[22.058,-7.589],[21.053,-3.343],[18.284,-1.969],[15.597,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[-0.287,-0.841],[-0.144,-0.82],[0,0],[0.164,0.656],[0.226,1.743],[2.236,0.205],[0,2.769],[0.923,0.8],[1.641,-0.021],[0,0]],"o":[[0,0],[0,0],[0,0],[0.533,0],[0.205,0.574],[0,0],[-0.164,-0.246],[-0.103,-0.41],[-0.267,-1.928],[0.718,-0.205],[0,-0.964],[-1.19,-1.026],[0,0],[0,0]],"v":[[27.381,0],[30.622,0],[30.622,-5.989],[33.411,-5.989],[35.011,-5.148],[35.811,0],[39.318,0],[38.867,-1.067],[38.416,-3.938],[35.749,-7.343],[38.847,-10.973],[37.554,-13.824],[33.063,-14.829],[27.381,-14.829]],"c":true},"ix":2},"nm":"r","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-0.492,-0.349],[0,-1.005],[0.226,-0.164],[0.369,0],[0,0]],"o":[[0,0],[1.005,0],[0.287,0.185],[0,1.046],[-0.513,0.41],[0,0],[0,0]],"v":[[30.519,-12.491],[32.652,-12.491],[34.744,-12.142],[35.524,-10.481],[34.703,-8.758],[33.083,-8.348],[30.519,-8.348]],"c":true},"ix":2},"nm":"r","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"r","np":5,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-0.554,0.103],[0,4.553],[1.866,1.374],[0.82,0],[0,0]],"o":[[0,0],[1.497,0],[2.81,-0.513],[0,-2.113],[-1.784,-1.313],[0,0],[0,0]],"v":[[41.068,0],[45.683,0],[48.349,-0.164],[53.6,-7.609],[51.077,-13.434],[45.97,-14.768],[41.068,-14.788]],"c":true},"ix":2},"nm":"d","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-0.656,-0.185],[0,-2.092],[1.251,-1.251],[1.354,0],[0.349,0.021]],"o":[[1.825,-0.082],[1.99,0.554],[0,0.718],[-0.923,0.923],[-0.369,0],[0,0]],"v":[[44.288,-12.388],[47.611,-12.183],[50.318,-7.609],[48.985,-3.425],[45.539,-2.4],[44.288,-2.441]],"c":true},"ix":2},"nm":"d","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"d","np":5,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[55.669,0],[58.849,0],[58.849,-14.87],[55.669,-14.87]],"c":true},"ix":2},"nm":"i","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"i","np":3,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[3.241,0],[0,-4.697],[-5.107,0],[-1.313,1.354],[-0.062,0.882],[0,0],[1.333,0],[0,0.882],[-2.359,0],[-0.062,-0.513]],"o":[[0,-2.954],[-4.164,0],[0,3.671],[1.354,0],[1.19,-1.231],[0,0],[-0.062,1.969],[-3.097,0],[0,-3.056],[2.154,0],[0,0]],"v":[[73.104,-9.989],[67.587,-14.911],[60.798,-7.097],[67.566,0.349],[71.894,-1.313],[73.227,-4.799],[69.884,-4.799],[67.218,-1.99],[64.121,-7.076],[67.464,-12.593],[69.864,-9.989]],"c":true},"ix":2},"nm":"c","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"c","np":3,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[74.546,-7.199],[76.372,-1.661],[81.622,0.369],[86.894,-1.743],[88.76,-7.548],[86.873,-13.127],[81.663,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[77.869,-7.302],[78.956,-11.609],[81.684,-12.86],[85.068,-10.235],[85.437,-7.589],[84.432,-3.343],[81.663,-1.969],[78.977,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[91.007,0],[94.001,0],[94.001,-12.306],[99.744,0],[104.113,0],[104.113,-14.829],[101.159,-14.829],[101.159,-3.159],[95.601,-14.829],[91.007,-14.829]],"c":true},"ix":2},"nm":"n","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"n","np":3,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[106.893,0],[109.497,0],[109.497,-2.728],[106.893,-2.728]],"c":true},"ix":2},"nm":".","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":".","np":3,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[3.241,0],[0,-4.697],[-5.107,0],[-1.313,1.354],[-0.062,0.882],[0,0],[1.333,0],[0,0.882],[-2.359,0],[-0.062,-0.513]],"o":[[0,-2.954],[-4.164,0],[0,3.671],[1.354,0],[1.19,-1.231],[0,0],[-0.062,1.969],[-3.097,0],[0,-3.056],[2.154,0],[0,0]],"v":[[124.04,-9.989],[118.523,-14.911],[111.734,-7.097],[118.502,0.349],[122.83,-1.313],[124.163,-4.799],[120.82,-4.799],[118.154,-1.99],[115.057,-7.076],[118.4,-12.593],[120.8,-9.989]],"c":true},"ix":2},"nm":"c","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"c","np":3,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[125.482,-7.199],[127.308,-1.661],[132.558,0.369],[137.829,-1.743],[139.696,-7.548],[137.809,-13.127],[132.599,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[128.805,-7.302],[129.892,-11.609],[132.62,-12.86],[136.004,-10.235],[136.373,-7.589],[135.368,-3.343],[132.599,-1.969],[129.912,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[141.696,0],[144.67,0],[144.67,-12.716],[148.629,0],[151.254,0],[155.295,-12.716],[155.295,0],[158.453,0],[158.453,-14.829],[153.408,-14.829],[150.024,-4.041],[146.885,-14.829],[141.696,-14.829]],"c":true},"ix":2},"nm":"m","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"m","np":3,"cix":2,"bm":0,"ix":12,"mn":"ADBE Vector Group","hd":false}],"ip":2.5,"op":25,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":3,"nm":"02092020","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-105,15,0],"ix":2,"l":2},"a":{"a":0,"k":[60,60,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"02092020002","np":3,"mn":"ADBE Checkbox Control","ix":1,"en":1,"ef":[{"ty":7,"nm":"Checkbox","mn":"ADBE Checkbox Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"Color & Stroke Change","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-55,-102,0],"ix":2,"l":2,"x":"var $bm_rt;\n$bm_rt = effect('Axis')('Point');"},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2,"x":"var $bm_rt;\nvar temp;\ntemp = effect('Scale')('Slider');\n$bm_rt = [\n temp,\n temp\n];"}},"ao":0,"ef":[{"ty":5,"nm":"Primary","np":3,"mn":"ADBE Color Control","ix":1,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]},{"ty":5,"nm":"Axis","np":3,"mn":"ADBE Point Control","ix":2,"en":1,"ef":[{"ty":3,"nm":"Point","mn":"ADBE Point Control-0001","ix":1,"v":{"a":0,"k":[250,250],"ix":1}}]},{"ty":5,"nm":"Scale","np":3,"mn":"ADBE Slider Control","ix":3,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":100,"ix":1}}]},{"ty":5,"nm":"State-Intro","np":3,"mn":"ADBE Slider Control","ix":4,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]},{"ty":5,"nm":"State-Hover","np":3,"mn":"ADBE Slider Control","ix":5,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":1,"ix":1}}]},{"ty":5,"nm":"State-Morph-Scan-OK","np":3,"mn":"ADBE Slider Control","ix":6,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]},{"ty":5,"nm":"State-Morph-Scan-WRONG","np":3,"mn":"ADBE Slider Control","ix":7,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]},{"ty":5,"nm":"State-Loop","np":3,"mn":"ADBE Slider Control","ix":8,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"in-scan","parent":3,"refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Intro')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":101,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"hover-scan","parent":3,"refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Hover')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":101,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"morph-scan-OK","parent":3,"refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Morph-Scan-OK')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":101,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"morph-scan-WRONG","parent":3,"refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Morph-Scan-WRONG')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":101,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":0,"nm":"loop-scan","parent":3,"refId":"comp_4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Loop')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":101,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/frontend/public/lotties/system-outline-82-extension.json b/frontend/public/lotties/system-outline-82-extension.json deleted file mode 100644 index 1a3f6b7d3..000000000 --- a/frontend/public/lotties/system-outline-82-extension.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.8.1","fr":60,"ip":0,"op":61,"w":500,"h":500,"nm":"82-extension-outline","ddd":0,"assets":[{"id":"comp_0","nm":"in-extension","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,2.81],[0,0],[0,0],[0,35.97],[-12.144,12.432],[-0.087,0.085],[-17.194,0],[0,0],[0,0],[2.81,0],[0,0],[0,8.643],[0,0],[7.854,7.698],[0.074,0.075],[11.159,0],[0,-22.961],[0,0],[8.644,0],[0,0],[0,-2.81],[0,0],[8.644,0],[0,0],[7.697,-7.854],[0.075,-0.074],[0,-11.159],[-22.962,0],[0,0],[0,-8.643],[0,0],[-2.81,0],[0,0],[0,0],[-35.97,0],[-12.431,-12.143],[-0.085,-0.087],[0,-17.191]],"o":[[0,0],[2.81,0],[0,0],[0,0],[-35.97,0],[0,-17.19],[0.084,-0.086],[12.563,-12.279],[0,0],[0,0],[0,-2.81],[0,0],[-8.644,0],[0,0],[0,-11.158],[-0.075,-0.073],[-7.696,-7.854],[-22.962,0],[0,0],[0,8.643],[0,0],[-2.81,0],[0,0],[0,8.643],[0,0],[-11.158,0],[-0.073,0.075],[-7.854,7.697],[0,22.961],[0,0],[8.644,0],[0,0],[0,2.81],[0,0],[0,0],[0,-35.855],[17.192,0],[0.087,0.085],[12.143,12.431],[0,0]],"v":[[96.279,156.229],[151.046,156.229],[156.229,151.045],[156.229,96.486],[148.546,96.486],[83.312,31.253],[102.143,-14.683],[102.399,-14.939],[148.546,-33.98],[156.229,-33.98],[156.229,-88.539],[151.046,-93.723],[88.546,-93.723],[72.896,-109.372],[72.896,-135.414],[60.717,-164.655],[60.493,-164.878],[31.254,-177.057],[-10.388,-135.414],[-10.388,-109.372],[-26.038,-93.723],[-88.538,-93.723],[-93.722,-88.539],[-93.722,-26.039],[-109.372,-10.389],[-135.414,-10.389],[-164.654,1.79],[-164.877,2.013],[-177.056,31.253],[-135.414,72.895],[-109.372,72.895],[-93.722,88.545],[-93.722,151.045],[-88.538,156.229],[-33.98,156.229],[-33.98,148.337],[31.254,83.312],[77.19,102.142],[77.449,102.401],[96.279,148.337]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[20.117,0],[0,0],[0,8.643],[0,0],[6.315,6.532],[8.909,0],[0,-18.596],[0,0],[8.644,0],[0,0],[0,20.117],[0,0],[0,0],[0,40.22],[-13.837,13.621],[-19.586,0],[0,0],[0,0],[-20.117,0],[0,0],[0,0],[-40.221,0],[-13.62,-13.838],[0,-19.586],[0,0],[0,0],[0,-20.117],[0,0],[8.644,0],[0,0],[6.662,-6.449],[0,-8.91],[-18.711,0],[0,0],[0,-8.643],[0,0]],"o":[[0,0],[-8.644,0],[0,0],[0,-8.909],[-6.531,-6.316],[-18.711,0],[0,0],[0,8.643],[0,0],[-20.117,0],[0,0],[0,0],[-40.221,0],[0,-19.584],[13.622,-13.841],[0,0],[0,0],[0,-20.117],[0,0],[0,0],[0,-40.22],[19.585,0],[13.841,13.622],[0,0],[0,0],[20.117,0],[0,0],[0,8.643],[0,0],[-8.911,0],[-6.318,6.533],[0,18.711],[0,0],[8.644,0],[0,0],[0,20.117]],"v":[[151.046,187.528],[80.629,187.528],[64.979,171.879],[64.979,148.337],[55.188,124.405],[31.254,114.611],[-2.68,148.337],[-2.68,171.879],[-18.33,187.528],[-88.538,187.528],[-125.022,151.045],[-125.022,104.195],[-135.414,104.195],[-208.356,31.253],[-186.9,-20.228],[-135.414,-41.689],[-125.022,-41.689],[-125.022,-88.539],[-88.538,-125.022],[-41.688,-125.022],[-41.688,-135.414],[31.254,-208.356],[82.735,-186.9],[104.196,-135.414],[104.196,-125.022],[151.046,-125.022],[187.53,-88.539],[187.53,-18.33],[171.88,-2.681],[148.546,-2.681],[124.408,7.317],[114.612,31.253],[148.546,65.187],[171.88,65.187],[187.53,80.837],[187.53,151.045]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('82-extension-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.235],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":18,"s":[0]},{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[-90]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":42,"s":[-90]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":53,"s":[13]},{"t":60,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.235,"y":1},"o":{"x":0.5,"y":0},"t":18,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":0.5},"o":{"x":0.333,"y":0.333},"t":30,"s":[291.748,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.268,"y":1},"o":{"x":0.5,"y":0},"t":42,"s":[291.748,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":53,"s":[281.647,281.334,0]}],"ix":2,"l":2},"a":{"a":0,"k":[42.06,41.748,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":-1,"s":[{"i":[[0.001,-0.001],[0,-0.002],[0,0],[-0.001,0],[0,0],[0,0],[0.081,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,-0.001],[0,0],[0,0],[0,0.081],[0,0],[0,0],[0,0.001],[0,0],[-0.001,0.001],[-0.001,0],[0,0],[0,0],[-0.081,0],[0,0],[0,0],[-0.001,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,-0.081],[0,0],[0,0]],"o":[[-0.001,0.001],[0,0],[0,0.001],[0,0],[0,0],[0,0.081],[0,0],[0,0],[0,0],[0,0],[0,0],[-0.001,0],[0,0],[0,0],[-0.081,0],[0,0],[0,0],[-0.001,0],[0,0],[0,-0.001],[0.001,-0.001],[0,0],[0,0],[0,-0.081],[0,0],[0,0],[0,-0.001],[0,0],[0,0],[0,0],[0,0],[0,0],[0.081,0],[0,0],[0,0],[-0.002,0]],"v":[[43.056,41.398],[43.055,41.401],[43.055,42.097],[43.057,42.099],[43.054,42.099],[43.054,42.597],[42.906,42.744],[42.407,42.744],[42.408,42.746],[42.408,42.745],[42.407,42.745],[41.708,42.745],[41.707,42.746],[41.706,42.744],[41.209,42.744],[41.061,42.597],[41.061,42.154],[41.062,42.154],[41.06,42.152],[41.06,41.345],[41.061,41.343],[41.064,41.342],[41.061,41.342],[41.061,40.899],[41.209,40.751],[41.651,40.751],[41.651,40.752],[41.653,40.75],[42.463,40.75],[42.463,40.75],[42.463,40.751],[42.464,40.751],[42.906,40.751],[43.054,40.899],[43.054,41.396],[43.06,41.396]],"c":true}]},{"i":{"x":0.5,"y":1},"o":{"x":0.333,"y":0},"t":15,"s":[{"i":[[0.146,-0.142],[0,-0.218],[0,-0.001],[-0.158,0],[0,0],[0,0],[11.458,0],[0,0],[0,0],[0.023,0.023],[0.036,0],[0.001,0],[0,-0.085],[0,0],[0,0],[0,11.458],[0,0],[0,0],[0.002,0.163],[0,0.001],[-0.083,0.081],[-0.126,0],[0,0],[0,0],[-11.458,0],[0,0],[0,0],[-0.113,0.001],[0,0],[-0.008,-0.008],[0,-0.013],[0,0],[0,0],[0,-11.458],[0,0],[0,0]],"o":[[-0.139,0.142],[0,0.002],[0.001,0.157],[0,0],[0,0],[0,11.458],[0,0],[0,0],[0,-0.036],[-0.023,-0.023],[-0.001,0],[-0.085,0.002],[0,0],[0,0],[-11.458,0],[0,0],[0,0],[-0.163,0],[0,-0.001],[0,-0.126],[0.081,-0.083],[0,0],[0,0],[0,-11.458],[0,0],[0,0],[0,-0.113],[0.001,0],[0.013,0],[0.008,0.008],[0,0],[0,0],[11.458,0],[0,0],[0,0],[-0.218,0]],"v":[[182.604,-7.728],[182.376,-7.172],[182.376,90.923],[182.663,91.207],[182.293,91.251],[182.293,161.459],[161.459,182.293],[91.042,182.293],[91.115,182.503],[91.078,182.413],[90.988,182.376],[-7.69,182.382],[-7.844,182.539],[-7.917,182.293],[-78.125,182.293],[-98.959,161.459],[-98.959,98.959],[-98.745,99.004],[-99.042,98.71],[-99.043,-15.124],[-98.908,-15.446],[-98.587,-15.58],[-98.959,-15.625],[-98.959,-78.125],[-78.125,-98.959],[-15.625,-98.959],[-15.663,-98.836],[-15.459,-99.042],[98.875,-99.043],[98.907,-99.029],[98.921,-98.997],[98.959,-98.959],[161.459,-98.959],[182.293,-78.125],[182.293,-7.917],[183.163,-7.96]],"c":true}]},{"i":{"x":0.235,"y":1},"o":{"x":0.5,"y":0},"t":18,"s":[{"i":[[0.146,-0.142],[0,-0.218],[0,-0.001],[-0.158,0],[0,0],[0,0],[11.458,0],[0,0],[0,0],[0.023,0.023],[0.036,0],[0.001,0],[0,-0.085],[0,0],[0,0],[0,11.458],[0,0],[0,0],[0.002,0.163],[0,0.001],[-0.083,0.081],[-0.126,0],[0,0],[0,0],[-11.458,0],[0,0],[0,0],[-0.113,0.001],[0,0],[-0.008,-0.008],[0,-0.013],[0,0],[0,0],[0,-11.458],[0,0],[0,0]],"o":[[-0.139,0.142],[0,0.002],[0.001,0.157],[0,0],[0,0],[0,11.458],[0,0],[0,0],[0,-0.036],[-0.023,-0.023],[-0.001,0],[-0.085,0.002],[0,0],[0,0],[-11.458,0],[0,0],[0,0],[-0.163,0],[0,-0.001],[0,-0.126],[0.081,-0.083],[0,0],[0,0],[0,-11.458],[0,0],[0,0],[0,-0.113],[0.001,0],[0.013,0],[0.008,0.008],[0,0],[0,0],[11.458,0],[0,0],[0,0],[-0.218,0]],"v":[[182.604,-7.728],[182.376,-7.172],[182.376,90.923],[182.663,91.207],[182.293,91.251],[182.293,161.459],[161.459,182.293],[91.042,182.293],[91.115,182.503],[91.078,182.413],[90.988,182.376],[-7.69,182.382],[-7.844,182.539],[-7.917,182.293],[-78.125,182.293],[-98.959,161.459],[-98.959,98.959],[-98.745,99.004],[-99.042,98.71],[-99.043,-15.124],[-98.908,-15.446],[-98.587,-15.58],[-98.959,-15.625],[-98.959,-78.125],[-78.125,-98.959],[-15.625,-98.959],[-15.663,-98.836],[-15.459,-99.042],[98.875,-99.043],[98.907,-99.029],[98.921,-98.997],[98.959,-98.959],[161.459,-98.959],[182.293,-78.125],[182.293,-7.917],[183.163,-7.96]],"c":true}]},{"i":{"x":0.5,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[{"i":[[0.146,-0.142],[0,-0.218],[0,-0.001],[-0.158,0],[0,0],[0,0],[11.458,0],[0,0],[0,0],[8.75,8.958],[13.749,0],[0.481,-0.014],[0,-26.807],[0,0],[0,0],[0,11.458],[0,0],[0,0],[0.002,0.163],[0,0.001],[-0.083,0.081],[-0.126,0],[0,0],[0,0],[-11.458,0],[0,0],[0,0],[-31.437,0.267],[-0.166,0],[-10.208,-10.417],[0,-15.834],[0,0],[0,0],[0,-11.458],[0,0],[0,0]],"o":[[-0.139,0.142],[0,0.002],[0.001,0.157],[0,0],[0,0],[0,11.458],[0,0],[0,0],[0,-13.751],[-8.958,-8.751],[-0.484,0],[-26.628,0.765],[0,0],[0,0],[-11.458,0],[0,0],[0,0],[-0.163,0],[0,-0.001],[0,-0.126],[0.081,-0.083],[0,0],[0,0],[0,-11.458],[0,0],[0,0],[0,-31.501],[0.166,-0.001],[15.834,0],[10.417,10.208],[0,0],[0,0],[11.458,0],[0,0],[0,0],[-0.218,0]],"v":[[182.604,-7.728],[182.376,-7.172],[182.376,90.923],[182.663,91.208],[182.293,91.251],[182.293,161.459],[161.459,182.293],[91.042,182.293],[91.042,158.751],[76.667,123.751],[41.667,109.376],[40.218,109.396],[-7.917,158.751],[-7.917,182.293],[-78.125,182.293],[-98.959,161.459],[-98.959,98.959],[-98.745,99.004],[-99.042,98.71],[-99.043,-15.124],[-98.908,-15.446],[-98.587,-15.58],[-98.959,-15.625],[-98.959,-78.125],[-78.125,-98.959],[-15.625,-98.959],[-15.625,-125.001],[41.168,-182.29],[41.667,-182.293],[82.084,-165.418],[98.959,-125.001],[98.959,-98.959],[161.459,-98.959],[182.293,-78.125],[182.293,-7.917],[183.163,-7.96]],"c":true}]},{"i":{"x":0.268,"y":1},"o":{"x":0.5,"y":0},"t":42,"s":[{"i":[[0.146,-0.142],[0,-0.218],[0,-0.001],[-0.158,0],[0,0],[0,0],[11.458,0],[0,0],[0,0],[8.75,8.958],[13.749,0],[0.481,-0.014],[0,-26.807],[0,0],[0,0],[0,11.458],[0,0],[0,0],[0.002,0.163],[0,0.001],[-0.083,0.081],[-0.126,0],[0,0],[0,0],[-11.458,0],[0,0],[0,0],[-31.437,0.267],[-0.166,0],[-10.208,-10.417],[0,-15.834],[0,0],[0,0],[0,-11.458],[0,0],[0,0]],"o":[[-0.139,0.142],[0,0.002],[0.001,0.157],[0,0],[0,0],[0,11.458],[0,0],[0,0],[0,-13.751],[-8.958,-8.751],[-0.484,0],[-26.628,0.765],[0,0],[0,0],[-11.458,0],[0,0],[0,0],[-0.163,0],[0,-0.001],[0,-0.126],[0.081,-0.083],[0,0],[0,0],[0,-11.458],[0,0],[0,0],[0,-31.501],[0.166,-0.001],[15.834,0],[10.417,10.208],[0,0],[0,0],[11.458,0],[0,0],[0,0],[-0.218,0]],"v":[[182.604,-7.728],[182.376,-7.172],[182.376,90.923],[182.663,91.208],[182.293,91.251],[182.293,161.459],[161.459,182.293],[91.042,182.293],[91.042,158.751],[76.667,123.751],[41.667,109.376],[40.218,109.396],[-7.917,158.751],[-7.917,182.293],[-78.125,182.293],[-98.959,161.459],[-98.959,98.959],[-98.745,99.004],[-99.042,98.71],[-99.043,-15.124],[-98.908,-15.446],[-98.587,-15.58],[-98.959,-15.625],[-98.959,-78.125],[-78.125,-98.959],[-15.625,-98.959],[-15.625,-125.001],[41.168,-182.29],[41.667,-182.293],[82.084,-165.418],[98.959,-125.001],[98.959,-98.959],[161.459,-98.959],[182.293,-78.125],[182.293,-7.917],[183.163,-7.96]],"c":true}]},{"t":53,"s":[{"i":[[9.167,-8.958],[0,-13.751],[-0.002,-0.163],[-27.336,0],[0,0],[0,0],[11.458,0],[0,0],[0,0],[8.75,8.958],[13.749,0],[0.481,-0.014],[0,-26.807],[0,0],[0,0],[0,11.458],[0,0],[0,0],[0.316,31.394],[0,0.197],[-10.417,10.208],[-15.834,0],[0,0],[0,0],[-11.458,0],[0,0],[0,0],[-31.437,0.267],[-0.166,0],[-10.208,-10.417],[0,-15.834],[0,0],[0,0],[0,-11.458],[0,0],[0,0]],"o":[[-8.75,8.958],[0,0.163],[0.262,27.069],[0,0],[0,0],[0,11.458],[0,0],[0,0],[0,-13.751],[-8.958,-8.751],[-0.484,0],[-26.628,0.765],[0,0],[0,0],[-11.458,0],[0,0],[0,0],[-31.47,0],[-0.002,-0.196],[0,-15.834],[10.208,-10.417],[0,0],[0,0],[0,-11.458],[0,0],[0,0],[0,-31.501],[0.166,-0.001],[15.834,0],[10.417,10.208],[0,0],[0,0],[11.458,0],[0,0],[0,0],[-13.751,0]],"v":[[123.751,6.667],[109.376,41.667],[109.378,42.156],[158.959,91.251],[182.293,91.251],[182.293,161.459],[161.459,182.293],[91.042,182.293],[91.042,158.751],[76.667,123.751],[41.667,109.376],[40.218,109.396],[-7.917,158.751],[-7.917,182.293],[-78.125,182.293],[-98.959,161.459],[-98.959,98.959],[-125.001,98.959],[-182.29,42.257],[-182.293,41.667],[-165.418,1.25],[-125.001,-15.625],[-98.959,-15.625],[-98.959,-78.125],[-78.125,-98.959],[-15.625,-98.959],[-15.625,-125.001],[41.168,-182.29],[41.667,-182.293],[82.084,-165.418],[98.959,-125.001],[98.959,-98.959],[161.459,-98.959],[182.293,-78.125],[182.293,-7.917],[158.959,-7.917]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('82-extension-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":0,"bm":0}]},{"id":"comp_1","nm":"hover-extension","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,2.81],[0,0],[0,0],[0,35.97],[-12.144,12.432],[-0.087,0.085],[-17.194,0],[0,0],[0,0],[2.81,0],[0,0],[0,8.643],[0,0],[7.854,7.698],[0.074,0.075],[11.159,0],[0,-22.961],[0,0],[8.644,0],[0,0],[0,-2.81],[0,0],[8.644,0],[0,0],[7.697,-7.854],[0.075,-0.074],[0,-11.159],[-22.962,0],[0,0],[0,-8.643],[0,0],[-2.81,0],[0,0],[0,0],[-35.97,0],[-12.431,-12.143],[-0.085,-0.087],[0,-17.191]],"o":[[0,0],[2.81,0],[0,0],[0,0],[-35.97,0],[0,-17.19],[0.084,-0.086],[12.563,-12.279],[0,0],[0,0],[0,-2.81],[0,0],[-8.644,0],[0,0],[0,-11.158],[-0.075,-0.073],[-7.696,-7.854],[-22.962,0],[0,0],[0,8.643],[0,0],[-2.81,0],[0,0],[0,8.643],[0,0],[-11.158,0],[-0.073,0.075],[-7.854,7.697],[0,22.961],[0,0],[8.644,0],[0,0],[0,2.81],[0,0],[0,0],[0,-35.855],[17.192,0],[0.087,0.085],[12.143,12.431],[0,0]],"v":[[96.279,156.229],[151.046,156.229],[156.229,151.045],[156.229,96.486],[148.546,96.486],[83.312,31.253],[102.143,-14.683],[102.399,-14.939],[148.546,-33.98],[156.229,-33.98],[156.229,-88.539],[151.046,-93.723],[88.546,-93.723],[72.896,-109.372],[72.896,-135.414],[60.717,-164.655],[60.493,-164.878],[31.254,-177.057],[-10.388,-135.414],[-10.388,-109.372],[-26.038,-93.723],[-88.538,-93.723],[-93.722,-88.539],[-93.722,-26.039],[-109.372,-10.389],[-135.414,-10.389],[-164.654,1.79],[-164.877,2.013],[-177.056,31.253],[-135.414,72.895],[-109.372,72.895],[-93.722,88.545],[-93.722,151.045],[-88.538,156.229],[-33.98,156.229],[-33.98,148.337],[31.254,83.312],[77.19,102.142],[77.449,102.401],[96.279,148.337]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[20.117,0],[0,0],[0,8.643],[0,0],[6.315,6.532],[8.909,0],[0,-18.596],[0,0],[8.644,0],[0,0],[0,20.117],[0,0],[0,0],[0,40.22],[-13.837,13.621],[-19.586,0],[0,0],[0,0],[-20.117,0],[0,0],[0,0],[-40.221,0],[-13.62,-13.838],[0,-19.586],[0,0],[0,0],[0,-20.117],[0,0],[8.644,0],[0,0],[6.662,-6.449],[0,-8.91],[-18.711,0],[0,0],[0,-8.643],[0,0]],"o":[[0,0],[-8.644,0],[0,0],[0,-8.909],[-6.531,-6.316],[-18.711,0],[0,0],[0,8.643],[0,0],[-20.117,0],[0,0],[0,0],[-40.221,0],[0,-19.584],[13.622,-13.841],[0,0],[0,0],[0,-20.117],[0,0],[0,0],[0,-40.22],[19.585,0],[13.841,13.622],[0,0],[0,0],[20.117,0],[0,0],[0,8.643],[0,0],[-8.911,0],[-6.318,6.533],[0,18.711],[0,0],[8.644,0],[0,0],[0,20.117]],"v":[[151.046,187.528],[80.629,187.528],[64.979,171.879],[64.979,148.337],[55.188,124.405],[31.254,114.611],[-2.68,148.337],[-2.68,171.879],[-18.33,187.528],[-88.538,187.528],[-125.022,151.045],[-125.022,104.195],[-135.414,104.195],[-208.356,31.253],[-186.9,-20.228],[-135.414,-41.689],[-125.022,-41.689],[-125.022,-88.539],[-88.538,-125.022],[-41.688,-125.022],[-41.688,-135.414],[31.254,-208.356],[82.735,-186.9],[104.196,-135.414],[104.196,-125.022],[151.046,-125.022],[187.53,-88.539],[187.53,-18.33],[171.88,-2.681],[148.546,-2.681],[124.408,7.317],[114.612,31.253],[148.546,65.187],[171.88,65.187],[187.53,80.837],[187.53,151.045]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('82-extension-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,2.81],[0,0],[0,0],[0,35.97],[-12.144,12.432],[-0.087,0.085],[-17.194,0],[0,0],[0,0],[2.81,0],[0,0],[0,8.643],[0,0],[7.854,7.698],[0.074,0.075],[11.159,0],[0,-22.961],[0,0],[8.644,0],[0,0],[0,-2.81],[0,0],[8.644,0],[0,0],[7.697,-7.854],[0.075,-0.074],[0,-11.159],[-22.962,0],[0,0],[0,-8.643],[0,0],[-2.81,0],[0,0],[0,0],[-35.97,0],[-12.431,-12.143],[-0.085,-0.087],[0,-17.191]],"o":[[0,0],[2.81,0],[0,0],[0,0],[-35.97,0],[0,-17.19],[0.084,-0.086],[12.563,-12.279],[0,0],[0,0],[0,-2.81],[0,0],[-8.644,0],[0,0],[0,-11.158],[-0.075,-0.073],[-7.696,-7.854],[-22.962,0],[0,0],[0,8.643],[0,0],[-2.81,0],[0,0],[0,8.643],[0,0],[-11.158,0],[-0.073,0.075],[-7.854,7.697],[0,22.961],[0,0],[8.644,0],[0,0],[0,2.81],[0,0],[0,0],[0,-35.855],[17.192,0],[0.087,0.085],[12.143,12.431],[0,0]],"v":[[96.279,156.229],[151.046,156.229],[156.229,151.045],[156.229,96.486],[148.546,96.486],[83.312,31.253],[102.143,-14.683],[102.399,-14.939],[148.546,-33.98],[156.229,-33.98],[156.229,-88.539],[151.046,-93.723],[88.546,-93.723],[72.896,-109.372],[72.896,-135.414],[60.717,-164.655],[60.493,-164.878],[31.254,-177.057],[-10.388,-135.414],[-10.388,-109.372],[-26.038,-93.723],[-88.538,-93.723],[-93.722,-88.539],[-93.722,-26.039],[-109.372,-10.389],[-135.414,-10.389],[-164.654,1.79],[-164.877,2.013],[-177.056,31.253],[-135.414,72.895],[-109.372,72.895],[-93.722,88.545],[-93.722,151.045],[-88.538,156.229],[-33.98,156.229],[-33.98,148.337],[31.254,83.312],[77.19,102.142],[77.449,102.401],[96.279,148.337]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[20.117,0],[0,0],[0,8.643],[0,0],[6.315,6.532],[8.909,0],[0,-18.596],[0,0],[8.644,0],[0,0],[0,20.117],[0,0],[0,0],[0,40.22],[-13.837,13.621],[-19.586,0],[0,0],[0,0],[-20.117,0],[0,0],[0,0],[-40.221,0],[-13.62,-13.838],[0,-19.586],[0,0],[0,0],[0,-20.117],[0,0],[8.644,0],[0,0],[6.662,-6.449],[0,-8.91],[-18.711,0],[0,0],[0,-8.643],[0,0]],"o":[[0,0],[-8.644,0],[0,0],[0,-8.909],[-6.531,-6.316],[-18.711,0],[0,0],[0,8.643],[0,0],[-20.117,0],[0,0],[0,0],[-40.221,0],[0,-19.584],[13.622,-13.841],[0,0],[0,0],[0,-20.117],[0,0],[0,0],[0,-40.22],[19.585,0],[13.841,13.622],[0,0],[0,0],[20.117,0],[0,0],[0,8.643],[0,0],[-8.911,0],[-6.318,6.533],[0,18.711],[0,0],[8.644,0],[0,0],[0,20.117]],"v":[[151.046,187.528],[80.629,187.528],[64.979,171.879],[64.979,148.337],[55.188,124.405],[31.254,114.611],[-2.68,148.337],[-2.68,171.879],[-18.33,187.528],[-88.538,187.528],[-125.022,151.045],[-125.022,104.195],[-135.414,104.195],[-208.356,31.253],[-186.9,-20.228],[-135.414,-41.689],[-125.022,-41.689],[-125.022,-88.539],[-88.538,-125.022],[-41.688,-125.022],[-41.688,-135.414],[31.254,-208.356],[82.735,-186.9],[104.196,-135.414],[104.196,-125.022],[151.046,-125.022],[187.53,-88.539],[187.53,-18.33],[171.88,-2.681],[148.546,-2.681],[124.408,7.317],[114.612,31.253],[148.546,65.187],[171.88,65.187],[187.53,80.837],[187.53,151.045]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('82-extension-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.22],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":1,"s":[0]},{"t":60,"s":[90]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.22,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[239.587,239.586,0],"to":[14,-0.167,0],"ti":[-14,0.167,0]},{"t":60,"s":[323.587,238.586,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.22,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[9.167,-8.958],[0,-13.751],[-27.5,0],[0,0],[0,0],[11.458,0],[0,0],[0,0],[8.75,8.958],[13.749,0],[0,-27.292],[0,0],[0,0],[0,11.458],[0,0],[0,0],[0,31.666],[-10.417,10.208],[-15.834,0],[0,0],[0,0],[-11.458,0],[0,0],[0,0],[-31.667,0],[-10.208,-10.417],[0,-15.834],[0,0],[0,0],[0,-11.458],[0,0],[0,0]],"o":[[-8.75,8.958],[0,27.292],[0,0],[0,0],[0,11.458],[0,0],[0,0],[0,-13.751],[-8.958,-8.751],[-27.292,0],[0,0],[0,0],[-11.458,0],[0,0],[0,0],[-31.667,0],[0,-15.834],[10.208,-10.417],[0,0],[0,0],[0,-11.458],[0,0],[0,0],[0,-31.667],[15.834,0],[10.417,10.208],[0,0],[0,0],[11.458,0],[0,0],[0,0],[-13.751,0]],"v":[[123.751,6.667],[109.376,41.667],[158.959,91.251],[182.293,91.251],[182.293,161.459],[161.459,182.293],[91.042,182.293],[91.042,158.751],[76.667,123.751],[41.667,109.376],[-7.917,158.751],[-7.917,182.293],[-78.125,182.293],[-98.959,161.459],[-98.959,98.959],[-125.001,98.959],[-182.293,41.667],[-165.418,1.25],[-125.001,-15.625],[-98.959,-15.625],[-98.959,-78.125],[-78.125,-98.959],[-15.625,-98.959],[-15.625,-125.001],[41.667,-182.293],[82.084,-165.418],[98.959,-125.001],[98.959,-98.959],[161.459,-98.959],[182.293,-78.125],[182.293,-7.917],[158.959,-7.917]],"c":true}]},{"t":60,"s":[{"i":[[9.167,-8.958],[0,-13.751],[-27.5,0],[0,0],[0,0],[11.458,0],[0,0],[0,0],[10.106,-10.233],[15.881,0],[0,31.176],[0,0],[0,0],[0,11.458],[0,0],[0,0],[0,31.666],[-10.417,10.208],[-15.834,0],[0,0],[0,0],[-11.458,0],[0,0],[0,0],[-27.5,0],[-8.865,9.068],[0,13.784],[0,0],[0,0],[0,-11.458],[0,0],[0,0]],"o":[[-8.75,8.958],[0,27.292],[0,0],[0,0],[0,11.458],[0,0],[0,0],[0,15.708],[-10.347,9.996],[-31.522,0],[0,0],[0,0],[-11.458,0],[0,0],[0,0],[-31.667,0],[0,-15.834],[10.208,-10.417],[0,0],[0,0],[0,-11.458],[0,0],[0,0],[0,27.568],[13.75,0],[9.046,-8.887],[0,0],[0,0],[11.458,0],[0,0],[0,0],[-13.751,0]],"v":[[123.751,6.667],[109.376,41.667],[158.959,91.251],[182.293,91.251],[182.293,161.459],[161.459,182.293],[98.711,182.293],[98.711,209.185],[82.108,249.166],[41.683,265.587],[-15.586,209.185],[-15.586,182.293],[-78.125,182.293],[-98.959,161.459],[-98.959,98.959],[-125.001,98.959],[-182.293,41.667],[-165.418,1.25],[-125.001,-15.625],[-98.959,-15.625],[-98.959,-78.125],[-78.125,-98.959],[-8.086,-98.959],[-8.086,-76.288],[41.667,-26.413],[76.765,-41.104],[91.42,-76.288],[91.42,-98.959],[161.459,-98.959],[182.293,-78.125],[182.293,-7.917],[158.959,-7.917]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('82-extension-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"lordicon.com Outlines","cl":"com","sr":1,"ks":{"o":{"a":0,"k":20,"ix":11,"x":"var $bm_rt;\nvar checkbox = thisComp.layer('02092020').effect('02092020002')('Checkbox');\nif (checkbox == 1) {\n $bm_rt = 20;\n} else {\n $bm_rt = 0;\n}\n;"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.934,481.369,0],"ix":2,"l":2},"a":{"a":0,"k":[79.934,0.369,0],"ix":1,"l":2},"s":{"a":0,"k":[265.159,265.159,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[1.415,0],[11.014,0],[11.014,-2.523],[4.656,-2.523],[4.656,-14.809],[1.415,-14.809]],"c":true},"ix":2},"nm":"l","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"l","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[11.167,-7.199],[12.992,-1.661],[18.243,0.369],[23.514,-1.743],[25.381,-7.548],[23.494,-13.127],[18.284,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[14.49,-7.302],[15.577,-11.609],[18.305,-12.86],[21.689,-10.235],[22.058,-7.589],[21.053,-3.343],[18.284,-1.969],[15.597,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[-0.287,-0.841],[-0.144,-0.82],[0,0],[0.164,0.656],[0.226,1.743],[2.236,0.205],[0,2.769],[0.923,0.8],[1.641,-0.021],[0,0]],"o":[[0,0],[0,0],[0,0],[0.533,0],[0.205,0.574],[0,0],[-0.164,-0.246],[-0.103,-0.41],[-0.267,-1.928],[0.718,-0.205],[0,-0.964],[-1.19,-1.026],[0,0],[0,0]],"v":[[27.381,0],[30.622,0],[30.622,-5.989],[33.411,-5.989],[35.011,-5.148],[35.811,0],[39.318,0],[38.867,-1.067],[38.416,-3.938],[35.749,-7.343],[38.847,-10.973],[37.554,-13.824],[33.063,-14.829],[27.381,-14.829]],"c":true},"ix":2},"nm":"r","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-0.492,-0.349],[0,-1.005],[0.226,-0.164],[0.369,0],[0,0]],"o":[[0,0],[1.005,0],[0.287,0.185],[0,1.046],[-0.513,0.41],[0,0],[0,0]],"v":[[30.519,-12.491],[32.652,-12.491],[34.744,-12.142],[35.524,-10.481],[34.703,-8.758],[33.083,-8.348],[30.519,-8.348]],"c":true},"ix":2},"nm":"r","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"r","np":5,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-0.554,0.103],[0,4.553],[1.866,1.374],[0.82,0],[0,0]],"o":[[0,0],[1.497,0],[2.81,-0.513],[0,-2.113],[-1.784,-1.313],[0,0],[0,0]],"v":[[41.068,0],[45.683,0],[48.349,-0.164],[53.6,-7.609],[51.077,-13.434],[45.97,-14.768],[41.068,-14.788]],"c":true},"ix":2},"nm":"d","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-0.656,-0.185],[0,-2.092],[1.251,-1.251],[1.354,0],[0.349,0.021]],"o":[[1.825,-0.082],[1.99,0.554],[0,0.718],[-0.923,0.923],[-0.369,0],[0,0]],"v":[[44.288,-12.388],[47.611,-12.183],[50.318,-7.609],[48.985,-3.425],[45.539,-2.4],[44.288,-2.441]],"c":true},"ix":2},"nm":"d","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"d","np":5,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[55.669,0],[58.849,0],[58.849,-14.87],[55.669,-14.87]],"c":true},"ix":2},"nm":"i","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"i","np":3,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[3.241,0],[0,-4.697],[-5.107,0],[-1.313,1.354],[-0.062,0.882],[0,0],[1.333,0],[0,0.882],[-2.359,0],[-0.062,-0.513]],"o":[[0,-2.954],[-4.164,0],[0,3.671],[1.354,0],[1.19,-1.231],[0,0],[-0.062,1.969],[-3.097,0],[0,-3.056],[2.154,0],[0,0]],"v":[[73.104,-9.989],[67.587,-14.911],[60.798,-7.097],[67.566,0.349],[71.894,-1.313],[73.227,-4.799],[69.884,-4.799],[67.218,-1.99],[64.121,-7.076],[67.464,-12.593],[69.864,-9.989]],"c":true},"ix":2},"nm":"c","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"c","np":3,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[74.546,-7.199],[76.372,-1.661],[81.622,0.369],[86.894,-1.743],[88.76,-7.548],[86.873,-13.127],[81.663,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[77.869,-7.302],[78.956,-11.609],[81.684,-12.86],[85.068,-10.235],[85.437,-7.589],[84.432,-3.343],[81.663,-1.969],[78.977,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[91.007,0],[94.001,0],[94.001,-12.306],[99.744,0],[104.113,0],[104.113,-14.829],[101.159,-14.829],[101.159,-3.159],[95.601,-14.829],[91.007,-14.829]],"c":true},"ix":2},"nm":"n","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"n","np":3,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[106.893,0],[109.497,0],[109.497,-2.728],[106.893,-2.728]],"c":true},"ix":2},"nm":".","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":".","np":3,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[3.241,0],[0,-4.697],[-5.107,0],[-1.313,1.354],[-0.062,0.882],[0,0],[1.333,0],[0,0.882],[-2.359,0],[-0.062,-0.513]],"o":[[0,-2.954],[-4.164,0],[0,3.671],[1.354,0],[1.19,-1.231],[0,0],[-0.062,1.969],[-3.097,0],[0,-3.056],[2.154,0],[0,0]],"v":[[124.04,-9.989],[118.523,-14.911],[111.734,-7.097],[118.502,0.349],[122.83,-1.313],[124.163,-4.799],[120.82,-4.799],[118.154,-1.99],[115.057,-7.076],[118.4,-12.593],[120.8,-9.989]],"c":true},"ix":2},"nm":"c","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"c","np":3,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[125.482,-7.199],[127.308,-1.661],[132.558,0.369],[137.829,-1.743],[139.696,-7.548],[137.809,-13.127],[132.599,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[128.805,-7.302],[129.892,-11.609],[132.62,-12.86],[136.004,-10.235],[136.373,-7.589],[135.368,-3.343],[132.599,-1.969],[129.912,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[141.696,0],[144.67,0],[144.67,-12.716],[148.629,0],[151.254,0],[155.295,-12.716],[155.295,0],[158.453,0],[158.453,-14.829],[153.408,-14.829],[150.024,-4.041],[146.885,-14.829],[141.696,-14.829]],"c":true},"ix":2},"nm":"m","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"m","np":3,"cix":2,"bm":0,"ix":12,"mn":"ADBE Vector Group","hd":false}],"ip":2.5,"op":25,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":3,"nm":"02092020","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-105,15,0],"ix":2,"l":2},"a":{"a":0,"k":[60,60,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"02092020002","np":3,"mn":"ADBE Checkbox Control","ix":1,"en":1,"ef":[{"ty":7,"nm":"Checkbox","mn":"ADBE Checkbox Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"Color & Stroke Change","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-55,-102,0],"ix":2,"l":2,"x":"var $bm_rt;\n$bm_rt = effect('Axis')('Point');"},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2,"x":"var $bm_rt;\nvar temp;\ntemp = effect('Scale')('Slider');\n$bm_rt = [\n temp,\n temp\n];"}},"ao":0,"ef":[{"ty":5,"nm":"Primary","np":3,"mn":"ADBE Color Control","ix":1,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]},{"ty":5,"nm":"Axis","np":3,"mn":"ADBE Point Control","ix":2,"en":1,"ef":[{"ty":3,"nm":"Point","mn":"ADBE Point Control-0001","ix":1,"v":{"a":0,"k":[250,250],"ix":1}}]},{"ty":5,"nm":"Scale","np":3,"mn":"ADBE Slider Control","ix":3,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":100,"ix":1}}]},{"ty":5,"nm":"State-Intro","np":3,"mn":"ADBE Slider Control","ix":4,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]},{"ty":5,"nm":"State-Hover","np":3,"mn":"ADBE Slider Control","ix":5,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":1,"ix":1}}]}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"in-extension","parent":3,"refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Intro')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":71,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"hover-extension","parent":3,"refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Hover')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":71,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/frontend/public/lotties/system-outline-90-lock-closed.json b/frontend/public/lotties/system-outline-90-lock-closed.json deleted file mode 100644 index 0c17d8343..000000000 --- a/frontend/public/lotties/system-outline-90-lock-closed.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.8.1","fr":60,"ip":0,"op":31,"w":500,"h":500,"nm":"90-lock-closed-outline","ddd":0,"assets":[{"id":"comp_0","nm":"in-lock","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[14.346,0],[0,0],[0,14.346],[0,0],[0,0]],"o":[[0,14.346],[0,0],[-14.346,0],[0,0],[0,0],[0,0]],"v":[[135.388,151.044],[109.372,177.06],[-109.377,177.06],[-135.394,151.044],[-135.394,-10.393],[135.388,-10.393]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-48.809,0],[0,0],[0,-48.809],[0,0],[0,0]],"o":[[0,-48.809],[0,0],[48.809,0],[0,0],[0,0],[0,0]],"v":[[-93.727,-88.54],[-5.21,-177.057],[5.206,-177.057],[93.723,-88.54],[93.723,-41.693],[-93.727,-41.693]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,0],[66.067,0],[0,0],[0,-66.067],[0,0],[0,0],[0,-8.643],[0,0],[-31.604,0],[0,0],[0,31.604],[0,0]],"o":[[0,0],[0,0],[0,-66.067],[0,0],[-66.067,0],[0,0],[0,0],[-8.644,0],[0,0],[0,31.604],[0,0],[31.604,0],[0,0],[0,-8.643]],"v":[[151.038,-41.693],[125.023,-41.693],[125.023,-88.54],[5.206,-208.357],[-5.21,-208.357],[-125.027,-88.54],[-125.027,-41.693],[-151.044,-41.693],[-166.694,-26.043],[-166.694,151.044],[-109.377,208.36],[109.372,208.36],[166.688,151.044],[166.688,-26.043]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.04,-0.42],[-0.07,-0.41],[-0.08,-0.41],[-0.24,-0.8],[-0.32,-0.77],[-0.4,-0.73],[-0.46,-0.7],[-0.24,-0.34],[-0.27,-0.32],[-0.28,-0.31],[-0.3,-0.29],[-0.311,-0.28],[-0.33,-0.26],[-0.204,-0.157],[0,0],[-8.644,0],[0,8.643],[0,0],[-0.189,0.159],[-0.311,0.28],[-0.301,0.3],[-0.28,0.31],[-0.27,0.32],[-0.25,0.33],[-0.23,0.34],[-0.39,0.74],[-0.32,0.77],[-0.25,0.8],[-0.16,0.81],[-0.06,0.41],[-0.05,0.42],[-0.021,0.42],[0,0.42],[0.02,0.42],[0.04,0.42],[0.06,0.42],[0.08,0.41],[0.24,0.8],[0.319,0.77],[0.39,0.74],[0.46,0.69],[0.25,0.33],[0.26,0.33],[0.279,0.31],[0.29,0.3],[0.31,0.28],[0.329,0.26],[0.34,0.25],[0.35,0.23],[0.73,0.39],[0.77,0.32],[0.79,0.25],[0.819,0.16],[0.41,0.06],[0.41,0.04],[0.42,0.02],[0.829,-0.04],[0.42,-0.04],[0.41,-0.06],[0.409,-0.08],[0.8,-0.24],[0.77,-0.32],[0.73,-0.39],[0.7,-0.46],[0.34,-0.25],[0.319,-0.26],[0.31,-0.28],[0.29,-0.29],[0.28,-0.31],[0.261,-0.32],[0.25,-0.34],[0.23,-0.35],[0.39,-0.73],[0.31,-0.77],[0.239,-0.8],[0.17,-0.81],[0.06,-0.41],[0.04,-0.41],[0.03,-0.42],[0,-0.42],[-0.02,-0.42]],"o":[[0.04,0.42],[0.06,0.41],[0.17,0.81],[0.239,0.8],[0.31,0.77],[0.39,0.74],[0.23,0.34],[0.25,0.33],[0.261,0.32],[0.28,0.31],[0.29,0.3],[0.31,0.28],[0.195,0.165],[0,0],[0,8.643],[8.644,0],[0,0],[0.196,-0.153],[0.329,-0.26],[0.31,-0.28],[0.29,-0.29],[0.279,-0.31],[0.26,-0.32],[0.25,-0.34],[0.46,-0.7],[0.39,-0.73],[0.319,-0.77],[0.24,-0.8],[0.08,-0.41],[0.06,-0.41],[0.04,-0.42],[0.02,-0.42],[0,-0.42],[-0.021,-0.42],[-0.05,-0.41],[-0.06,-0.41],[-0.16,-0.81],[-0.25,-0.8],[-0.32,-0.77],[-0.39,-0.73],[-0.23,-0.35],[-0.25,-0.34],[-0.27,-0.32],[-0.28,-0.31],[-0.301,-0.29],[-0.311,-0.28],[-0.32,-0.26],[-0.33,-0.25],[-0.689,-0.46],[-0.739,-0.39],[-0.77,-0.32],[-0.8,-0.24],[-0.4,-0.08],[-0.42,-0.06],[-0.42,-0.04],[-0.84,-0.04],[-0.421,0.02],[-0.42,0.04],[-0.41,0.06],[-0.811,0.16],[-0.8,0.25],[-0.771,0.32],[-0.74,0.39],[-0.34,0.23],[-0.33,0.25],[-0.33,0.26],[-0.311,0.28],[-0.3,0.3],[-0.28,0.31],[-0.27,0.33],[-0.24,0.33],[-0.46,0.69],[-0.4,0.74],[-0.32,0.77],[-0.24,0.8],[-0.08,0.41],[-0.07,0.42],[-0.04,0.42],[-0.02,0.42],[0,0.42],[0.03,0.42]],"v":[[-25.495,-16.37],[-25.335,-15.12],[-25.125,-13.88],[-24.505,-11.45],[-23.655,-9.09],[-22.585,-6.82],[-21.306,-4.66],[-20.585,-3.63],[-19.806,-2.64],[-18.985,-1.68],[-18.115,-0.77],[-17.205,0.1],[-16.245,0.92],[-15.641,1.396],[-15.641,28.86],[0.01,44.51],[15.66,28.86],[15.66,1.381],[16.245,0.92],[17.205,0.1],[18.125,-0.77],[18.995,-1.68],[19.814,-2.64],[20.585,-3.63],[21.305,-4.66],[22.595,-6.82],[23.665,-9.09],[24.515,-11.45],[25.125,-13.88],[25.345,-15.12],[25.505,-16.37],[25.595,-17.63],[25.625,-18.89],[25.595,-20.15],[25.505,-21.41],[25.345,-22.66],[25.125,-23.9],[24.515,-26.33],[23.665,-28.69],[22.595,-30.96],[21.305,-33.11],[20.585,-34.14],[19.814,-35.14],[18.995,-36.09],[18.125,-37.01],[17.205,-37.88],[16.245,-38.7],[15.255,-39.47],[14.225,-40.19],[12.074,-41.48],[9.805,-42.55],[7.444,-43.4],[5.005,-44.01],[3.774,-44.23],[2.524,-44.39],[1.265,-44.48],[-1.255,-44.48],[-2.516,-44.39],[-3.766,-44.23],[-5.005,-44.01],[-7.436,-43.4],[-9.795,-42.55],[-12.065,-41.48],[-14.226,-40.19],[-15.255,-39.47],[-16.245,-38.7],[-17.205,-37.88],[-18.115,-37.01],[-18.985,-36.09],[-19.806,-35.14],[-20.585,-34.14],[-21.306,-33.11],[-22.585,-30.96],[-23.655,-28.69],[-24.505,-26.33],[-25.125,-23.9],[-25.335,-22.66],[-25.495,-21.41],[-25.596,-20.15],[-25.625,-18.89],[-25.596,-17.63]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,81],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":30,"op":316,"st":30,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.318],"y":[1.007]},"o":{"x":[0.167],"y":[0.167]},"t":1,"s":[0]},{"i":{"x":[0.29],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":16,"s":[-24]},{"t":30,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.318,"y":0.999},"o":{"x":0.167,"y":0.167},"t":1,"s":[311.997,439.333,0],"to":[-6.083,-89.417,0],"ti":[18.083,13.417,0]},{"i":{"x":0.186,"y":0.995},"o":{"x":0.333,"y":0},"t":13,"s":[223.997,307.333,0],"to":[8.167,6.833,0],"ti":[-2.75,-4.5,0]},{"t":27,"s":[249.997,333.333,0]}],"ix":2,"l":2},"a":{"a":0,"k":[249.997,333.333,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":1,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[2.183,22.836],[2.183,22.836]],"c":false}]},{"t":8,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[0,0]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":51.25,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,312.498],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":1,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[2.183,-3.642],[3.27,-3.448]],"c":false}]},{"t":8,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-26.478],[0,26.478]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[250.005,333.768],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":1,"s":[{"i":[[0,0],[0,0],[-23.012,0],[0,0],[0,23.012],[0,0]],"o":[[0,0],[0,23.012],[0,0],[23.013,0],[0,0],[0,0]],"v":[[-149.003,-4.562],[-148.755,0.919],[-107.088,42.586],[111.66,42.586],[153.327,0.919],[153.079,-4.562]],"c":true}]},{"t":8,"s":[{"i":[[0,0],[0,0],[-23.012,0],[0,0],[0,23.012],[0,0]],"o":[[0,0],[0,23.012],[0,0],[23.013,0],[0,0],[0,0]],"v":[[-151.041,-109.377],[-151.041,67.71],[-109.374,109.377],[109.374,109.377],[151.041,67.71],[151.041,-109.377]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[249.997,333.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":30,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":1,"s":[140.623,321.957,0],"to":[0,-16.333,0],"ti":[0,16.333,0]},{"t":8,"s":[140.623,223.957,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-109.375,83.332,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":16.426,"s":[{"i":[[0,0],[0,0],[57.53,0],[0,0],[0,-57.53],[0,0]],"o":[[0,0],[0,-57.53],[0,0],[-57.53,0],[0,0],[0,0]],"v":[[109.248,4.832],[109.375,5.835],[5.208,-98.332],[-5.208,-98.332],[-109.375,5.835],[-109.375,83.332]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":21.361,"s":[{"i":[[0,0],[0,0],[57.53,0],[0,0],[0,-57.53],[0,0]],"o":[[0,0],[0,-57.53],[0,0],[-57.53,0],[0,0],[0,0]],"v":[[109.375,83.332],[109.754,74.178],[5.587,-29.989],[-4.829,-29.989],[-108.996,74.178],[-109.375,83.332]],"c":false}]},{"t":30,"s":[{"i":[[0,0],[0,0],[57.53,0],[0,0],[0,-57.53],[0,0]],"o":[[0,0],[0,-57.53],[0,0],[-57.53,0],[0,0],[0,0]],"v":[[109.375,83.332],[109.375,20.835],[5.208,-83.332],[-5.208,-83.332],[-109.375,20.835],[-109.375,83.332]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.1],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":1,"s":[100]},{"t":12.10546875,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":30,"st":0,"bm":0}]},{"id":"comp_1","nm":"hover-lock","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[14.346,0],[0,0],[0,14.346],[0,0],[0,0]],"o":[[0,14.346],[0,0],[-14.346,0],[0,0],[0,0],[0,0]],"v":[[135.388,151.044],[109.372,177.06],[-109.377,177.06],[-135.394,151.044],[-135.394,-10.393],[135.388,-10.393]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-48.809,0],[0,0],[0,-48.809],[0,0],[0,0]],"o":[[0,-48.809],[0,0],[48.809,0],[0,0],[0,0],[0,0]],"v":[[-93.727,-88.54],[-5.21,-177.057],[5.206,-177.057],[93.723,-88.54],[93.723,-41.693],[-93.727,-41.693]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,0],[66.067,0],[0,0],[0,-66.067],[0,0],[0,0],[0,-8.643],[0,0],[-31.604,0],[0,0],[0,31.604],[0,0]],"o":[[0,0],[0,0],[0,-66.067],[0,0],[-66.067,0],[0,0],[0,0],[-8.644,0],[0,0],[0,31.604],[0,0],[31.604,0],[0,0],[0,-8.643]],"v":[[151.038,-41.693],[125.023,-41.693],[125.023,-88.54],[5.206,-208.357],[-5.21,-208.357],[-125.027,-88.54],[-125.027,-41.693],[-151.044,-41.693],[-166.694,-26.043],[-166.694,151.044],[-109.377,208.36],[109.372,208.36],[166.688,151.044],[166.688,-26.043]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.04,-0.42],[-0.07,-0.41],[-0.08,-0.41],[-0.24,-0.8],[-0.32,-0.77],[-0.4,-0.73],[-0.46,-0.7],[-0.24,-0.34],[-0.27,-0.32],[-0.28,-0.31],[-0.3,-0.29],[-0.311,-0.28],[-0.33,-0.26],[-0.204,-0.157],[0,0],[-8.644,0],[0,8.643],[0,0],[-0.189,0.159],[-0.311,0.28],[-0.301,0.3],[-0.28,0.31],[-0.27,0.32],[-0.25,0.33],[-0.23,0.34],[-0.39,0.74],[-0.32,0.77],[-0.25,0.8],[-0.16,0.81],[-0.06,0.41],[-0.05,0.42],[-0.021,0.42],[0,0.42],[0.02,0.42],[0.04,0.42],[0.06,0.42],[0.08,0.41],[0.24,0.8],[0.319,0.77],[0.39,0.74],[0.46,0.69],[0.25,0.33],[0.26,0.33],[0.279,0.31],[0.29,0.3],[0.31,0.28],[0.329,0.26],[0.34,0.25],[0.35,0.23],[0.73,0.39],[0.77,0.32],[0.79,0.25],[0.819,0.16],[0.41,0.06],[0.41,0.04],[0.42,0.02],[0.829,-0.04],[0.42,-0.04],[0.41,-0.06],[0.409,-0.08],[0.8,-0.24],[0.77,-0.32],[0.73,-0.39],[0.7,-0.46],[0.34,-0.25],[0.319,-0.26],[0.31,-0.28],[0.29,-0.29],[0.28,-0.31],[0.261,-0.32],[0.25,-0.34],[0.23,-0.35],[0.39,-0.73],[0.31,-0.77],[0.239,-0.8],[0.17,-0.81],[0.06,-0.41],[0.04,-0.41],[0.03,-0.42],[0,-0.42],[-0.02,-0.42]],"o":[[0.04,0.42],[0.06,0.41],[0.17,0.81],[0.239,0.8],[0.31,0.77],[0.39,0.74],[0.23,0.34],[0.25,0.33],[0.261,0.32],[0.28,0.31],[0.29,0.3],[0.31,0.28],[0.195,0.165],[0,0],[0,8.643],[8.644,0],[0,0],[0.196,-0.153],[0.329,-0.26],[0.31,-0.28],[0.29,-0.29],[0.279,-0.31],[0.26,-0.32],[0.25,-0.34],[0.46,-0.7],[0.39,-0.73],[0.319,-0.77],[0.24,-0.8],[0.08,-0.41],[0.06,-0.41],[0.04,-0.42],[0.02,-0.42],[0,-0.42],[-0.021,-0.42],[-0.05,-0.41],[-0.06,-0.41],[-0.16,-0.81],[-0.25,-0.8],[-0.32,-0.77],[-0.39,-0.73],[-0.23,-0.35],[-0.25,-0.34],[-0.27,-0.32],[-0.28,-0.31],[-0.301,-0.29],[-0.311,-0.28],[-0.32,-0.26],[-0.33,-0.25],[-0.689,-0.46],[-0.739,-0.39],[-0.77,-0.32],[-0.8,-0.24],[-0.4,-0.08],[-0.42,-0.06],[-0.42,-0.04],[-0.84,-0.04],[-0.421,0.02],[-0.42,0.04],[-0.41,0.06],[-0.811,0.16],[-0.8,0.25],[-0.771,0.32],[-0.74,0.39],[-0.34,0.23],[-0.33,0.25],[-0.33,0.26],[-0.311,0.28],[-0.3,0.3],[-0.28,0.31],[-0.27,0.33],[-0.24,0.33],[-0.46,0.69],[-0.4,0.74],[-0.32,0.77],[-0.24,0.8],[-0.08,0.41],[-0.07,0.42],[-0.04,0.42],[-0.02,0.42],[0,0.42],[0.03,0.42]],"v":[[-25.495,-16.37],[-25.335,-15.12],[-25.125,-13.88],[-24.505,-11.45],[-23.655,-9.09],[-22.585,-6.82],[-21.306,-4.66],[-20.585,-3.63],[-19.806,-2.64],[-18.985,-1.68],[-18.115,-0.77],[-17.205,0.1],[-16.245,0.92],[-15.641,1.396],[-15.641,28.86],[0.01,44.51],[15.66,28.86],[15.66,1.381],[16.245,0.92],[17.205,0.1],[18.125,-0.77],[18.995,-1.68],[19.814,-2.64],[20.585,-3.63],[21.305,-4.66],[22.595,-6.82],[23.665,-9.09],[24.515,-11.45],[25.125,-13.88],[25.345,-15.12],[25.505,-16.37],[25.595,-17.63],[25.625,-18.89],[25.595,-20.15],[25.505,-21.41],[25.345,-22.66],[25.125,-23.9],[24.515,-26.33],[23.665,-28.69],[22.595,-30.96],[21.305,-33.11],[20.585,-34.14],[19.814,-35.14],[18.995,-36.09],[18.125,-37.01],[17.205,-37.88],[16.245,-38.7],[15.255,-39.47],[14.225,-40.19],[12.074,-41.48],[9.805,-42.55],[7.444,-43.4],[5.005,-44.01],[3.774,-44.23],[2.524,-44.39],[1.265,-44.48],[-1.255,-44.48],[-2.516,-44.39],[-3.766,-44.23],[-5.005,-44.01],[-7.436,-43.4],[-9.795,-42.55],[-12.065,-41.48],[-14.226,-40.19],[-15.255,-39.47],[-16.245,-38.7],[-17.205,-37.88],[-18.115,-37.01],[-18.985,-36.09],[-19.806,-35.14],[-20.585,-34.14],[-21.306,-33.11],[-22.585,-30.96],[-23.655,-28.69],[-24.505,-26.33],[-25.125,-23.9],[-25.335,-22.66],[-25.495,-21.41],[-25.596,-20.15],[-25.625,-18.89],[-25.596,-17.63]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,81],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":30,"op":302,"st":-30,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[14.346,0],[0,0],[0,14.346],[0,0],[0,0]],"o":[[0,14.346],[0,0],[-14.346,0],[0,0],[0,0],[0,0]],"v":[[135.388,151.044],[109.372,177.06],[-109.377,177.06],[-135.394,151.044],[-135.394,-10.393],[135.388,-10.393]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-48.809,0],[0,0],[0,-48.809],[0,0],[0,0]],"o":[[0,-48.809],[0,0],[48.809,0],[0,0],[0,0],[0,0]],"v":[[-93.727,-88.54],[-5.21,-177.057],[5.206,-177.057],[93.723,-88.54],[93.723,-41.693],[-93.727,-41.693]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,0],[66.067,0],[0,0],[0,-66.067],[0,0],[0,0],[0,-8.643],[0,0],[-31.604,0],[0,0],[0,31.604],[0,0]],"o":[[0,0],[0,0],[0,-66.067],[0,0],[-66.067,0],[0,0],[0,0],[-8.644,0],[0,0],[0,31.604],[0,0],[31.604,0],[0,0],[0,-8.643]],"v":[[151.038,-41.693],[125.023,-41.693],[125.023,-88.54],[5.206,-208.357],[-5.21,-208.357],[-125.027,-88.54],[-125.027,-41.693],[-151.044,-41.693],[-166.694,-26.043],[-166.694,151.044],[-109.377,208.36],[109.372,208.36],[166.688,151.044],[166.688,-26.043]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.04,-0.42],[-0.07,-0.41],[-0.08,-0.41],[-0.24,-0.8],[-0.32,-0.77],[-0.4,-0.73],[-0.46,-0.7],[-0.24,-0.34],[-0.27,-0.32],[-0.28,-0.31],[-0.3,-0.29],[-0.311,-0.28],[-0.33,-0.26],[-0.204,-0.157],[0,0],[-8.644,0],[0,8.643],[0,0],[-0.189,0.159],[-0.311,0.28],[-0.301,0.3],[-0.28,0.31],[-0.27,0.32],[-0.25,0.33],[-0.23,0.34],[-0.39,0.74],[-0.32,0.77],[-0.25,0.8],[-0.16,0.81],[-0.06,0.41],[-0.05,0.42],[-0.021,0.42],[0,0.42],[0.02,0.42],[0.04,0.42],[0.06,0.42],[0.08,0.41],[0.24,0.8],[0.319,0.77],[0.39,0.74],[0.46,0.69],[0.25,0.33],[0.26,0.33],[0.279,0.31],[0.29,0.3],[0.31,0.28],[0.329,0.26],[0.34,0.25],[0.35,0.23],[0.73,0.39],[0.77,0.32],[0.79,0.25],[0.819,0.16],[0.41,0.06],[0.41,0.04],[0.42,0.02],[0.829,-0.04],[0.42,-0.04],[0.41,-0.06],[0.409,-0.08],[0.8,-0.24],[0.77,-0.32],[0.73,-0.39],[0.7,-0.46],[0.34,-0.25],[0.319,-0.26],[0.31,-0.28],[0.29,-0.29],[0.28,-0.31],[0.261,-0.32],[0.25,-0.34],[0.23,-0.35],[0.39,-0.73],[0.31,-0.77],[0.239,-0.8],[0.17,-0.81],[0.06,-0.41],[0.04,-0.41],[0.03,-0.42],[0,-0.42],[-0.02,-0.42]],"o":[[0.04,0.42],[0.06,0.41],[0.17,0.81],[0.239,0.8],[0.31,0.77],[0.39,0.74],[0.23,0.34],[0.25,0.33],[0.261,0.32],[0.28,0.31],[0.29,0.3],[0.31,0.28],[0.195,0.165],[0,0],[0,8.643],[8.644,0],[0,0],[0.196,-0.153],[0.329,-0.26],[0.31,-0.28],[0.29,-0.29],[0.279,-0.31],[0.26,-0.32],[0.25,-0.34],[0.46,-0.7],[0.39,-0.73],[0.319,-0.77],[0.24,-0.8],[0.08,-0.41],[0.06,-0.41],[0.04,-0.42],[0.02,-0.42],[0,-0.42],[-0.021,-0.42],[-0.05,-0.41],[-0.06,-0.41],[-0.16,-0.81],[-0.25,-0.8],[-0.32,-0.77],[-0.39,-0.73],[-0.23,-0.35],[-0.25,-0.34],[-0.27,-0.32],[-0.28,-0.31],[-0.301,-0.29],[-0.311,-0.28],[-0.32,-0.26],[-0.33,-0.25],[-0.689,-0.46],[-0.739,-0.39],[-0.77,-0.32],[-0.8,-0.24],[-0.4,-0.08],[-0.42,-0.06],[-0.42,-0.04],[-0.84,-0.04],[-0.421,0.02],[-0.42,0.04],[-0.41,0.06],[-0.811,0.16],[-0.8,0.25],[-0.771,0.32],[-0.74,0.39],[-0.34,0.23],[-0.33,0.25],[-0.33,0.26],[-0.311,0.28],[-0.3,0.3],[-0.28,0.31],[-0.27,0.33],[-0.24,0.33],[-0.46,0.69],[-0.4,0.74],[-0.32,0.77],[-0.24,0.8],[-0.08,0.41],[-0.07,0.42],[-0.04,0.42],[-0.02,0.42],[0,0.42],[0.03,0.42]],"v":[[-25.495,-16.37],[-25.335,-15.12],[-25.125,-13.88],[-24.505,-11.45],[-23.655,-9.09],[-22.585,-6.82],[-21.306,-4.66],[-20.585,-3.63],[-19.806,-2.64],[-18.985,-1.68],[-18.115,-0.77],[-17.205,0.1],[-16.245,0.92],[-15.641,1.396],[-15.641,28.86],[0.01,44.51],[15.66,28.86],[15.66,1.381],[16.245,0.92],[17.205,0.1],[18.125,-0.77],[18.995,-1.68],[19.814,-2.64],[20.585,-3.63],[21.305,-4.66],[22.595,-6.82],[23.665,-9.09],[24.515,-11.45],[25.125,-13.88],[25.345,-15.12],[25.505,-16.37],[25.595,-17.63],[25.625,-18.89],[25.595,-20.15],[25.505,-21.41],[25.345,-22.66],[25.125,-23.9],[24.515,-26.33],[23.665,-28.69],[22.595,-30.96],[21.305,-33.11],[20.585,-34.14],[19.814,-35.14],[18.995,-36.09],[18.125,-37.01],[17.205,-37.88],[16.245,-38.7],[15.255,-39.47],[14.225,-40.19],[12.074,-41.48],[9.805,-42.55],[7.444,-43.4],[5.005,-44.01],[3.774,-44.23],[2.524,-44.39],[1.265,-44.48],[-1.255,-44.48],[-2.516,-44.39],[-3.766,-44.23],[-5.005,-44.01],[-7.436,-43.4],[-9.795,-42.55],[-12.065,-41.48],[-14.226,-40.19],[-15.255,-39.47],[-16.245,-38.7],[-17.205,-37.88],[-18.115,-37.01],[-18.985,-36.09],[-19.806,-35.14],[-20.585,-34.14],[-21.306,-33.11],[-22.585,-30.96],[-23.655,-28.69],[-24.505,-26.33],[-25.125,-23.9],[-25.335,-22.66],[-25.495,-21.41],[-25.596,-20.15],[-25.625,-18.89],[-25.596,-17.63]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,81],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.267],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":15.096,"s":[-18]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":23,"s":[14]},{"t":31,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.267,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[249.997,333.333,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":11,"s":[192.997,333.333,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":20,"s":[273.997,333.333,0],"to":[0,0,0],"ti":[0,0,0]},{"t":29,"s":[249.997,333.333,0]}],"ix":2,"l":2},"a":{"a":0,"k":[249.997,333.333,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[0,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":51.25,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,312.498],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-26.478],[0,26.478]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[250.005,333.768],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-23.012,0],[0,0],[0,23.012],[0,0]],"o":[[0,0],[0,23.012],[0,0],[23.013,0],[0,0],[0,0]],"v":[[-151.041,-109.377],[-151.041,67.71],[-109.374,109.377],[109.374,109.377],[151.041,67.71],[151.041,-109.377]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[249.997,333.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":30,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[140.623,223.957,0],"ix":2,"l":2},"a":{"a":0,"k":[-109.375,83.332,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[57.53,0],[0,0],[0,-57.53],[0,0]],"o":[[0,0],[0,-57.53],[0,0],[-57.53,0],[0,0],[0,0]],"v":[[109.375,83.332],[109.375,20.835],[5.208,-83.332],[-5.208,-83.332],[-109.375,20.835],[-109.375,83.332]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":6,"s":[{"i":[[0,0],[0,0],[57.53,0],[0,0],[0,-57.53],[0,0]],"o":[[0,0],[0,-57.53],[0,0],[-57.53,0],[0,0],[0,0]],"v":[[109.375,83.332],[109.375,40.835],[5.208,-63.332],[-5.208,-63.332],[-109.375,40.835],[-109.375,83.332]],"c":false}]},{"t":13.095703125,"s":[{"i":[[0,0],[0,0],[57.53,0],[0,0],[0,-57.53],[0,0]],"o":[[0,0],[0,-57.53],[0,0],[-57.53,0],[0,0],[0,0]],"v":[[109.375,83.332],[109.375,20.835],[5.208,-83.332],[-5.208,-83.332],[-109.375,20.835],[-109.375,83.332]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":30,"st":0,"bm":0}]},{"id":"comp_2","nm":"morph-lock-unlock","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[14.346,0],[0,0],[0,14.346],[0,0],[0,0]],"o":[[0,14.346],[0,0],[-14.346,0],[0,0],[0,0],[0,0]],"v":[[135.388,151.044],[109.372,177.06],[-109.377,177.06],[-135.394,151.044],[-135.394,-10.393],[135.388,-10.393]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-48.809,0],[0,0],[0,-48.809],[0,0],[0,0]],"o":[[0,-48.809],[0,0],[48.809,0],[0,0],[0,0],[0,0]],"v":[[-93.727,-88.54],[-5.21,-177.057],[5.206,-177.057],[93.723,-88.54],[93.723,-41.693],[-93.727,-41.693]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,0],[66.067,0],[0,0],[0,-66.067],[0,0],[0,0],[0,-8.643],[0,0],[-31.604,0],[0,0],[0,31.604],[0,0]],"o":[[0,0],[0,0],[0,-66.067],[0,0],[-66.067,0],[0,0],[0,0],[-8.644,0],[0,0],[0,31.604],[0,0],[31.604,0],[0,0],[0,-8.643]],"v":[[151.038,-41.693],[125.023,-41.693],[125.023,-88.54],[5.206,-208.357],[-5.21,-208.357],[-125.027,-88.54],[-125.027,-41.693],[-151.044,-41.693],[-166.694,-26.043],[-166.694,151.044],[-109.377,208.36],[109.372,208.36],[166.688,151.044],[166.688,-26.043]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.04,-0.42],[-0.07,-0.41],[-0.08,-0.41],[-0.24,-0.8],[-0.32,-0.77],[-0.4,-0.73],[-0.46,-0.7],[-0.24,-0.34],[-0.27,-0.32],[-0.28,-0.31],[-0.3,-0.29],[-0.311,-0.28],[-0.33,-0.26],[-0.204,-0.157],[0,0],[-8.644,0],[0,8.643],[0,0],[-0.189,0.159],[-0.311,0.28],[-0.301,0.3],[-0.28,0.31],[-0.27,0.32],[-0.25,0.33],[-0.23,0.34],[-0.39,0.74],[-0.32,0.77],[-0.25,0.8],[-0.16,0.81],[-0.06,0.41],[-0.05,0.42],[-0.021,0.42],[0,0.42],[0.02,0.42],[0.04,0.42],[0.06,0.42],[0.08,0.41],[0.24,0.8],[0.319,0.77],[0.39,0.74],[0.46,0.69],[0.25,0.33],[0.26,0.33],[0.279,0.31],[0.29,0.3],[0.31,0.28],[0.329,0.26],[0.34,0.25],[0.35,0.23],[0.73,0.39],[0.77,0.32],[0.79,0.25],[0.819,0.16],[0.41,0.06],[0.41,0.04],[0.42,0.02],[0.829,-0.04],[0.42,-0.04],[0.41,-0.06],[0.409,-0.08],[0.8,-0.24],[0.77,-0.32],[0.73,-0.39],[0.7,-0.46],[0.34,-0.25],[0.319,-0.26],[0.31,-0.28],[0.29,-0.29],[0.28,-0.31],[0.261,-0.32],[0.25,-0.34],[0.23,-0.35],[0.39,-0.73],[0.31,-0.77],[0.239,-0.8],[0.17,-0.81],[0.06,-0.41],[0.04,-0.41],[0.03,-0.42],[0,-0.42],[-0.02,-0.42]],"o":[[0.04,0.42],[0.06,0.41],[0.17,0.81],[0.239,0.8],[0.31,0.77],[0.39,0.74],[0.23,0.34],[0.25,0.33],[0.261,0.32],[0.28,0.31],[0.29,0.3],[0.31,0.28],[0.195,0.165],[0,0],[0,8.643],[8.644,0],[0,0],[0.196,-0.153],[0.329,-0.26],[0.31,-0.28],[0.29,-0.29],[0.279,-0.31],[0.26,-0.32],[0.25,-0.34],[0.46,-0.7],[0.39,-0.73],[0.319,-0.77],[0.24,-0.8],[0.08,-0.41],[0.06,-0.41],[0.04,-0.42],[0.02,-0.42],[0,-0.42],[-0.021,-0.42],[-0.05,-0.41],[-0.06,-0.41],[-0.16,-0.81],[-0.25,-0.8],[-0.32,-0.77],[-0.39,-0.73],[-0.23,-0.35],[-0.25,-0.34],[-0.27,-0.32],[-0.28,-0.31],[-0.301,-0.29],[-0.311,-0.28],[-0.32,-0.26],[-0.33,-0.25],[-0.689,-0.46],[-0.739,-0.39],[-0.77,-0.32],[-0.8,-0.24],[-0.4,-0.08],[-0.42,-0.06],[-0.42,-0.04],[-0.84,-0.04],[-0.421,0.02],[-0.42,0.04],[-0.41,0.06],[-0.811,0.16],[-0.8,0.25],[-0.771,0.32],[-0.74,0.39],[-0.34,0.23],[-0.33,0.25],[-0.33,0.26],[-0.311,0.28],[-0.3,0.3],[-0.28,0.31],[-0.27,0.33],[-0.24,0.33],[-0.46,0.69],[-0.4,0.74],[-0.32,0.77],[-0.24,0.8],[-0.08,0.41],[-0.07,0.42],[-0.04,0.42],[-0.02,0.42],[0,0.42],[0.03,0.42]],"v":[[-25.495,-16.37],[-25.335,-15.12],[-25.125,-13.88],[-24.505,-11.45],[-23.655,-9.09],[-22.585,-6.82],[-21.306,-4.66],[-20.585,-3.63],[-19.806,-2.64],[-18.985,-1.68],[-18.115,-0.77],[-17.205,0.1],[-16.245,0.92],[-15.641,1.396],[-15.641,28.86],[0.01,44.51],[15.66,28.86],[15.66,1.381],[16.245,0.92],[17.205,0.1],[18.125,-0.77],[18.995,-1.68],[19.814,-2.64],[20.585,-3.63],[21.305,-4.66],[22.595,-6.82],[23.665,-9.09],[24.515,-11.45],[25.125,-13.88],[25.345,-15.12],[25.505,-16.37],[25.595,-17.63],[25.625,-18.89],[25.595,-20.15],[25.505,-21.41],[25.345,-22.66],[25.125,-23.9],[24.515,-26.33],[23.665,-28.69],[22.595,-30.96],[21.305,-33.11],[20.585,-34.14],[19.814,-35.14],[18.995,-36.09],[18.125,-37.01],[17.205,-37.88],[16.245,-38.7],[15.255,-39.47],[14.225,-40.19],[12.074,-41.48],[9.805,-42.55],[7.444,-43.4],[5.005,-44.01],[3.774,-44.23],[2.524,-44.39],[1.265,-44.48],[-1.255,-44.48],[-2.516,-44.39],[-3.766,-44.23],[-5.005,-44.01],[-7.436,-43.4],[-9.795,-42.55],[-12.065,-41.48],[-14.226,-40.19],[-15.255,-39.47],[-16.245,-38.7],[-17.205,-37.88],[-18.115,-37.01],[-18.985,-36.09],[-19.806,-35.14],[-20.585,-34.14],[-21.306,-33.11],[-22.585,-30.96],[-23.655,-28.69],[-24.505,-26.33],[-25.125,-23.9],[-25.335,-22.66],[-25.495,-21.41],[-25.596,-20.15],[-25.625,-18.89],[-25.596,-17.63]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,81],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.997,250.002,0],"ix":2,"l":2},"a":{"a":0,"k":[249.997,250.002,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.02,-0.42],[-0.04,-0.42],[-0.07,-0.41],[-0.08,-0.41],[-0.24,-0.8],[-0.32,-0.77],[-0.4,-0.73],[-0.46,-0.7],[-0.24,-0.34],[-0.27,-0.32],[-0.28,-0.31],[-0.3,-0.29],[-0.311,-0.28],[-0.33,-0.26],[-0.204,-0.157],[0,0],[-8.644,0],[0,8.643],[0,0],[-0.189,0.159],[-0.311,0.28],[-0.301,0.3],[-0.28,0.31],[-0.27,0.32],[-0.25,0.33],[-0.23,0.34],[-0.39,0.74],[-0.32,0.77],[-0.25,0.8],[-0.16,0.81],[-0.06,0.41],[-0.05,0.42],[-0.021,0.42],[0,0.42],[0.02,0.42],[0.04,0.42],[0.06,0.42],[0.08,0.41],[0.24,0.8],[0.319,0.77],[0.39,0.74],[0.46,0.69],[0.25,0.33],[0.26,0.33],[0.279,0.31],[0.29,0.3],[0.31,0.28],[0.329,0.26],[0.34,0.25],[0.35,0.23],[0.73,0.39],[0.77,0.32],[0.79,0.25],[0.819,0.16],[0.41,0.06],[0.41,0.04],[0.42,0.02],[0.829,-0.04],[0.42,-0.04],[0.41,-0.06],[0.409,-0.08],[0.8,-0.24],[0.77,-0.32],[0.73,-0.39],[0.7,-0.46],[0.34,-0.25],[0.319,-0.26],[0.31,-0.28],[0.29,-0.29],[0.28,-0.31],[0.261,-0.32],[0.25,-0.34],[0.23,-0.35],[0.39,-0.73],[0.31,-0.77],[0.239,-0.8],[0.17,-0.81],[0.06,-0.41],[0.04,-0.41],[0.03,-0.42],[0,-0.42]],"o":[[0.03,0.42],[0.04,0.42],[0.06,0.41],[0.17,0.81],[0.239,0.8],[0.31,0.77],[0.39,0.74],[0.23,0.34],[0.25,0.33],[0.261,0.32],[0.28,0.31],[0.29,0.3],[0.31,0.28],[0.195,0.165],[0,0],[0,8.643],[8.644,0],[0,0],[0.196,-0.153],[0.329,-0.26],[0.31,-0.28],[0.29,-0.29],[0.279,-0.31],[0.26,-0.32],[0.25,-0.34],[0.46,-0.7],[0.39,-0.73],[0.319,-0.77],[0.24,-0.8],[0.08,-0.41],[0.06,-0.41],[0.04,-0.42],[0.02,-0.42],[0,-0.42],[-0.021,-0.42],[-0.05,-0.41],[-0.06,-0.41],[-0.16,-0.81],[-0.25,-0.8],[-0.32,-0.77],[-0.39,-0.73],[-0.23,-0.35],[-0.25,-0.34],[-0.27,-0.32],[-0.28,-0.31],[-0.301,-0.29],[-0.311,-0.28],[-0.32,-0.26],[-0.33,-0.25],[-0.689,-0.46],[-0.739,-0.39],[-0.77,-0.32],[-0.8,-0.24],[-0.4,-0.08],[-0.42,-0.06],[-0.42,-0.04],[-0.84,-0.04],[-0.421,0.02],[-0.42,0.04],[-0.41,0.06],[-0.811,0.16],[-0.8,0.25],[-0.771,0.32],[-0.74,0.39],[-0.34,0.23],[-0.33,0.25],[-0.33,0.26],[-0.311,0.28],[-0.3,0.3],[-0.28,0.31],[-0.27,0.33],[-0.24,0.33],[-0.46,0.69],[-0.4,0.74],[-0.32,0.77],[-0.24,0.8],[-0.08,0.41],[-0.07,0.42],[-0.04,0.42],[-0.02,0.42],[0,0.42]],"v":[[-25.596,-17.63],[-25.495,-16.37],[-25.335,-15.12],[-25.125,-13.88],[-24.505,-11.45],[-23.655,-9.09],[-22.585,-6.82],[-21.306,-4.66],[-20.585,-3.63],[-19.806,-2.64],[-18.985,-1.68],[-18.115,-0.77],[-17.205,0.1],[-16.245,0.92],[-15.641,1.396],[-15.641,28.86],[0.01,44.51],[15.66,28.86],[15.66,1.381],[16.245,0.92],[17.205,0.1],[18.125,-0.77],[18.995,-1.68],[19.814,-2.64],[20.585,-3.63],[21.305,-4.66],[22.595,-6.82],[23.665,-9.09],[24.515,-11.45],[25.125,-13.88],[25.345,-15.12],[25.505,-16.37],[25.595,-17.63],[25.625,-18.89],[25.595,-20.15],[25.505,-21.41],[25.345,-22.66],[25.125,-23.9],[24.515,-26.33],[23.665,-28.69],[22.595,-30.96],[21.305,-33.11],[20.585,-34.14],[19.814,-35.14],[18.995,-36.09],[18.125,-37.01],[17.205,-37.88],[16.245,-38.7],[15.255,-39.47],[14.225,-40.19],[12.074,-41.48],[9.805,-42.55],[7.444,-43.4],[5.005,-44.01],[3.774,-44.23],[2.524,-44.39],[1.265,-44.48],[-1.255,-44.48],[-2.516,-44.39],[-3.766,-44.23],[-5.005,-44.01],[-7.436,-43.4],[-9.795,-42.55],[-12.065,-41.48],[-14.226,-40.19],[-15.255,-39.47],[-16.245,-38.7],[-17.205,-37.88],[-18.115,-37.01],[-18.985,-36.09],[-19.806,-35.14],[-20.585,-34.14],[-21.306,-33.11],[-22.585,-30.96],[-23.655,-28.69],[-24.505,-26.33],[-25.125,-23.9],[-25.335,-22.66],[-25.495,-21.41],[-25.596,-20.15],[-25.625,-18.89]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[249.995,331.386],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[14.346,0],[0,0],[0,14.346],[0,0],[0,0],[-0.029,0],[-0.029,0],[0,0]],"o":[[0,14.346],[0,0],[-14.346,0],[0,0],[0,0],[0.029,0],[0.029,0],[0,0],[0,0]],"v":[[135.391,151.042],[109.375,177.058],[-109.374,177.058],[-135.391,151.042],[-135.391,-10.395],[-109.456,-10.395],[-109.369,-10.391],[-109.282,-10.395],[135.391,-10.395]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,0],[-48.809,0],[0,0],[-4.304,-45.367],[-8.61,0.821],[0.816,8.604],[21.928,20.01],[29.913,0],[0,0],[0,-66.067],[0,0],[0,0],[0,-8.643],[0,0],[-31.604,0],[0,0],[0,31.604],[0,0]],"o":[[0,0],[0,0],[0,-48.809],[0,0],[45.725,0],[0.816,8.604],[8.605,-0.816],[-2.795,-29.465],[-22.048,-20.119],[0,0],[-66.067,0],[0,0],[0,0],[-8.644,0],[0,0],[0,31.604],[0,0],[31.604,0],[0,0],[0,-8.643]],"v":[[151.041,-41.695],[-93.719,-41.695],[-93.719,-88.541],[-5.202,-177.058],[5.215,-177.058],[92.969,-97.48],[110.026,-83.378],[124.129,-100.436],[85.791,-177.159],[5.215,-208.358],[-5.202,-208.358],[-125.02,-88.541],[-125.02,-41.695],[-151.041,-41.695],[-166.691,-26.045],[-166.691,151.042],[-109.374,208.358],[109.375,208.358],[166.691,151.042],[166.691,-26.045]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[249.997,250.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":30,"op":302,"st":-30,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.51],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.51],"y":[0]},"t":7,"s":[0]},{"i":{"x":[0.44],"y":[1]},"o":{"x":[0.51],"y":[0]},"t":17,"s":[0]},{"t":30,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.51,"y":0},"t":0,"s":[249.997,333.333,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.51,"y":0},"t":7,"s":[249.997,373.333,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.44,"y":1},"o":{"x":0.51,"y":0},"t":17,"s":[249.997,309.333,0],"to":[0,0,0],"ti":[0,0,0]},{"t":30,"s":[249.997,333.333,0]}],"ix":2,"l":2},"a":{"a":0,"k":[249.997,333.333,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[0,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":51.25,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,312.498],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-26.478],[0,26.478]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[250.005,333.768],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-23.012,0],[0,0],[0,23.012],[0,0]],"o":[[0,0],[0,23.012],[0,0],[23.013,0],[0,0],[0,0]],"v":[[-151.041,-109.377],[-151.041,67.71],[-109.374,109.377],[109.374,109.377],[151.041,67.71],[151.041,-109.377]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[249.997,333.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":30,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[140.623,223.957,0],"ix":2,"l":2},"a":{"a":0,"k":[-109.375,83.332,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.51,"y":0},"t":7,"s":[{"i":[[0,0],[0,0],[57.53,0],[0,0],[0,-57.53],[0,0]],"o":[[0,0],[0,-57.53],[0,0],[-57.53,0],[0,0],[0,0]],"v":[[109.236,83.332],[109.375,20.835],[5.208,-83.332],[-5.208,-83.332],[-109.375,20.835],[-109.375,83.332]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":15.031,"s":[{"i":[[0,0],[0,0],[57.53,0],[0,0],[0,-57.53],[0,0]],"o":[[0,0],[0,-57.53],[0,0],[-57.53,0],[0,0],[0,0]],"v":[[109.248,4.832],[109.375,5.835],[5.208,-98.332],[-5.208,-98.332],[-109.375,5.835],[-109.375,83.332]],"c":false}]},{"t":26,"s":[{"i":[[0,0],[0,0],[57.53,0],[0,0],[0,-57.53],[0,0]],"o":[[0,0],[0,-57.53],[0,0],[-57.53,0],[0,0],[0,0]],"v":[[109.248,20.332],[109.375,20.835],[5.208,-83.332],[-5.208,-83.332],[-109.375,20.835],[-109.375,83.332]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.51],"y":[0]},"t":7,"s":[0]},{"t":15.03125,"s":[2.5]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 2","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":30,"st":0,"bm":0}]},{"id":"comp_3","nm":"in-unlock","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.997,250.002,0],"ix":2,"l":2},"a":{"a":0,"k":[249.997,250.002,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.02,-0.42],[-0.04,-0.42],[-0.07,-0.41],[-0.08,-0.41],[-0.24,-0.8],[-0.32,-0.77],[-0.4,-0.73],[-0.46,-0.7],[-0.24,-0.34],[-0.27,-0.32],[-0.28,-0.31],[-0.3,-0.29],[-0.311,-0.28],[-0.33,-0.26],[-0.204,-0.157],[0,0],[-8.644,0],[0,8.643],[0,0],[-0.189,0.159],[-0.311,0.28],[-0.301,0.3],[-0.28,0.31],[-0.27,0.32],[-0.25,0.33],[-0.23,0.34],[-0.39,0.74],[-0.32,0.77],[-0.25,0.8],[-0.16,0.81],[-0.06,0.41],[-0.05,0.42],[-0.021,0.42],[0,0.42],[0.02,0.42],[0.04,0.42],[0.06,0.42],[0.08,0.41],[0.24,0.8],[0.319,0.77],[0.39,0.74],[0.46,0.69],[0.25,0.33],[0.26,0.33],[0.279,0.31],[0.29,0.3],[0.31,0.28],[0.329,0.26],[0.34,0.25],[0.35,0.23],[0.73,0.39],[0.77,0.32],[0.79,0.25],[0.819,0.16],[0.41,0.06],[0.41,0.04],[0.42,0.02],[0.829,-0.04],[0.42,-0.04],[0.41,-0.06],[0.409,-0.08],[0.8,-0.24],[0.77,-0.32],[0.73,-0.39],[0.7,-0.46],[0.34,-0.25],[0.319,-0.26],[0.31,-0.28],[0.29,-0.29],[0.28,-0.31],[0.261,-0.32],[0.25,-0.34],[0.23,-0.35],[0.39,-0.73],[0.31,-0.77],[0.239,-0.8],[0.17,-0.81],[0.06,-0.41],[0.04,-0.41],[0.03,-0.42],[0,-0.42]],"o":[[0.03,0.42],[0.04,0.42],[0.06,0.41],[0.17,0.81],[0.239,0.8],[0.31,0.77],[0.39,0.74],[0.23,0.34],[0.25,0.33],[0.261,0.32],[0.28,0.31],[0.29,0.3],[0.31,0.28],[0.195,0.165],[0,0],[0,8.643],[8.644,0],[0,0],[0.196,-0.153],[0.329,-0.26],[0.31,-0.28],[0.29,-0.29],[0.279,-0.31],[0.26,-0.32],[0.25,-0.34],[0.46,-0.7],[0.39,-0.73],[0.319,-0.77],[0.24,-0.8],[0.08,-0.41],[0.06,-0.41],[0.04,-0.42],[0.02,-0.42],[0,-0.42],[-0.021,-0.42],[-0.05,-0.41],[-0.06,-0.41],[-0.16,-0.81],[-0.25,-0.8],[-0.32,-0.77],[-0.39,-0.73],[-0.23,-0.35],[-0.25,-0.34],[-0.27,-0.32],[-0.28,-0.31],[-0.301,-0.29],[-0.311,-0.28],[-0.32,-0.26],[-0.33,-0.25],[-0.689,-0.46],[-0.739,-0.39],[-0.77,-0.32],[-0.8,-0.24],[-0.4,-0.08],[-0.42,-0.06],[-0.42,-0.04],[-0.84,-0.04],[-0.421,0.02],[-0.42,0.04],[-0.41,0.06],[-0.811,0.16],[-0.8,0.25],[-0.771,0.32],[-0.74,0.39],[-0.34,0.23],[-0.33,0.25],[-0.33,0.26],[-0.311,0.28],[-0.3,0.3],[-0.28,0.31],[-0.27,0.33],[-0.24,0.33],[-0.46,0.69],[-0.4,0.74],[-0.32,0.77],[-0.24,0.8],[-0.08,0.41],[-0.07,0.42],[-0.04,0.42],[-0.02,0.42],[0,0.42]],"v":[[-25.596,-17.63],[-25.495,-16.37],[-25.335,-15.12],[-25.125,-13.88],[-24.505,-11.45],[-23.655,-9.09],[-22.585,-6.82],[-21.306,-4.66],[-20.585,-3.63],[-19.806,-2.64],[-18.985,-1.68],[-18.115,-0.77],[-17.205,0.1],[-16.245,0.92],[-15.641,1.396],[-15.641,28.86],[0.01,44.51],[15.66,28.86],[15.66,1.381],[16.245,0.92],[17.205,0.1],[18.125,-0.77],[18.995,-1.68],[19.814,-2.64],[20.585,-3.63],[21.305,-4.66],[22.595,-6.82],[23.665,-9.09],[24.515,-11.45],[25.125,-13.88],[25.345,-15.12],[25.505,-16.37],[25.595,-17.63],[25.625,-18.89],[25.595,-20.15],[25.505,-21.41],[25.345,-22.66],[25.125,-23.9],[24.515,-26.33],[23.665,-28.69],[22.595,-30.96],[21.305,-33.11],[20.585,-34.14],[19.814,-35.14],[18.995,-36.09],[18.125,-37.01],[17.205,-37.88],[16.245,-38.7],[15.255,-39.47],[14.225,-40.19],[12.074,-41.48],[9.805,-42.55],[7.444,-43.4],[5.005,-44.01],[3.774,-44.23],[2.524,-44.39],[1.265,-44.48],[-1.255,-44.48],[-2.516,-44.39],[-3.766,-44.23],[-5.005,-44.01],[-7.436,-43.4],[-9.795,-42.55],[-12.065,-41.48],[-14.226,-40.19],[-15.255,-39.47],[-16.245,-38.7],[-17.205,-37.88],[-18.115,-37.01],[-18.985,-36.09],[-19.806,-35.14],[-20.585,-34.14],[-21.306,-33.11],[-22.585,-30.96],[-23.655,-28.69],[-24.505,-26.33],[-25.125,-23.9],[-25.335,-22.66],[-25.495,-21.41],[-25.596,-20.15],[-25.625,-18.89]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[249.995,331.386],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[14.346,0],[0,0],[0,14.346],[0,0],[0,0],[-0.029,0],[-0.029,0],[0,0]],"o":[[0,14.346],[0,0],[-14.346,0],[0,0],[0,0],[0.029,0],[0.029,0],[0,0],[0,0]],"v":[[135.391,151.042],[109.375,177.058],[-109.374,177.058],[-135.391,151.042],[-135.391,-10.395],[-109.456,-10.395],[-109.369,-10.391],[-109.282,-10.395],[135.391,-10.395]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,0],[-48.809,0],[0,0],[-4.304,-45.367],[-8.61,0.821],[0.816,8.604],[21.928,20.01],[29.913,0],[0,0],[0,-66.067],[0,0],[0,0],[0,-8.643],[0,0],[-31.604,0],[0,0],[0,31.604],[0,0]],"o":[[0,0],[0,0],[0,-48.809],[0,0],[45.725,0],[0.816,8.604],[8.605,-0.816],[-2.795,-29.465],[-22.048,-20.119],[0,0],[-66.067,0],[0,0],[0,0],[-8.644,0],[0,0],[0,31.604],[0,0],[31.604,0],[0,0],[0,-8.643]],"v":[[151.041,-41.695],[-93.719,-41.695],[-93.719,-88.541],[-5.202,-177.058],[5.215,-177.058],[92.969,-97.48],[110.026,-83.378],[124.129,-100.436],[85.791,-177.159],[5.215,-208.358],[-5.202,-208.358],[-125.02,-88.541],[-125.02,-41.695],[-151.041,-41.695],[-166.691,-26.045],[-166.691,151.042],[-109.374,208.358],[109.375,208.358],[166.691,151.042],[166.691,-26.045]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[249.997,250.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":30,"op":302,"st":-30,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.318],"y":[1.007]},"o":{"x":[0.167],"y":[0.167]},"t":1,"s":[0]},{"i":{"x":[0.29],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":16,"s":[-24]},{"t":30,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.318,"y":0.999},"o":{"x":0.167,"y":0.167},"t":1,"s":[311.997,439.333,0],"to":[-6.083,-89.417,0],"ti":[18.083,13.417,0]},{"i":{"x":0.186,"y":0.995},"o":{"x":0.333,"y":0},"t":13,"s":[223.997,307.333,0],"to":[8.167,6.833,0],"ti":[-2.75,-4.5,0]},{"t":27,"s":[249.997,333.333,0]}],"ix":2,"l":2},"a":{"a":0,"k":[249.997,333.333,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":1,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[2.183,22.836],[2.183,22.836]],"c":false}]},{"t":8,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[0,0]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":51.25,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,312.498],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":1,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[2.183,-3.642],[3.27,-3.448]],"c":false}]},{"t":8,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-26.478],[0,26.478]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[250.005,333.768],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":1,"s":[{"i":[[0,0],[0,0],[-23.012,0],[0,0],[0,23.012],[0,0]],"o":[[0,0],[0,23.012],[0,0],[23.013,0],[0,0],[0,0]],"v":[[-149.003,-4.562],[-148.755,0.919],[-107.088,42.586],[111.66,42.586],[153.327,0.919],[153.079,-4.562]],"c":true}]},{"t":8,"s":[{"i":[[0,0],[0,0],[-23.012,0],[0,0],[0,23.012],[0,0]],"o":[[0,0],[0,23.012],[0,0],[23.013,0],[0,0],[0,0]],"v":[[-151.041,-109.377],[-151.041,67.71],[-109.374,109.377],[109.374,109.377],[151.041,67.71],[151.041,-109.377]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[249.997,333.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":30,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":13,"s":[0]},{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":19,"s":[-11]},{"t":30,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":1,"s":[140.623,321.957,0],"to":[0,-16.333,0],"ti":[0,16.333,0]},{"t":8,"s":[140.623,223.957,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-109.375,83.332,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.985,"y":1},"o":{"x":0.167,"y":0.167},"t":1,"s":[{"i":[[0,0],[0,0],[2.415,0],[0,0],[0,-2.6],[0,0]],"o":[[0,0],[0,-2.415],[0,0],[-2.6,0],[0,0],[0,0]],"v":[[109.375,83.332],[109.375,83.791],[105.002,79.418],[-104.668,79.418],[-109.375,84.125],[-109.375,83.332]],"c":false}]},{"t":16,"s":[{"i":[[0,0],[0,0],[57.53,0],[0,0],[0,-57.53],[0,0]],"o":[[0,0],[0,-57.53],[0,0],[-57.53,0],[0,0],[0,0]],"v":[[109.248,20.332],[109.375,20.835],[5.208,-83.332],[-5.208,-83.332],[-109.375,20.835],[-109.375,83.332]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.985],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":1,"s":[0]},{"t":16,"s":[4.5]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":30,"st":0,"bm":0}]},{"id":"comp_4","nm":"hover-unlock","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.997,250.002,0],"ix":2,"l":2},"a":{"a":0,"k":[249.997,250.002,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.02,-0.42],[-0.04,-0.42],[-0.07,-0.41],[-0.08,-0.41],[-0.24,-0.8],[-0.32,-0.77],[-0.4,-0.73],[-0.46,-0.7],[-0.24,-0.34],[-0.27,-0.32],[-0.28,-0.31],[-0.3,-0.29],[-0.311,-0.28],[-0.33,-0.26],[-0.204,-0.157],[0,0],[-8.644,0],[0,8.643],[0,0],[-0.189,0.159],[-0.311,0.28],[-0.301,0.3],[-0.28,0.31],[-0.27,0.32],[-0.25,0.33],[-0.23,0.34],[-0.39,0.74],[-0.32,0.77],[-0.25,0.8],[-0.16,0.81],[-0.06,0.41],[-0.05,0.42],[-0.021,0.42],[0,0.42],[0.02,0.42],[0.04,0.42],[0.06,0.42],[0.08,0.41],[0.24,0.8],[0.319,0.77],[0.39,0.74],[0.46,0.69],[0.25,0.33],[0.26,0.33],[0.279,0.31],[0.29,0.3],[0.31,0.28],[0.329,0.26],[0.34,0.25],[0.35,0.23],[0.73,0.39],[0.77,0.32],[0.79,0.25],[0.819,0.16],[0.41,0.06],[0.41,0.04],[0.42,0.02],[0.829,-0.04],[0.42,-0.04],[0.41,-0.06],[0.409,-0.08],[0.8,-0.24],[0.77,-0.32],[0.73,-0.39],[0.7,-0.46],[0.34,-0.25],[0.319,-0.26],[0.31,-0.28],[0.29,-0.29],[0.28,-0.31],[0.261,-0.32],[0.25,-0.34],[0.23,-0.35],[0.39,-0.73],[0.31,-0.77],[0.239,-0.8],[0.17,-0.81],[0.06,-0.41],[0.04,-0.41],[0.03,-0.42],[0,-0.42]],"o":[[0.03,0.42],[0.04,0.42],[0.06,0.41],[0.17,0.81],[0.239,0.8],[0.31,0.77],[0.39,0.74],[0.23,0.34],[0.25,0.33],[0.261,0.32],[0.28,0.31],[0.29,0.3],[0.31,0.28],[0.195,0.165],[0,0],[0,8.643],[8.644,0],[0,0],[0.196,-0.153],[0.329,-0.26],[0.31,-0.28],[0.29,-0.29],[0.279,-0.31],[0.26,-0.32],[0.25,-0.34],[0.46,-0.7],[0.39,-0.73],[0.319,-0.77],[0.24,-0.8],[0.08,-0.41],[0.06,-0.41],[0.04,-0.42],[0.02,-0.42],[0,-0.42],[-0.021,-0.42],[-0.05,-0.41],[-0.06,-0.41],[-0.16,-0.81],[-0.25,-0.8],[-0.32,-0.77],[-0.39,-0.73],[-0.23,-0.35],[-0.25,-0.34],[-0.27,-0.32],[-0.28,-0.31],[-0.301,-0.29],[-0.311,-0.28],[-0.32,-0.26],[-0.33,-0.25],[-0.689,-0.46],[-0.739,-0.39],[-0.77,-0.32],[-0.8,-0.24],[-0.4,-0.08],[-0.42,-0.06],[-0.42,-0.04],[-0.84,-0.04],[-0.421,0.02],[-0.42,0.04],[-0.41,0.06],[-0.811,0.16],[-0.8,0.25],[-0.771,0.32],[-0.74,0.39],[-0.34,0.23],[-0.33,0.25],[-0.33,0.26],[-0.311,0.28],[-0.3,0.3],[-0.28,0.31],[-0.27,0.33],[-0.24,0.33],[-0.46,0.69],[-0.4,0.74],[-0.32,0.77],[-0.24,0.8],[-0.08,0.41],[-0.07,0.42],[-0.04,0.42],[-0.02,0.42],[0,0.42]],"v":[[-25.596,-17.63],[-25.495,-16.37],[-25.335,-15.12],[-25.125,-13.88],[-24.505,-11.45],[-23.655,-9.09],[-22.585,-6.82],[-21.306,-4.66],[-20.585,-3.63],[-19.806,-2.64],[-18.985,-1.68],[-18.115,-0.77],[-17.205,0.1],[-16.245,0.92],[-15.641,1.396],[-15.641,28.86],[0.01,44.51],[15.66,28.86],[15.66,1.381],[16.245,0.92],[17.205,0.1],[18.125,-0.77],[18.995,-1.68],[19.814,-2.64],[20.585,-3.63],[21.305,-4.66],[22.595,-6.82],[23.665,-9.09],[24.515,-11.45],[25.125,-13.88],[25.345,-15.12],[25.505,-16.37],[25.595,-17.63],[25.625,-18.89],[25.595,-20.15],[25.505,-21.41],[25.345,-22.66],[25.125,-23.9],[24.515,-26.33],[23.665,-28.69],[22.595,-30.96],[21.305,-33.11],[20.585,-34.14],[19.814,-35.14],[18.995,-36.09],[18.125,-37.01],[17.205,-37.88],[16.245,-38.7],[15.255,-39.47],[14.225,-40.19],[12.074,-41.48],[9.805,-42.55],[7.444,-43.4],[5.005,-44.01],[3.774,-44.23],[2.524,-44.39],[1.265,-44.48],[-1.255,-44.48],[-2.516,-44.39],[-3.766,-44.23],[-5.005,-44.01],[-7.436,-43.4],[-9.795,-42.55],[-12.065,-41.48],[-14.226,-40.19],[-15.255,-39.47],[-16.245,-38.7],[-17.205,-37.88],[-18.115,-37.01],[-18.985,-36.09],[-19.806,-35.14],[-20.585,-34.14],[-21.306,-33.11],[-22.585,-30.96],[-23.655,-28.69],[-24.505,-26.33],[-25.125,-23.9],[-25.335,-22.66],[-25.495,-21.41],[-25.596,-20.15],[-25.625,-18.89]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[249.995,331.386],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[14.346,0],[0,0],[0,14.346],[0,0],[0,0],[-0.029,0],[-0.029,0],[0,0]],"o":[[0,14.346],[0,0],[-14.346,0],[0,0],[0,0],[0.029,0],[0.029,0],[0,0],[0,0]],"v":[[135.391,151.042],[109.375,177.058],[-109.374,177.058],[-135.391,151.042],[-135.391,-10.395],[-109.456,-10.395],[-109.369,-10.391],[-109.282,-10.395],[135.391,-10.395]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,0],[-48.809,0],[0,0],[-4.304,-45.367],[-8.61,0.821],[0.816,8.604],[21.928,20.01],[29.913,0],[0,0],[0,-66.067],[0,0],[0,0],[0,-8.643],[0,0],[-31.604,0],[0,0],[0,31.604],[0,0]],"o":[[0,0],[0,0],[0,-48.809],[0,0],[45.725,0],[0.816,8.604],[8.605,-0.816],[-2.795,-29.465],[-22.048,-20.119],[0,0],[-66.067,0],[0,0],[0,0],[-8.644,0],[0,0],[0,31.604],[0,0],[31.604,0],[0,0],[0,-8.643]],"v":[[151.041,-41.695],[-93.719,-41.695],[-93.719,-88.541],[-5.202,-177.058],[5.215,-177.058],[92.969,-97.48],[110.026,-83.378],[124.129,-100.436],[85.791,-177.159],[5.215,-208.358],[-5.202,-208.358],[-125.02,-88.541],[-125.02,-41.695],[-151.041,-41.695],[-166.691,-26.045],[-166.691,151.042],[-109.374,208.358],[109.375,208.358],[166.691,151.042],[166.691,-26.045]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[249.997,250.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.997,250.002,0],"ix":2,"l":2},"a":{"a":0,"k":[249.997,250.002,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.02,-0.42],[-0.04,-0.42],[-0.07,-0.41],[-0.08,-0.41],[-0.24,-0.8],[-0.32,-0.77],[-0.4,-0.73],[-0.46,-0.7],[-0.24,-0.34],[-0.27,-0.32],[-0.28,-0.31],[-0.3,-0.29],[-0.311,-0.28],[-0.33,-0.26],[-0.204,-0.157],[0,0],[-8.644,0],[0,8.643],[0,0],[-0.189,0.159],[-0.311,0.28],[-0.301,0.3],[-0.28,0.31],[-0.27,0.32],[-0.25,0.33],[-0.23,0.34],[-0.39,0.74],[-0.32,0.77],[-0.25,0.8],[-0.16,0.81],[-0.06,0.41],[-0.05,0.42],[-0.021,0.42],[0,0.42],[0.02,0.42],[0.04,0.42],[0.06,0.42],[0.08,0.41],[0.24,0.8],[0.319,0.77],[0.39,0.74],[0.46,0.69],[0.25,0.33],[0.26,0.33],[0.279,0.31],[0.29,0.3],[0.31,0.28],[0.329,0.26],[0.34,0.25],[0.35,0.23],[0.73,0.39],[0.77,0.32],[0.79,0.25],[0.819,0.16],[0.41,0.06],[0.41,0.04],[0.42,0.02],[0.829,-0.04],[0.42,-0.04],[0.41,-0.06],[0.409,-0.08],[0.8,-0.24],[0.77,-0.32],[0.73,-0.39],[0.7,-0.46],[0.34,-0.25],[0.319,-0.26],[0.31,-0.28],[0.29,-0.29],[0.28,-0.31],[0.261,-0.32],[0.25,-0.34],[0.23,-0.35],[0.39,-0.73],[0.31,-0.77],[0.239,-0.8],[0.17,-0.81],[0.06,-0.41],[0.04,-0.41],[0.03,-0.42],[0,-0.42]],"o":[[0.03,0.42],[0.04,0.42],[0.06,0.41],[0.17,0.81],[0.239,0.8],[0.31,0.77],[0.39,0.74],[0.23,0.34],[0.25,0.33],[0.261,0.32],[0.28,0.31],[0.29,0.3],[0.31,0.28],[0.195,0.165],[0,0],[0,8.643],[8.644,0],[0,0],[0.196,-0.153],[0.329,-0.26],[0.31,-0.28],[0.29,-0.29],[0.279,-0.31],[0.26,-0.32],[0.25,-0.34],[0.46,-0.7],[0.39,-0.73],[0.319,-0.77],[0.24,-0.8],[0.08,-0.41],[0.06,-0.41],[0.04,-0.42],[0.02,-0.42],[0,-0.42],[-0.021,-0.42],[-0.05,-0.41],[-0.06,-0.41],[-0.16,-0.81],[-0.25,-0.8],[-0.32,-0.77],[-0.39,-0.73],[-0.23,-0.35],[-0.25,-0.34],[-0.27,-0.32],[-0.28,-0.31],[-0.301,-0.29],[-0.311,-0.28],[-0.32,-0.26],[-0.33,-0.25],[-0.689,-0.46],[-0.739,-0.39],[-0.77,-0.32],[-0.8,-0.24],[-0.4,-0.08],[-0.42,-0.06],[-0.42,-0.04],[-0.84,-0.04],[-0.421,0.02],[-0.42,0.04],[-0.41,0.06],[-0.811,0.16],[-0.8,0.25],[-0.771,0.32],[-0.74,0.39],[-0.34,0.23],[-0.33,0.25],[-0.33,0.26],[-0.311,0.28],[-0.3,0.3],[-0.28,0.31],[-0.27,0.33],[-0.24,0.33],[-0.46,0.69],[-0.4,0.74],[-0.32,0.77],[-0.24,0.8],[-0.08,0.41],[-0.07,0.42],[-0.04,0.42],[-0.02,0.42],[0,0.42]],"v":[[-25.596,-17.63],[-25.495,-16.37],[-25.335,-15.12],[-25.125,-13.88],[-24.505,-11.45],[-23.655,-9.09],[-22.585,-6.82],[-21.306,-4.66],[-20.585,-3.63],[-19.806,-2.64],[-18.985,-1.68],[-18.115,-0.77],[-17.205,0.1],[-16.245,0.92],[-15.641,1.396],[-15.641,28.86],[0.01,44.51],[15.66,28.86],[15.66,1.381],[16.245,0.92],[17.205,0.1],[18.125,-0.77],[18.995,-1.68],[19.814,-2.64],[20.585,-3.63],[21.305,-4.66],[22.595,-6.82],[23.665,-9.09],[24.515,-11.45],[25.125,-13.88],[25.345,-15.12],[25.505,-16.37],[25.595,-17.63],[25.625,-18.89],[25.595,-20.15],[25.505,-21.41],[25.345,-22.66],[25.125,-23.9],[24.515,-26.33],[23.665,-28.69],[22.595,-30.96],[21.305,-33.11],[20.585,-34.14],[19.814,-35.14],[18.995,-36.09],[18.125,-37.01],[17.205,-37.88],[16.245,-38.7],[15.255,-39.47],[14.225,-40.19],[12.074,-41.48],[9.805,-42.55],[7.444,-43.4],[5.005,-44.01],[3.774,-44.23],[2.524,-44.39],[1.265,-44.48],[-1.255,-44.48],[-2.516,-44.39],[-3.766,-44.23],[-5.005,-44.01],[-7.436,-43.4],[-9.795,-42.55],[-12.065,-41.48],[-14.226,-40.19],[-15.255,-39.47],[-16.245,-38.7],[-17.205,-37.88],[-18.115,-37.01],[-18.985,-36.09],[-19.806,-35.14],[-20.585,-34.14],[-21.306,-33.11],[-22.585,-30.96],[-23.655,-28.69],[-24.505,-26.33],[-25.125,-23.9],[-25.335,-22.66],[-25.495,-21.41],[-25.596,-20.15],[-25.625,-18.89]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[249.995,331.386],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[14.346,0],[0,0],[0,14.346],[0,0],[0,0],[-0.029,0],[-0.029,0],[0,0]],"o":[[0,14.346],[0,0],[-14.346,0],[0,0],[0,0],[0.029,0],[0.029,0],[0,0],[0,0]],"v":[[135.391,151.042],[109.375,177.058],[-109.374,177.058],[-135.391,151.042],[-135.391,-10.395],[-109.456,-10.395],[-109.369,-10.391],[-109.282,-10.395],[135.391,-10.395]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,0],[-48.809,0],[0,0],[-4.304,-45.367],[-8.61,0.821],[0.816,8.604],[21.928,20.01],[29.913,0],[0,0],[0,-66.067],[0,0],[0,0],[0,-8.643],[0,0],[-31.604,0],[0,0],[0,31.604],[0,0]],"o":[[0,0],[0,0],[0,-48.809],[0,0],[45.725,0],[0.816,8.604],[8.605,-0.816],[-2.795,-29.465],[-22.048,-20.119],[0,0],[-66.067,0],[0,0],[0,0],[-8.644,0],[0,0],[0,31.604],[0,0],[31.604,0],[0,0],[0,-8.643]],"v":[[151.041,-41.695],[-93.719,-41.695],[-93.719,-88.541],[-5.202,-177.058],[5.215,-177.058],[92.969,-97.48],[110.026,-83.378],[124.129,-100.436],[85.791,-177.159],[5.215,-208.358],[-5.202,-208.358],[-125.02,-88.541],[-125.02,-41.695],[-151.041,-41.695],[-166.691,-26.045],[-166.691,151.042],[-109.374,208.358],[109.375,208.358],[166.691,151.042],[166.691,-26.045]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[249.997,250.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":30,"op":332,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.267],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":15.096,"s":[-18]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":23,"s":[14]},{"t":31,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.267,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[249.997,333.333,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":11,"s":[192.997,333.333,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":20,"s":[273.997,333.333,0],"to":[0,0,0],"ti":[0,0,0]},{"t":29,"s":[249.997,333.333,0]}],"ix":2,"l":2},"a":{"a":0,"k":[249.997,333.333,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[0,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":51.25,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[249.998,312.498],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-26.478],[0,26.478]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[250.005,333.768],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-23.012,0],[0,0],[0,23.012],[0,0]],"o":[[0,0],[0,23.012],[0,0],[23.013,0],[0,0],[0,0]],"v":[[-151.041,-109.377],[-151.041,67.71],[-109.374,109.377],[109.374,109.377],[151.041,67.71],[151.041,-109.377]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[249.997,333.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":30,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[140.623,223.957,0],"ix":2,"l":2},"a":{"a":0,"k":[-109.375,83.332,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[57.53,0],[0,0],[0,-57.53],[0,0]],"o":[[0,0],[0,-57.53],[0,0],[-57.53,0],[0,0],[0,0]],"v":[[109.248,20.332],[109.375,20.835],[5.208,-83.332],[-5.208,-83.332],[-109.375,20.835],[-109.375,83.332]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":9.059,"s":[{"i":[[0,0],[0,0],[57.53,0],[0,0],[0,-57.53],[0,0]],"o":[[0,0],[0,-57.53],[0,0],[-57.53,0],[0,0],[0,0]],"v":[[110.108,40.71],[110.235,41.213],[6.068,-62.954],[-4.348,-62.954],[-108.515,41.213],[-109.375,83.332]],"c":false}]},{"t":13.587890625,"s":[{"i":[[0,0],[0,0],[57.53,0],[0,0],[0,-57.53],[0,0]],"o":[[0,0],[0,-57.53],[0,0],[-57.53,0],[0,0],[0,0]],"v":[[109.248,20.332],[109.375,20.835],[5.208,-83.332],[-5.208,-83.332],[-109.375,20.835],[-109.375,83.332]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tm","s":{"a":0,"k":2.5,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 2","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('90-lock-closed-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":30,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"lordicon.com Outlines","cl":"com","sr":1,"ks":{"o":{"a":0,"k":20,"ix":11,"x":"var $bm_rt;\nvar checkbox = thisComp.layer('02092020').effect('02092020002')('Checkbox');\nif (checkbox == 1) {\n $bm_rt = 20;\n} else {\n $bm_rt = 0;\n}\n;"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.934,481.369,0],"ix":2,"l":2},"a":{"a":0,"k":[79.934,0.369,0],"ix":1,"l":2},"s":{"a":0,"k":[265.159,265.159,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[1.415,0],[11.014,0],[11.014,-2.523],[4.656,-2.523],[4.656,-14.809],[1.415,-14.809]],"c":true},"ix":2},"nm":"l","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"l","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[11.167,-7.199],[12.992,-1.661],[18.243,0.369],[23.514,-1.743],[25.381,-7.548],[23.494,-13.127],[18.284,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[14.49,-7.302],[15.577,-11.609],[18.305,-12.86],[21.689,-10.235],[22.058,-7.589],[21.053,-3.343],[18.284,-1.969],[15.597,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[-0.287,-0.841],[-0.144,-0.82],[0,0],[0.164,0.656],[0.226,1.743],[2.236,0.205],[0,2.769],[0.923,0.8],[1.641,-0.021],[0,0]],"o":[[0,0],[0,0],[0,0],[0.533,0],[0.205,0.574],[0,0],[-0.164,-0.246],[-0.103,-0.41],[-0.267,-1.928],[0.718,-0.205],[0,-0.964],[-1.19,-1.026],[0,0],[0,0]],"v":[[27.381,0],[30.622,0],[30.622,-5.989],[33.411,-5.989],[35.011,-5.148],[35.811,0],[39.318,0],[38.867,-1.067],[38.416,-3.938],[35.749,-7.343],[38.847,-10.973],[37.554,-13.824],[33.063,-14.829],[27.381,-14.829]],"c":true},"ix":2},"nm":"r","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-0.492,-0.349],[0,-1.005],[0.226,-0.164],[0.369,0],[0,0]],"o":[[0,0],[1.005,0],[0.287,0.185],[0,1.046],[-0.513,0.41],[0,0],[0,0]],"v":[[30.519,-12.491],[32.652,-12.491],[34.744,-12.142],[35.524,-10.481],[34.703,-8.758],[33.083,-8.348],[30.519,-8.348]],"c":true},"ix":2},"nm":"r","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"r","np":5,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-0.554,0.103],[0,4.553],[1.866,1.374],[0.82,0],[0,0]],"o":[[0,0],[1.497,0],[2.81,-0.513],[0,-2.113],[-1.784,-1.313],[0,0],[0,0]],"v":[[41.068,0],[45.683,0],[48.349,-0.164],[53.6,-7.609],[51.077,-13.434],[45.97,-14.768],[41.068,-14.788]],"c":true},"ix":2},"nm":"d","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-0.656,-0.185],[0,-2.092],[1.251,-1.251],[1.354,0],[0.349,0.021]],"o":[[1.825,-0.082],[1.99,0.554],[0,0.718],[-0.923,0.923],[-0.369,0],[0,0]],"v":[[44.288,-12.388],[47.611,-12.183],[50.318,-7.609],[48.985,-3.425],[45.539,-2.4],[44.288,-2.441]],"c":true},"ix":2},"nm":"d","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"d","np":5,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[55.669,0],[58.849,0],[58.849,-14.87],[55.669,-14.87]],"c":true},"ix":2},"nm":"i","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"i","np":3,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[3.241,0],[0,-4.697],[-5.107,0],[-1.313,1.354],[-0.062,0.882],[0,0],[1.333,0],[0,0.882],[-2.359,0],[-0.062,-0.513]],"o":[[0,-2.954],[-4.164,0],[0,3.671],[1.354,0],[1.19,-1.231],[0,0],[-0.062,1.969],[-3.097,0],[0,-3.056],[2.154,0],[0,0]],"v":[[73.104,-9.989],[67.587,-14.911],[60.798,-7.097],[67.566,0.349],[71.894,-1.313],[73.227,-4.799],[69.884,-4.799],[67.218,-1.99],[64.121,-7.076],[67.464,-12.593],[69.864,-9.989]],"c":true},"ix":2},"nm":"c","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"c","np":3,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[74.546,-7.199],[76.372,-1.661],[81.622,0.369],[86.894,-1.743],[88.76,-7.548],[86.873,-13.127],[81.663,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[77.869,-7.302],[78.956,-11.609],[81.684,-12.86],[85.068,-10.235],[85.437,-7.589],[84.432,-3.343],[81.663,-1.969],[78.977,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[91.007,0],[94.001,0],[94.001,-12.306],[99.744,0],[104.113,0],[104.113,-14.829],[101.159,-14.829],[101.159,-3.159],[95.601,-14.829],[91.007,-14.829]],"c":true},"ix":2},"nm":"n","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"n","np":3,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[106.893,0],[109.497,0],[109.497,-2.728],[106.893,-2.728]],"c":true},"ix":2},"nm":".","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":".","np":3,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[3.241,0],[0,-4.697],[-5.107,0],[-1.313,1.354],[-0.062,0.882],[0,0],[1.333,0],[0,0.882],[-2.359,0],[-0.062,-0.513]],"o":[[0,-2.954],[-4.164,0],[0,3.671],[1.354,0],[1.19,-1.231],[0,0],[-0.062,1.969],[-3.097,0],[0,-3.056],[2.154,0],[0,0]],"v":[[124.04,-9.989],[118.523,-14.911],[111.734,-7.097],[118.502,0.349],[122.83,-1.313],[124.163,-4.799],[120.82,-4.799],[118.154,-1.99],[115.057,-7.076],[118.4,-12.593],[120.8,-9.989]],"c":true},"ix":2},"nm":"c","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"c","np":3,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[125.482,-7.199],[127.308,-1.661],[132.558,0.369],[137.829,-1.743],[139.696,-7.548],[137.809,-13.127],[132.599,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[128.805,-7.302],[129.892,-11.609],[132.62,-12.86],[136.004,-10.235],[136.373,-7.589],[135.368,-3.343],[132.599,-1.969],[129.912,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[141.696,0],[144.67,0],[144.67,-12.716],[148.629,0],[151.254,0],[155.295,-12.716],[155.295,0],[158.453,0],[158.453,-14.829],[153.408,-14.829],[150.024,-4.041],[146.885,-14.829],[141.696,-14.829]],"c":true},"ix":2},"nm":"m","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"m","np":3,"cix":2,"bm":0,"ix":12,"mn":"ADBE Vector Group","hd":false}],"ip":2.5,"op":25,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":3,"nm":"02092020","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-105,15,0],"ix":2,"l":2},"a":{"a":0,"k":[60,60,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"02092020002","np":3,"mn":"ADBE Checkbox Control","ix":1,"en":1,"ef":[{"ty":7,"nm":"Checkbox","mn":"ADBE Checkbox Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"Color & Stroke Change","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-55,-102,0],"ix":2,"l":2,"x":"var $bm_rt;\n$bm_rt = effect('Axis')('Point');"},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2,"x":"var $bm_rt;\nvar temp;\ntemp = effect('Scale')('Slider');\n$bm_rt = [\n temp,\n temp\n];"}},"ao":0,"ef":[{"ty":5,"nm":"Primary","np":3,"mn":"ADBE Color Control","ix":1,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]},{"ty":5,"nm":"Axis","np":3,"mn":"ADBE Point Control","ix":2,"en":1,"ef":[{"ty":3,"nm":"Point","mn":"ADBE Point Control-0001","ix":1,"v":{"a":0,"k":[250,250],"ix":1}}]},{"ty":5,"nm":"Scale","np":3,"mn":"ADBE Slider Control","ix":3,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":100,"ix":1}}]},{"ty":5,"nm":"State-Intro-Lock","np":3,"mn":"ADBE Slider Control","ix":4,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]},{"ty":5,"nm":"State-Hover-Lock","np":3,"mn":"ADBE Slider Control","ix":5,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":1,"ix":1}}]},{"ty":5,"nm":"State-Morph-Lock-Unlock","np":3,"mn":"ADBE Slider Control","ix":6,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]},{"ty":5,"nm":"State-Intro-Unlock","np":3,"mn":"ADBE Slider Control","ix":7,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]},{"ty":5,"nm":"State-Hover-Unlock","np":3,"mn":"ADBE Slider Control","ix":8,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"in-lock","parent":3,"refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Intro-Lock')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":41,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"hover-lock","parent":3,"refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Hover-Lock')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":41,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"morph-lock-unlock","parent":3,"refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Morph-Lock-Unlock')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":41,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"in-unlock","parent":3,"refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Intro-Unlock')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":41,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":0,"nm":"hover-unlock","parent":3,"refId":"comp_4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Hover-Unlock')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":41,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/frontend/public/lotties/system-outline-96-groups.json b/frontend/public/lotties/system-outline-96-groups.json deleted file mode 100644 index 22638b692..000000000 --- a/frontend/public/lotties/system-outline-96-groups.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.8.1","fr":60,"ip":0,"op":61,"w":500,"h":500,"nm":"96-groups-outline","ddd":0,"assets":[{"id":"comp_0","nm":"in-groups","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.003,260.41,0],"ix":2,"l":2},"a":{"a":0,"k":[250.003,260.41,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,8.643],[-8.644,0],[0,0],[24.064,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-43.092],[0,0]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[-2.61,-23.379],[0,0],[-8.644,0],[0,-8.643],[0,0],[43.092,0],[0,0],[0,8.643]],"v":[[46.875,52.108],[-46.875,52.108],[-62.526,36.458],[-46.875,20.809],[30.935,20.809],[-15.625,-20.809],[-46.875,-20.809],[-62.526,-36.458],[-46.875,-52.108],[-15.625,-52.108],[62.526,26.042],[62.526,36.458]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[395.837,270.826],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,8.643],[0,0],[-43.093,0],[0,0],[0,-8.643],[8.644,0],[0,0],[2.61,-23.379],[0,0],[0,-8.643]],"o":[[0,0],[-8.644,0],[0,0],[0,-43.092],[0,0],[8.644,0],[0,8.643],[0,0],[-24.064,0],[0,0],[8.644,0],[0,8.643]],"v":[[46.875,52.108],[-46.875,52.108],[-62.526,36.458],[-62.526,26.042],[15.625,-52.108],[46.875,-52.108],[62.526,-36.458],[46.875,-20.809],[15.625,-20.809],[-30.935,20.809],[46.875,20.809],[62.526,36.458]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[104.169,270.829],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.608,-23.381],[0,0],[24.066,0],[0,0]],"o":[[0,0],[-2.608,-23.381],[0,0],[-24.065,0]],"v":[[-93.435,20.81],[93.435,20.81],[46.874,-20.81],[-46.875,-20.81]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,8.643],[0,0],[-43.092,0],[0,0],[0,-43.092],[0,0]],"o":[[0,0],[-8.644,0],[0,0],[0,-43.092],[0,0],[43.093,0],[0,0],[0,8.643]],"v":[[109.375,52.11],[-109.375,52.11],[-125.025,36.46],[-125.025,26.041],[-46.875,-52.11],[46.874,-52.11],[125.025,26.041],[125.025,36.46]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,406.242],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":3,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-12.586],[0,0],[-12.587,0],[0,0],[0,0],[12.586,0]],"o":[[-12.587,0],[0,0],[0,12.586],[0,0],[0,0],[0,-12.586],[0,0]],"v":[[-3.19,-36.434],[-26.017,-13.607],[-26.017,13.608],[-3.19,36.434],[26.017,36.434],[26.017,-13.607],[3.191,-36.434]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,29.845],[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0]],"o":[[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0],[29.845,0],[0,0],[0,8.643]],"v":[[41.667,67.733],[-3.19,67.733],[-57.317,13.608],[-57.317,-13.607],[-3.19,-67.733],[3.191,-67.733],[57.317,-13.607],[57.317,52.083]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.003,265.62],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":3,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-12.586],[0,0],[-12.587,0],[0,0],[0,0],[12.586,0]],"o":[[-12.587,0],[0,0],[0,12.586],[0,0],[0,0],[0,-12.586],[0,0]],"v":[[-3.19,-36.434],[-26.017,-13.607],[-26.017,13.608],[-3.19,36.434],[26.017,36.434],[26.017,-13.607],[3.191,-36.434]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,29.845],[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0]],"o":[[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0],[29.845,0],[0,0],[0,8.643]],"v":[[41.667,67.734],[-3.19,67.734],[-57.317,13.608],[-57.317,-13.607],[-3.19,-67.734],[3.191,-67.734],[57.317,-13.607],[57.317,52.084]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[151.044,130.202],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":3,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-12.586],[0,0],[-12.587,0],[0,0],[0,0],[12.586,0]],"o":[[-12.587,0],[0,0],[0,12.586],[0,0],[0,0],[0,-12.586],[0,0]],"v":[[-3.19,-36.434],[-26.016,-13.607],[-26.016,13.608],[-3.19,36.434],[26.016,36.434],[26.016,-13.607],[3.192,-36.434]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,29.845],[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0]],"o":[[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0],[29.845,0],[0,0],[0,8.643]],"v":[[41.667,67.734],[-3.19,67.734],[-57.317,13.608],[-57.317,-13.607],[-3.19,-67.734],[3.192,-67.734],[57.317,-13.607],[57.317,52.084]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[343.753,130.202],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":3,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":0.20066889632107,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.004,406.242,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.413,"y":1},"o":{"x":0.167,"y":0.167},"t":1,"s":[{"i":[[0.761,0],[0,0],[0,-0.673],[0,0],[0,0],[0,0]],"o":[[0,0],[-0.673,0],[0,0],[0,0],[0,0],[0,-0.761]],"v":[[107.996,69.54],[-108.157,69.54],[-109.375,70.758],[-109.375,72.46],[109.375,72.46],[109.375,70.919]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[34.518,0],[0,0],[0,-34.517],[0,0],[0,0],[0,0]],"o":[[0,0],[-34.518,0],[0,0],[0,0],[0,0],[0,-34.517]],"v":[[46.875,-51.46],[-46.875,-51.46],[-109.375,11.04],[-109.375,34.46],[109.375,34.46],[109.375,11.04]],"c":true}]},{"t":35.9998693561873,"s":[{"i":[[34.518,0],[0,0],[0,-34.517],[0,0],[0,0],[0,0]],"o":[[0,0],[-34.518,0],[0,0],[0,0],[0,0],[0,-34.517]],"v":[[46.875,-36.46],[-46.875,-36.46],[-109.375,26.041],[-109.375,36.46],[109.375,36.46],[109.375,26.041]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":0.79933110367893,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":0.20066889632107,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.413,"y":1},"o":{"x":0.167,"y":0.167},"t":1,"s":[250.003,425.62,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[250.003,230.62,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":36,"s":[250.003,287.62,0],"to":[0,0,0],"ti":[0,0,0]},{"t":52.9998954849498,"s":[250.003,265.62,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.413,"y":1},"o":{"x":0.167,"y":0.167},"t":1,"s":[{"i":[[1.198,0],[0,0],[0,-1.195],[0,0],[-0.643,0],[0,0],[0,0]],"o":[[0,0],[-1.195,0],[0,0],[0,0.643],[0,0],[0,0],[0,-1.198]],"v":[[39.497,47.416],[-39.503,47.416],[-41.667,49.58],[-41.667,50.92],[-40.503,52.084],[41.667,52.084],[41.667,49.586]],"c":true}]},{"t":18.0000261287625,"s":[{"i":[[21.249,0],[0,0],[0,-21.251],[0,0],[-21.249,0],[0,0],[0,0]],"o":[[0,0],[-21.249,0],[0,0],[0,21.249],[0,0],[0,0],[0,-21.251]],"v":[[3.192,-52.084],[-3.19,-52.084],[-41.667,-13.607],[-41.667,13.608],[-3.19,52.084],[41.667,52.084],[41.667,-13.607]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":0.79933110367893,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":0.20066889632107,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[104.17,270.829,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.413,"y":1},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[0,0],[0,0],[0,0],[-4.222,0],[0,0]],"o":[[0,0],[0,0],[0,-4.222],[0,0],[0,0]],"v":[[47.01,112.459],[-46.741,112.459],[-46.741,111.171],[-39.111,103.541],[47.01,103.541]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":21,"s":[{"i":[[0,0],[0,0],[0,0],[-34.583,0],[0,0]],"o":[[0,0],[0,0],[0,-34.584],[0,0],[0,0]],"v":[[46.875,36.459],[-46.875,36.459],[-46.875,-6.958],[15.625,-69.459],[46.875,-69.459]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":39,"s":[{"i":[[0,0],[0,0],[0,0],[-34.583,0],[0,0]],"o":[[0,0],[0,0],[0,-34.584],[0,0],[0,0]],"v":[[46.875,36.459],[-46.875,36.459],[-46.875,26.042],[15.625,-36.459],[46.875,-36.459]],"c":false}]},{"t":55.9998954849498,"s":[{"i":[[0,0],[0,0],[0,0],[-34.583,0],[0,0]],"o":[[0,0],[0,0],[0,-34.584],[0,0],[0,0]],"v":[[46.875,36.459],[-46.875,36.459],[-46.875,26.042],[15.625,-36.459],[46.875,-36.459]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":4,"op":60,"st":3.79933110367893,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".primary.design","cl":"primary design","sr":0.20066889632107,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.413,"y":1},"o":{"x":0.167,"y":0.167},"t":4,"s":[151.044,324.202,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":21,"s":[151.044,95.202,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":39,"s":[151.044,140.202,0],"to":[0,0,0],"ti":[0,0,0]},{"t":55.9998954849498,"s":[151.044,130.202,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.413,"y":1},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[0.393,0],[0,0],[0,-0.344],[0,0],[-0.434,0],[0,0],[0,0]],"o":[[0,0],[-0.344,0],[0,0],[0,0.434],[0,0],[0,0],[0,-0.393]],"v":[[40.956,50.416],[-41.044,50.416],[-41.667,51.039],[-41.667,51.298],[-40.881,52.084],[41.667,52.084],[41.667,51.127]],"c":true}]},{"t":21.0000261287625,"s":[{"i":[[21.249,0],[0,0],[0,-21.251],[0,0],[-21.249,0],[0,0],[0,0]],"o":[[0,0],[-21.249,0],[0,0],[0,21.249],[0,0],[0,0],[0,-21.251]],"v":[[3.192,-52.084],[-3.19,-52.084],[-41.667,-13.607],[-41.667,13.608],[-3.19,52.084],[41.667,52.084],[41.667,-13.607]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":4,"op":60,"st":3.79933110367893,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".primary.design","cl":"primary design","sr":0.20066889632107,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.413,"y":1},"o":{"x":0.167,"y":0.167},"t":8,"s":[343.753,329.202,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":25,"s":[343.753,95.202,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":43,"s":[343.753,141.202,0],"to":[0,0,0],"ti":[0,0,0]},{"t":59.9998954849498,"s":[343.753,130.202,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.413,"y":1},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[0.37,0],[0,0],[0,-0.001],[0,0],[-0.572,0],[0,0],[0,0]],"o":[[0,0],[-0.504,0],[0,0],[0,0.002],[0,0],[0,0],[0,-0.001]],"v":[[40.997,51.798],[-40.753,51.798],[-41.667,51.8],[-41.667,52.081],[-40.631,52.084],[41.667,52.084],[41.667,51.799]],"c":true}]},{"t":25.0000261287625,"s":[{"i":[[21.249,0],[0,0],[0,-21.251],[0,0],[-21.249,0],[0,0],[0,0]],"o":[[0,0],[-21.249,0],[0,0],[0,21.249],[0,0],[0,0],[0,-21.251]],"v":[[3.192,-52.084],[-3.19,-52.084],[-41.667,-13.607],[-41.667,13.608],[-3.19,52.084],[41.667,52.084],[41.667,-13.607]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":8,"op":60,"st":7.79933110367893,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":".primary.design","cl":"primary design","sr":0.20066889632107,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[395.838,270.826,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.413,"y":1},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[0,0],[0,0],[0,0],[0.948,0],[0,0]],"o":[[0,0],[0,0],[0,-0.948],[0,0],[0,0]],"v":[[-46.913,113.459],[46.838,113.459],[46.8,112.254],[45.087,110.541],[-46.95,110.541]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":25,"s":[{"i":[[0,0],[0,0],[0,0],[34.583,0],[0,0]],"o":[[0,0],[0,0],[0,-34.584],[0,0],[0,0]],"v":[[-46.875,36.459],[46.875,36.459],[46.588,-7.958],[-15.912,-70.459],[-47.162,-70.459]],"c":false}]},{"t":42.9998693561873,"s":[{"i":[[0,0],[0,0],[0,0],[34.583,0],[0,0]],"o":[[0,0],[0,0],[0,-34.584],[0,0],[0,0]],"v":[[-46.875,36.459],[46.875,36.459],[46.875,26.042],[-15.625,-36.459],[-46.875,-36.459]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":8,"op":60,"st":7.79933110367893,"bm":0}]},{"id":"comp_1","nm":"hover-groups","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.003,260.41,0],"ix":2,"l":2},"a":{"a":0,"k":[250.003,260.41,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,8.643],[-8.644,0],[0,0],[24.064,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-43.092],[0,0]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[-2.61,-23.379],[0,0],[-8.644,0],[0,-8.643],[0,0],[43.092,0],[0,0],[0,8.643]],"v":[[46.875,52.108],[-46.875,52.108],[-62.526,36.458],[-46.875,20.809],[30.935,20.809],[-15.625,-20.809],[-46.875,-20.809],[-62.526,-36.458],[-46.875,-52.108],[-15.625,-52.108],[62.526,26.042],[62.526,36.458]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[395.837,270.826],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,8.643],[0,0],[-43.093,0],[0,0],[0,-8.643],[8.644,0],[0,0],[2.61,-23.379],[0,0],[0,-8.643]],"o":[[0,0],[-8.644,0],[0,0],[0,-43.092],[0,0],[8.644,0],[0,8.643],[0,0],[-24.064,0],[0,0],[8.644,0],[0,8.643]],"v":[[46.875,52.108],[-46.875,52.108],[-62.526,36.458],[-62.526,26.042],[15.625,-52.108],[46.875,-52.108],[62.526,-36.458],[46.875,-20.809],[15.625,-20.809],[-30.935,20.809],[46.875,20.809],[62.526,36.458]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[104.169,270.829],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.608,-23.381],[0,0],[24.066,0],[0,0]],"o":[[0,0],[-2.608,-23.381],[0,0],[-24.065,0]],"v":[[-93.435,20.81],[93.435,20.81],[46.874,-20.81],[-46.875,-20.81]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,8.643],[0,0],[-43.092,0],[0,0],[0,-43.092],[0,0]],"o":[[0,0],[-8.644,0],[0,0],[0,-43.092],[0,0],[43.093,0],[0,0],[0,8.643]],"v":[[109.375,52.11],[-109.375,52.11],[-125.025,36.46],[-125.025,26.041],[-46.875,-52.11],[46.874,-52.11],[125.025,26.041],[125.025,36.46]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,406.242],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":3,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-12.586],[0,0],[-12.587,0],[0,0],[0,0],[12.586,0]],"o":[[-12.587,0],[0,0],[0,12.586],[0,0],[0,0],[0,-12.586],[0,0]],"v":[[-3.19,-36.434],[-26.017,-13.607],[-26.017,13.608],[-3.19,36.434],[26.017,36.434],[26.017,-13.607],[3.191,-36.434]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,29.845],[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0]],"o":[[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0],[29.845,0],[0,0],[0,8.643]],"v":[[41.667,67.733],[-3.19,67.733],[-57.317,13.608],[-57.317,-13.607],[-3.19,-67.733],[3.191,-67.733],[57.317,-13.607],[57.317,52.083]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.003,265.62],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":3,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-12.586],[0,0],[-12.587,0],[0,0],[0,0],[12.586,0]],"o":[[-12.587,0],[0,0],[0,12.586],[0,0],[0,0],[0,-12.586],[0,0]],"v":[[-3.19,-36.434],[-26.017,-13.607],[-26.017,13.608],[-3.19,36.434],[26.017,36.434],[26.017,-13.607],[3.191,-36.434]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,29.845],[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0]],"o":[[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0],[29.845,0],[0,0],[0,8.643]],"v":[[41.667,67.734],[-3.19,67.734],[-57.317,13.608],[-57.317,-13.607],[-3.19,-67.734],[3.191,-67.734],[57.317,-13.607],[57.317,52.084]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[151.044,130.202],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":3,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-12.586],[0,0],[-12.587,0],[0,0],[0,0],[12.586,0]],"o":[[-12.587,0],[0,0],[0,12.586],[0,0],[0,0],[0,-12.586],[0,0]],"v":[[-3.19,-36.434],[-26.016,-13.607],[-26.016,13.608],[-3.19,36.434],[26.016,36.434],[26.016,-13.607],[3.192,-36.434]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,29.845],[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0]],"o":[[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0],[29.845,0],[0,0],[0,8.643]],"v":[[41.667,67.734],[-3.19,67.734],[-57.317,13.608],[-57.317,-13.607],[-3.19,-67.734],[3.192,-67.734],[57.317,-13.607],[57.317,52.084]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[343.753,130.202],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":3,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":-60,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.003,260.41,0],"ix":2,"l":2},"a":{"a":0,"k":[250.003,260.41,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,8.643],[-8.644,0],[0,0],[24.064,0],[0,0],[0,8.643],[-8.644,0],[0,0],[0,-43.092],[0,0]],"o":[[0,0],[-8.644,0],[0,-8.643],[0,0],[-2.61,-23.379],[0,0],[-8.644,0],[0,-8.643],[0,0],[43.092,0],[0,0],[0,8.643]],"v":[[46.875,52.108],[-46.875,52.108],[-62.526,36.458],[-46.875,20.809],[30.935,20.809],[-15.625,-20.809],[-46.875,-20.809],[-62.526,-36.458],[-46.875,-52.108],[-15.625,-52.108],[62.526,26.042],[62.526,36.458]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[395.837,270.826],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,8.643],[0,0],[-43.093,0],[0,0],[0,-8.643],[8.644,0],[0,0],[2.61,-23.379],[0,0],[0,-8.643]],"o":[[0,0],[-8.644,0],[0,0],[0,-43.092],[0,0],[8.644,0],[0,8.643],[0,0],[-24.064,0],[0,0],[8.644,0],[0,8.643]],"v":[[46.875,52.108],[-46.875,52.108],[-62.526,36.458],[-62.526,26.042],[15.625,-52.108],[46.875,-52.108],[62.526,-36.458],[46.875,-20.809],[15.625,-20.809],[-30.935,20.809],[46.875,20.809],[62.526,36.458]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[104.169,270.829],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.608,-23.381],[0,0],[24.066,0],[0,0]],"o":[[0,0],[-2.608,-23.381],[0,0],[-24.065,0]],"v":[[-93.435,20.81],[93.435,20.81],[46.874,-20.81],[-46.875,-20.81]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,8.643],[0,0],[-43.092,0],[0,0],[0,-43.092],[0,0]],"o":[[0,0],[-8.644,0],[0,0],[0,-43.092],[0,0],[43.093,0],[0,0],[0,8.643]],"v":[[109.375,52.11],[-109.375,52.11],[-125.025,36.46],[-125.025,26.041],[-46.875,-52.11],[46.874,-52.11],[125.025,26.041],[125.025,36.46]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.004,406.242],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":3,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-12.586],[0,0],[-12.587,0],[0,0],[0,0],[12.586,0]],"o":[[-12.587,0],[0,0],[0,12.586],[0,0],[0,0],[0,-12.586],[0,0]],"v":[[-3.19,-36.434],[-26.017,-13.607],[-26.017,13.608],[-3.19,36.434],[26.017,36.434],[26.017,-13.607],[3.191,-36.434]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,29.845],[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0]],"o":[[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0],[29.845,0],[0,0],[0,8.643]],"v":[[41.667,67.733],[-3.19,67.733],[-57.317,13.608],[-57.317,-13.607],[-3.19,-67.733],[3.191,-67.733],[57.317,-13.607],[57.317,52.083]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.003,265.62],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":3,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-12.586],[0,0],[-12.587,0],[0,0],[0,0],[12.586,0]],"o":[[-12.587,0],[0,0],[0,12.586],[0,0],[0,0],[0,-12.586],[0,0]],"v":[[-3.19,-36.434],[-26.017,-13.607],[-26.017,13.608],[-3.19,36.434],[26.017,36.434],[26.017,-13.607],[3.191,-36.434]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,29.845],[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0]],"o":[[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0],[29.845,0],[0,0],[0,8.643]],"v":[[41.667,67.734],[-3.19,67.734],[-57.317,13.608],[-57.317,-13.607],[-3.19,-67.734],[3.191,-67.734],[57.317,-13.607],[57.317,52.084]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[151.044,130.202],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":3,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-12.586],[0,0],[-12.587,0],[0,0],[0,0],[12.586,0]],"o":[[-12.587,0],[0,0],[0,12.586],[0,0],[0,0],[0,-12.586],[0,0]],"v":[[-3.19,-36.434],[-26.016,-13.607],[-26.016,13.608],[-3.19,36.434],[26.016,36.434],[26.016,-13.607],[3.192,-36.434]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.644,0],[0,0],[0,29.845],[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0]],"o":[[0,0],[-29.846,0],[0,0],[0,-29.846],[0,0],[29.845,0],[0,0],[0,8.643]],"v":[[41.667,67.734],[-3.19,67.734],[-57.317,13.608],[-57.317,-13.607],[-3.19,-67.734],[3.192,-67.734],[57.317,-13.607],[57.317,52.084]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[343.753,130.202],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":3,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":0.20066889632107,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.004,406.242,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":0,"s":[{"i":[[34.518,0],[0,0],[0,-34.517],[0,0],[0,0],[0,0]],"o":[[0,0],[-34.518,0],[0,0],[0,0],[0,0],[0,-34.517]],"v":[[46.875,-36.46],[-46.875,-36.46],[-109.375,26.041],[-109.375,36.46],[109.375,36.46],[109.375,26.041]],"c":true}]},{"i":{"x":0.413,"y":1},"o":{"x":0.333,"y":0},"t":14,"s":[{"i":[[22.97,0],[0,0],[0,-22.939],[0,0],[0,0],[0,0]],"o":[[0,0],[-22.94,0],[0,0],[0,0],[0,0],[0,-22.969]],"v":[[67.784,-10.067],[-67.839,-10.067],[-109.375,31.469],[-109.375,47.459],[109.375,47.459],[109.375,31.524]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30.001,"s":[{"i":[[34.518,0],[0,0],[0,-34.517],[0,0],[0,0],[0,0]],"o":[[0,0],[-34.518,0],[0,0],[0,0],[0,0],[0,-34.517]],"v":[[46.875,-51.46],[-46.875,-51.46],[-109.375,11.04],[-109.375,34.46],[109.375,34.46],[109.375,11.04]],"c":true}]},{"t":42.3432796822742,"s":[{"i":[[34.518,0],[0,0],[0,-34.517],[0,0],[0,0],[0,0]],"o":[[0,0],[-34.518,0],[0,0],[0,0],[0,0],[0,-34.517]],"v":[[46.875,-36.46],[-46.875,-36.46],[-109.375,26.041],[-109.375,36.46],[109.375,36.46],[109.375,26.041]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":0.79933110367893,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":0.20066889632107,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":0,"s":[250.003,265.62,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.572,"y":1},"o":{"x":0.333,"y":0},"t":14,"s":[250.003,341.327,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30.001,"s":[250.003,230.62,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":42.343,"s":[250.003,287.62,0],"to":[0,0,0],"ti":[0,0,0]},{"t":54.0001045150502,"s":[250.003,265.62,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.413,"y":1},"o":{"x":0.167,"y":0},"t":0,"s":[{"i":[[21.249,0],[0,0],[0,-21.251],[0,0],[-21.249,0],[0,0],[0,0]],"o":[[0,0],[-21.249,0],[0,0],[0,21.249],[0,0],[0,0],[0,-21.251]],"v":[[3.192,-52.084],[-3.19,-52.084],[-41.667,-13.607],[-41.667,13.608],[-3.19,52.084],[41.667,52.084],[41.667,-13.607]],"c":true}]},{"t":30.0005748327759,"s":[{"i":[[21.249,0],[0,0],[0,-21.251],[0,0],[-21.249,0],[0,0],[0,0]],"o":[[0,0],[-21.249,0],[0,0],[0,21.249],[0,0],[0,0],[0,-21.251]],"v":[[3.192,-52.084],[-3.19,-52.084],[-41.667,-13.607],[-41.667,13.608],[-3.19,52.084],[41.667,52.084],[41.667,-13.607]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":0.79933110367893,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".primary.design","cl":"primary design","sr":0.20066889632107,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[104.17,270.829,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0],[-34.583,0],[0,0]],"o":[[0,0],[0,0],[0,-34.584],[0,0],[0,0]],"v":[[46.875,36.459],[-46.875,36.459],[-46.875,26.042],[15.625,-36.459],[46.875,-36.459]],"c":false}]},{"i":{"x":0.413,"y":1},"o":{"x":0.333,"y":0},"t":17,"s":[{"i":[[0,0],[0,0],[0,0],[-24.197,0],[0,0]],"o":[[0,0],[0,0],[0,-24.198],[0,0],[0,0]],"v":[[46.921,62.457],[-46.829,62.457],[-46.829,33.453],[-3.099,-10.277],[46.921,-10.277]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":33.001,"s":[{"i":[[0,0],[0,0],[0,0],[-34.583,0],[0,0]],"o":[[0,0],[0,0],[0,-34.584],[0,0],[0,0]],"v":[[46.875,36.459],[-46.875,36.459],[-46.875,-6.958],[15.625,-69.459],[46.875,-69.459]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":45.343,"s":[{"i":[[0,0],[0,0],[0,0],[-34.583,0],[0,0]],"o":[[0,0],[0,0],[0,-34.584],[0,0],[0,0]],"v":[[46.875,36.459],[-46.875,36.459],[-46.875,26.042],[15.625,-36.459],[46.875,-36.459]],"c":false}]},{"t":57.0001045150502,"s":[{"i":[[0,0],[0,0],[0,0],[-34.583,0],[0,0]],"o":[[0,0],[0,0],[0,-34.584],[0,0],[0,0]],"v":[[46.875,36.459],[-46.875,36.459],[-46.875,26.042],[15.625,-36.459],[46.875,-36.459]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":3.79933110367893,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".primary.design","cl":"primary design","sr":0.20066889632107,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":0,"s":[151.044,130.202,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.572,"y":1},"o":{"x":0.333,"y":0},"t":17,"s":[151.044,207.541,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":33.001,"s":[151.044,95.202,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":45.343,"s":[151.044,140.202,0],"to":[0,0,0],"ti":[0,0,0]},{"t":57.0001045150502,"s":[151.044,130.202,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.413,"y":1},"o":{"x":0.167,"y":0},"t":0,"s":[{"i":[[21.249,0],[0,0],[0,-21.251],[0,0],[-21.249,0],[0,0],[0,0]],"o":[[0,0],[-21.249,0],[0,0],[0,21.249],[0,0],[0,0],[0,-21.251]],"v":[[3.192,-52.084],[-3.19,-52.084],[-41.667,-13.607],[-41.667,13.608],[-3.19,52.084],[41.667,52.084],[41.667,-13.607]],"c":true}]},{"t":33.0005748327759,"s":[{"i":[[21.249,0],[0,0],[0,-21.251],[0,0],[-21.249,0],[0,0],[0,0]],"o":[[0,0],[-21.249,0],[0,0],[0,21.249],[0,0],[0,0],[0,-21.251]],"v":[[3.192,-52.084],[-3.19,-52.084],[-41.667,-13.607],[-41.667,13.608],[-3.19,52.084],[41.667,52.084],[41.667,-13.607]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":3.79933110367893,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":".primary.design","cl":"primary design","sr":0.20066889632107,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[343.753,130.202,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.572,"y":1},"o":{"x":0.333,"y":0},"t":20,"s":[343.753,209.252,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":36,"s":[343.753,95.202,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":48.343,"s":[343.753,141.202,0],"to":[0,0,0],"ti":[0,0,0]},{"t":59.9998954849498,"s":[343.753,130.202,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.413,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[21.249,0],[0,0],[0,-21.251],[0,0],[-21.249,0],[0,0],[0,0]],"o":[[0,0],[-21.249,0],[0,0],[0,21.249],[0,0],[0,0],[0,-21.251]],"v":[[3.192,-52.084],[-3.19,-52.084],[-41.667,-13.607],[-41.667,13.608],[-3.19,52.084],[41.667,52.084],[41.667,-13.607]],"c":true}]},{"t":36.0003658026756,"s":[{"i":[[21.249,0],[0,0],[0,-21.251],[0,0],[-21.249,0],[0,0],[0,0]],"o":[[0,0],[-21.249,0],[0,0],[0,21.249],[0,0],[0,0],[0,-21.251]],"v":[[3.192,-52.084],[-3.19,-52.084],[-41.667,-13.607],[-41.667,13.608],[-3.19,52.084],[41.667,52.084],[41.667,-13.607]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":7.79933110367893,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":".primary.design","cl":"primary design","sr":0.20066889632107,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[395.838,270.826,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0],[34.583,0],[0,0]],"o":[[0,0],[0,0],[0,-34.584],[0,0],[0,0]],"v":[[-46.875,36.459],[46.875,36.459],[46.875,26.042],[-15.625,-36.459],[-46.875,-36.459]],"c":false}]},{"i":{"x":0.413,"y":1},"o":{"x":0.333,"y":0},"t":20,"s":[{"i":[[0,0],[0,0],[0,0],[23.077,0],[0,0]],"o":[[0,0],[0,0],[0,-23.078],[0,0],[0,0]],"v":[[-46.888,62.799],[46.862,62.799],[46.661,33.165],[4.955,-8.541],[-47.09,-8.541]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":36,"s":[{"i":[[0,0],[0,0],[0,0],[34.583,0],[0,0]],"o":[[0,0],[0,0],[0,-34.584],[0,0],[0,0]],"v":[[-46.875,36.459],[46.875,36.459],[46.588,-7.958],[-15.912,-70.459],[-47.162,-70.459]],"c":false}]},{"t":48.3430706521739,"s":[{"i":[[0,0],[0,0],[0,0],[34.583,0],[0,0]],"o":[[0,0],[0,0],[0,-34.584],[0,0],[0,0]],"v":[[-46.875,36.459],[46.875,36.459],[46.875,26.042],[-15.625,-36.459],[-46.875,-36.459]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":7.79933110367893,"bm":0}]},{"id":"comp_2","nm":"morph-group-single","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.002,249.974,0],"ix":2,"l":2},"a":{"a":0,"k":[250,249.999,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0.96],[0,0],[-0.96,0],[0,0],[0,-0.97]],"o":[[0,0],[-0.96,0],[0,0],[0,-0.96],[0,0],[0.96,0],[0,0]],"v":[[2,2.75],[-0.25,2.75],[-2,1],[-2,-1],[-0.25,-2.75],[0.25,-2.75],[2,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[1.79,0],[0,0],[0,-1.79],[0,0],[-1.79,0],[0,0],[0,0.41],[0,0]],"o":[[0,0],[-1.79,0],[0,0],[0,1.79],[0,0],[0.41,0],[0,0],[0,-1.79]],"v":[[0.25,-4.25],[-0.25,-4.25],[-3.5,-1],[-3.5,1],[-0.25,4.25],[2.75,4.25],[3.5,3.5],[3.5,-1]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250,247.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.38,-1.6],[1.78,0],[0,0],[0.8,-1.53],[0.04,1.99],[-1.57,1.64],[-2.27,0.05],[-0.07,0],[-1.6,-1.52],[-0.05,-2.27]],"o":[[-0.81,-1.52],[0,0],[-1.78,0],[-1.29,-1.49],[-0.06,-2.27],[1.57,-1.64],[0.07,0],[2.19,0],[1.64,1.57],[0.05,2.11]],"v":[[6.43,5.551],[2.25,3.011],[-2.25,3.011],[-6.43,5.561],[-8.49,0.211],[-6.15,-5.859],[-0.2,-8.489],[0.01,-8.489],[5.87,-6.139],[8.5,-0.189]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[1.87,-0.05],[1.55,1.25],[-1.37,0],[0,0],[-0.46,-1.25]],"o":[[-2.01,0.05],[0.46,-1.26],[0,0],[1.37,0],[-1.45,1.16]],"v":[[0.21,8.501],[-5.28,6.651],[-2.24,4.511],[2.26,4.511],[5.3,6.641]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[1.93,1.84],[2.67,-0.06],[1.84,-1.93],[-0.06,-2.67],[-1.93,-1.84],[-2.58,0],[-0.08,0],[-1.84,1.93],[0.06,2.67]],"o":[[-1.94,-1.84],[-2.67,0.06],[-1.84,1.94],[0.06,2.67],[1.88,1.79],[0.08,0],[2.67,-0.06],[1.84,-1.93],[-0.07,-2.67]],"v":[[6.9,-7.239],[-0.24,-9.999],[-7.24,-6.899],[-10,0.241],[-6.9,7.241],[-0.01,10.001],[0.24,10.001],[7.24,6.901],[10,-0.239]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250,249.999],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":4,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[250,249.999],"ix":2},"a":{"a":0,"k":[250,249.999],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-180,"ix":10},"p":{"a":0,"k":[250.004,250.003,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.24,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[-1.15,-47.582],[93.245,-2.254],[2.254,93.245],[-34.998,0.428],[-42.245,0.014],[-31.103,-0.386]],"o":[[2.254,93.245],[-93.245,2.254],[-1.233,-51],[28.99,-0.354],[45.664,-0.015],[32.41,0.402]],"v":[[168.908,-72],[4.153,100.917],[-168.764,-63.838],[-113.49,-192.393],[-4.009,-192.982],[114.626,-192.381]],"c":true}]},{"t":60,"s":[{"i":[[-1.312,-54.294],[106.399,-2.572],[2.572,106.399],[-39.935,36.311],[-48.204,1.165],[-35.491,-32.769]],"o":[[2.572,106.399],[-106.399,2.572],[-1.407,-58.194],[33.08,-30.077],[52.105,-1.259],[36.981,34.145]],"v":[[192.652,-4.656],[4.656,192.652],[-192.652,4.656],[-129.581,-142.64],[-4.656,-192.652],[130.713,-141.6]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.24],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":1,"s":[39]},{"t":60,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.24],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":1,"s":[60]},{"t":60,"s":[100]}],"ix":2},"o":{"a":0,"k":94,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":0.20066889632107,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.004,406.242,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.901},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[34.518,0],[0,0],[0,-34.517],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[-34.518,0],[0,0],[0,0],[0,0],[0,0],[0,-34.517]],"v":[[46.875,-36.46],[-46.875,-36.46],[-109.375,26.041],[-109.375,36.46],[4.181,36.46],[109.375,36.46],[109.375,26.041]],"c":true}]},{"i":{"x":0.24,"y":1},"o":{"x":0.167,"y":0.099},"t":16.172,"s":[{"i":[[34.557,0],[0,0],[0,-32.321],[0,0],[-73.674,0],[0,0],[0,0]],"o":[[0,0],[-38.202,0],[0,0],[0,0],[73.326,0],[0,0],[-0.721,-36.266]],"v":[[46.867,-48.997],[-47.017,-48.997],[-114.395,14.67],[-114.437,21.691],[3.671,36.366],[114.339,21.681],[114.381,14.661]],"c":true}]},{"t":59.9997909698997,"s":[{"i":[[34.646,0],[0,0],[0,-27.281],[0,0],[-90.002,0],[0,0],[0,0]],"o":[[0,0],[-46.659,0],[0,0],[0,0],[81.998,0],[0,0],[-2.376,-40.281]],"v":[[46.851,-77.775],[-47.344,-77.775],[-125.918,-11.43],[-126.056,-12.211],[2.498,36.152],[125.734,-12.242],[125.872,-11.461]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":0.79933110367893,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":0.20066889632107,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.003,265.62,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.24,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[21.249,0],[0,0],[0,-21.251],[0,0],[-21.249,0],[0,0],[0,0]],"o":[[0,0],[-21.249,0],[0,0],[0,21.249],[0,0],[0,0],[0,-21.251]],"v":[[3.192,-52.084],[-3.19,-52.084],[-41.667,-13.607],[-41.667,13.608],[-3.19,52.084],[41.667,52.084],[41.667,-13.607]],"c":true}]},{"t":59.9997909698997,"s":[{"i":[[29.22,0],[0,0],[0,-29.221],[0,0],[-29.22,0],[0,0],[0,0]],"o":[[0,0],[-29.22,0],[0,0],[0,29.22],[0,0],[0,0],[0,-29.221]],"v":[[4.583,-145.62],[-4.192,-145.62],[-57.101,-92.711],[-57.101,-52.287],[-4.192,0.62],[57.49,0.62],[57.49,-92.711]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":0.79933110367893,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".primary.design","cl":"primary design","sr":0.20066889632107,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.24,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[104.17,270.829,0],"to":[-42.5,0,0],"ti":[42.5,0,0]},{"t":59.9999477424749,"s":[-150.83,270.829,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-34.583,0],[0,0]],"o":[[0,0],[0,0],[0,-34.584],[0,0],[0,0]],"v":[[46.875,36.459],[-46.875,36.459],[-46.875,26.042],[15.625,-36.459],[46.875,-36.459]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":3.79933110367893,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".primary.design","cl":"primary design","parent":5,"sr":0.20066889632107,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[46.875,-140.626,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[21.249,0],[0,0],[0,-21.251],[0,0],[-21.249,0],[0,0],[0,0]],"o":[[0,0],[-21.249,0],[0,0],[0,21.249],[0,0],[0,0],[0,-21.251]],"v":[[3.192,-52.084],[-3.19,-52.084],[-41.667,-13.607],[-41.667,13.608],[-3.19,52.084],[41.667,52.084],[41.667,-13.607]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":3.79933110367893,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":".primary.design","cl":"primary design","sr":0.20066889632107,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.24,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[395.838,270.826,0],"to":[41.333,0,0],"ti":[-41.333,0,0]},{"t":59.9998954849498,"s":[643.838,270.826,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[34.583,0],[0,0]],"o":[[0,0],[0,0],[0,-34.584],[0,0],[0,0]],"v":[[-46.875,36.459],[46.875,36.459],[46.875,26.042],[-15.625,-36.459],[-46.875,-36.459]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":7.79933110367893,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":".primary.design","cl":"primary design","parent":7,"sr":0.20066889632107,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-52.084,-140.624,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[21.249,0],[0,0],[0,-21.251],[0,0],[-21.249,0],[0,0],[0,0]],"o":[[0,0],[-21.249,0],[0,0],[0,21.249],[0,0],[0,0],[0,-21.251]],"v":[[3.192,-52.084],[-3.19,-52.084],[-41.667,-13.607],[-41.667,13.608],[-3.19,52.084],[41.667,52.084],[41.667,-13.607]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.070588235294,0.074509803922,0.192156862745,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('96-groups-outline').layer('Color & Stroke Change').effect('Primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":7.79933110367893,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"lordicon.com Outlines","cl":"com","sr":1,"ks":{"o":{"a":0,"k":20,"ix":11,"x":"var $bm_rt;\nvar checkbox = thisComp.layer('02092020').effect('02092020002')('Checkbox');\nif (checkbox == 1) {\n $bm_rt = 20;\n} else {\n $bm_rt = 0;\n}\n;"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.934,481.369,0],"ix":2,"l":2},"a":{"a":0,"k":[79.934,0.369,0],"ix":1,"l":2},"s":{"a":0,"k":[265.159,265.159,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[1.415,0],[11.014,0],[11.014,-2.523],[4.656,-2.523],[4.656,-14.809],[1.415,-14.809]],"c":true},"ix":2},"nm":"l","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"l","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[11.167,-7.199],[12.992,-1.661],[18.243,0.369],[23.514,-1.743],[25.381,-7.548],[23.494,-13.127],[18.284,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[14.49,-7.302],[15.577,-11.609],[18.305,-12.86],[21.689,-10.235],[22.058,-7.589],[21.053,-3.343],[18.284,-1.969],[15.597,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[-0.287,-0.841],[-0.144,-0.82],[0,0],[0.164,0.656],[0.226,1.743],[2.236,0.205],[0,2.769],[0.923,0.8],[1.641,-0.021],[0,0]],"o":[[0,0],[0,0],[0,0],[0.533,0],[0.205,0.574],[0,0],[-0.164,-0.246],[-0.103,-0.41],[-0.267,-1.928],[0.718,-0.205],[0,-0.964],[-1.19,-1.026],[0,0],[0,0]],"v":[[27.381,0],[30.622,0],[30.622,-5.989],[33.411,-5.989],[35.011,-5.148],[35.811,0],[39.318,0],[38.867,-1.067],[38.416,-3.938],[35.749,-7.343],[38.847,-10.973],[37.554,-13.824],[33.063,-14.829],[27.381,-14.829]],"c":true},"ix":2},"nm":"r","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-0.492,-0.349],[0,-1.005],[0.226,-0.164],[0.369,0],[0,0]],"o":[[0,0],[1.005,0],[0.287,0.185],[0,1.046],[-0.513,0.41],[0,0],[0,0]],"v":[[30.519,-12.491],[32.652,-12.491],[34.744,-12.142],[35.524,-10.481],[34.703,-8.758],[33.083,-8.348],[30.519,-8.348]],"c":true},"ix":2},"nm":"r","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"r","np":5,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-0.554,0.103],[0,4.553],[1.866,1.374],[0.82,0],[0,0]],"o":[[0,0],[1.497,0],[2.81,-0.513],[0,-2.113],[-1.784,-1.313],[0,0],[0,0]],"v":[[41.068,0],[45.683,0],[48.349,-0.164],[53.6,-7.609],[51.077,-13.434],[45.97,-14.768],[41.068,-14.788]],"c":true},"ix":2},"nm":"d","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-0.656,-0.185],[0,-2.092],[1.251,-1.251],[1.354,0],[0.349,0.021]],"o":[[1.825,-0.082],[1.99,0.554],[0,0.718],[-0.923,0.923],[-0.369,0],[0,0]],"v":[[44.288,-12.388],[47.611,-12.183],[50.318,-7.609],[48.985,-3.425],[45.539,-2.4],[44.288,-2.441]],"c":true},"ix":2},"nm":"d","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"d","np":5,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[55.669,0],[58.849,0],[58.849,-14.87],[55.669,-14.87]],"c":true},"ix":2},"nm":"i","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"i","np":3,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[3.241,0],[0,-4.697],[-5.107,0],[-1.313,1.354],[-0.062,0.882],[0,0],[1.333,0],[0,0.882],[-2.359,0],[-0.062,-0.513]],"o":[[0,-2.954],[-4.164,0],[0,3.671],[1.354,0],[1.19,-1.231],[0,0],[-0.062,1.969],[-3.097,0],[0,-3.056],[2.154,0],[0,0]],"v":[[73.104,-9.989],[67.587,-14.911],[60.798,-7.097],[67.566,0.349],[71.894,-1.313],[73.227,-4.799],[69.884,-4.799],[67.218,-1.99],[64.121,-7.076],[67.464,-12.593],[69.864,-9.989]],"c":true},"ix":2},"nm":"c","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"c","np":3,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[74.546,-7.199],[76.372,-1.661],[81.622,0.369],[86.894,-1.743],[88.76,-7.548],[86.873,-13.127],[81.663,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[77.869,-7.302],[78.956,-11.609],[81.684,-12.86],[85.068,-10.235],[85.437,-7.589],[84.432,-3.343],[81.663,-1.969],[78.977,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[91.007,0],[94.001,0],[94.001,-12.306],[99.744,0],[104.113,0],[104.113,-14.829],[101.159,-14.829],[101.159,-3.159],[95.601,-14.829],[91.007,-14.829]],"c":true},"ix":2},"nm":"n","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"n","np":3,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[106.893,0],[109.497,0],[109.497,-2.728],[106.893,-2.728]],"c":true},"ix":2},"nm":".","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":".","np":3,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[3.241,0],[0,-4.697],[-5.107,0],[-1.313,1.354],[-0.062,0.882],[0,0],[1.333,0],[0,0.882],[-2.359,0],[-0.062,-0.513]],"o":[[0,-2.954],[-4.164,0],[0,3.671],[1.354,0],[1.19,-1.231],[0,0],[-0.062,1.969],[-3.097,0],[0,-3.056],[2.154,0],[0,0]],"v":[[124.04,-9.989],[118.523,-14.911],[111.734,-7.097],[118.502,0.349],[122.83,-1.313],[124.163,-4.799],[120.82,-4.799],[118.154,-1.99],[115.057,-7.076],[118.4,-12.593],[120.8,-9.989]],"c":true},"ix":2},"nm":"c","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"c","np":3,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.938],[-1.62,-1.723],[-1.949,0],[-1.641,1.846],[0,2.154],[1.579,1.805],[1.579,0]],"o":[[0,1.354],[1.354,1.415],[1.231,0],[1.21,-1.354],[0,-1.456],[-1.456,-1.641],[-5.333,0]],"v":[[125.482,-7.199],[127.308,-1.661],[132.558,0.369],[137.829,-1.743],[139.696,-7.548],[137.809,-13.127],[132.599,-15.137]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.415],[-0.841,1.026],[-1.19,0],[-0.615,-1.825],[0,-0.718],[0.492,-0.738],[1.292,0],[0.451,0.615]],"o":[[0,-1.682],[0.595,-0.759],[1.518,0],[0.308,0.902],[0,2.359],[-0.595,0.923],[-1.477,0],[-0.882,-1.149]],"v":[[128.805,-7.302],[129.892,-11.609],[132.62,-12.86],[136.004,-10.235],[136.373,-7.589],[135.368,-3.343],[132.599,-1.969],[129.912,-3.159]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"o","np":5,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[141.696,0],[144.67,0],[144.67,-12.716],[148.629,0],[151.254,0],[155.295,-12.716],[155.295,0],[158.453,0],[158.453,-14.829],[153.408,-14.829],[150.024,-4.041],[146.885,-14.829],[141.696,-14.829]],"c":true},"ix":2},"nm":"m","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"m","np":3,"cix":2,"bm":0,"ix":12,"mn":"ADBE Vector Group","hd":false}],"ip":2.5,"op":25,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":3,"nm":"02092020","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-105,15,0],"ix":2,"l":2},"a":{"a":0,"k":[60,60,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"02092020002","np":3,"mn":"ADBE Checkbox Control","ix":1,"en":1,"ef":[{"ty":7,"nm":"Checkbox","mn":"ADBE Checkbox Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"Color & Stroke Change","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-55,-102,0],"ix":2,"l":2,"x":"var $bm_rt;\n$bm_rt = effect('Axis')('Point');"},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2,"x":"var $bm_rt;\nvar temp;\ntemp = effect('Scale')('Slider');\n$bm_rt = [\n temp,\n temp\n];"}},"ao":0,"ef":[{"ty":5,"nm":"Primary","np":3,"mn":"ADBE Color Control","ix":1,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]},{"ty":5,"nm":"Axis","np":3,"mn":"ADBE Point Control","ix":2,"en":1,"ef":[{"ty":3,"nm":"Point","mn":"ADBE Point Control-0001","ix":1,"v":{"a":0,"k":[250,250],"ix":1}}]},{"ty":5,"nm":"Scale","np":3,"mn":"ADBE Slider Control","ix":3,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":100,"ix":1}}]},{"ty":5,"nm":"State-Intro","np":3,"mn":"ADBE Slider Control","ix":4,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]},{"ty":5,"nm":"State-Hover","np":3,"mn":"ADBE Slider Control","ix":5,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":1,"ix":1}}]},{"ty":5,"nm":"State-Morph","np":3,"mn":"ADBE Slider Control","ix":6,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"in-groups","parent":3,"refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Intro')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":71,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"hover-groups","parent":3,"refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Hover')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":71,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"morph-group-single","parent":3,"refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11,"x":"var $bm_rt;\n$bm_rt = $bm_mul(thisComp.layer('Color & Stroke Change').effect('State-Morph')('Slider'), 100);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":71,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/frontend/public/lotties/three-ellipsis.json b/frontend/public/lotties/three-ellipsis.json new file mode 100644 index 000000000..692b639e2 --- /dev/null +++ b/frontend/public/lotties/three-ellipsis.json @@ -0,0 +1 @@ +{"v":"5.7.5","fr":100,"ip":0,"op":100,"w":300,"h":300,"nm":"Comp 1","ddd":0,"metadata":{},"assets":[{"id":"0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"ball4","sr":1,"ks":{"p":{"a":0,"k":[-197,-103],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":"ball4","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[26,0],[0,26],[-26,0],[0,-26],[26,0]],"i":[[0,0],[14.359,0],[0,14.359],[-14.359,0],[0,-14.359]],"o":[[0,14.359],[-14.359,0],[0,-14.359],[14.359,0],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":1,"k":[{"t":27,"s":[223,169.5],"i":{"x":[0.465],"y":[1]},"o":{"x":[0.534],"y":[0]}},{"t":52,"s":[223,129.5],"i":{"x":[0.424],"y":[1]},"o":{"x":[0.514],"y":[0]}},{"t":77,"s":[223,169.5],"i":{"x":[0.465],"y":[1]},"o":{"x":[0.054],"y":[0]}},{"t":102,"s":[223,129.5],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":101,"st":0,"bm":0}]},{"id":"1","layers":[{"ddd":0,"ind":2,"ty":4,"nm":"ball 4","sr":1,"ks":{"p":{"a":0,"k":[-124,-103],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":"ball 4","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[26,0],[0,26],[-26,0],[0,-26],[26,0]],"i":[[0,0],[14.359,0],[0,14.359],[-14.359,0],[0,-14.359]],"o":[[0,14.359],[-14.359,0],[0,-14.359],[14.359,0],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":1,"k":[{"t":18,"s":[150,169.5],"i":{"x":[0.465],"y":[1]},"o":{"x":[0.534],"y":[0]}},{"t":43,"s":[150,129.5],"i":{"x":[0.424],"y":[1]},"o":{"x":[0.514],"y":[0]}},{"t":68,"s":[150,169.5],"i":{"x":[0.465],"y":[1]},"o":{"x":[0.054],"y":[0]}},{"t":93,"s":[150,129.5],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":101,"st":0,"bm":0}]},{"id":"2","layers":[{"ddd":0,"ind":3,"ty":4,"nm":"ball 3","sr":1,"ks":{"p":{"a":0,"k":[-51,-103],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":"ball 3","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[26,0],[0,26],[-26,0],[0,-26],[26,0]],"i":[[0,0],[14.359,0],[0,14.359],[-14.359,0],[0,-14.359]],"o":[[0,14.359],[-14.359,0],[0,-14.359],[14.359,0],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":1,"k":[{"t":10,"s":[77,169.5],"i":{"x":[0.465],"y":[1]},"o":{"x":[0.534],"y":[0]}},{"t":35,"s":[77,129.5],"i":{"x":[0.424],"y":[1]},"o":{"x":[0.514],"y":[0]}},{"t":60,"s":[77,169.5],"i":{"x":[0.465],"y":[1]},"o":{"x":[0.054],"y":[0]}},{"t":85,"s":[77,129.5],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":101,"st":0,"bm":0}]},{"id":"3","layers":[{"ddd":0,"ind":4,"ty":4,"nm":"ball3","sr":1,"ks":{"p":{"a":0,"k":[-197,-103],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":"ball3","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[26,0],[0,26],[-26,0],[0,-26],[26,0]],"i":[[0,0],[14.359,0],[0,14.359],[-14.359,0],[0,-14.359]],"o":[[0,14.359],[-14.359,0],[0,-14.359],[14.359,0],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":1,"k":[{"t":-25,"s":[223,169.5],"i":{"x":[0.465],"y":[1]},"o":{"x":[0.534],"y":[0]}},{"t":0,"s":[223,129.5],"i":{"x":[0.424],"y":[1]},"o":{"x":[0.514],"y":[0]}},{"t":25,"s":[223,169.5],"i":{"x":[0.465],"y":[1]},"o":{"x":[0.054],"y":[0]}},{"t":50,"s":[223,129.5],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":101,"st":0,"bm":0}]},{"id":"4","layers":[{"ddd":0,"ind":5,"ty":4,"nm":"ball 2","sr":1,"ks":{"p":{"a":0,"k":[-124,-103],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":"ball 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[26,0],[0,26],[-26,0],[0,-26],[26,0]],"i":[[0,0],[14.359,0],[0,14.359],[-14.359,0],[0,-14.359]],"o":[[0,14.359],[-14.359,0],[0,-14.359],[14.359,0],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":1,"k":[{"t":-33,"s":[150,169.5],"i":{"x":[0.465],"y":[1]},"o":{"x":[0.534],"y":[0]}},{"t":-8,"s":[150,129.5],"i":{"x":[0.424],"y":[1]},"o":{"x":[0.514],"y":[0]}},{"t":17,"s":[150,169.5],"i":{"x":[0.465],"y":[1]},"o":{"x":[0.054],"y":[0]}},{"t":42,"s":[150,129.5],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":101,"st":0,"bm":0}]},{"id":"5","layers":[{"ddd":0,"ind":6,"ty":4,"nm":"ball 1","sr":1,"ks":{"p":{"a":0,"k":[-51,-103],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":"ball 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[26,0],[0,26],[-26,0],[0,-26],[26,0]],"i":[[0,0],[14.359,0],[0,14.359],[-14.359,0],[0,-14.359]],"o":[[0,14.359],[-14.359,0],[0,-14.359],[14.359,0],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":1,"k":[{"t":-42,"s":[77,169.5],"i":{"x":[0.465],"y":[1]},"o":{"x":[0.534],"y":[0]}},{"t":-17,"s":[77,129.5],"i":{"x":[0.424],"y":[1]},"o":{"x":[0.514],"y":[0]}},{"t":8,"s":[77,169.5],"i":{"x":[0.465],"y":[1]},"o":{"x":[0.054],"y":[0]}},{"t":33,"s":[77,129.5],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":101,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":12345679,"ty":4,"nm":"Group Layer 8","sr":1,"ks":{"p":{"a":0,"k":[219,278.6311475409836,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[30.737704918032787,30.737704918032787,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[220.741,37.184],[225.501,35.896],[228.749,32.36800000000001],[229.981,27.216000000000008],[228.749,22.12],[225.501,18.592],[220.741,17.304],[215.981,18.592],[212.677,22.12],[211.501,27.216000000000008],[212.677,32.36800000000001],[215.981,35.896],[220.741,37.184],[220.741,37.184],[220.741,37.184]],"i":[[0,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.382000000000062,0.8586999999999989],[1.79200000000003,0],[1.418999999999983,-0.8586999999999989],[0.8220000000000027,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.382000000000062,-0.8586999999999989],[0.8220000000000027,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.418999999999983,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[221.357,43.06400000000001],[214.917,41.608],[210.49300000000005,37.408],[211.221,36.232],[211.221,42.392],[205.173,42.392],[205.173,0],[211.501,0],[211.501,18.36800000000001],[210.49300000000005,16.912000000000006],[214.973,12.88],[221.357,11.424000000000007],[229.085,13.49600000000001],[234.51700000000005,19.152],[236.533,27.216000000000008],[234.51700000000005,35.28],[229.141,40.992],[221.357,43.06400000000001],[221.357,43.06400000000001],[221.357,43.06400000000001]],"i":[[0,0],[1.942000000000007,0.9706999999999937],[1.045999999999935,1.829300000000003],[-0.2426666666666506,0.3919999999999959],[0,-2.053333333333327],[2.015999999999963,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.122666666666674],[0.3360000000000127,0.4853333333333296],[-1.865999999999985,0.9707000000000079],[-2.38900000000001,0],[-2.277000000000044,-1.3813000000000102],[-1.30600000000004,-2.389300000000006],[0,-2.986699999999999],[1.343999999999937,-2.389300000000006],[2.27800000000002,-1.4187000000000012],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.351999999999975,0],[-1.903999999999996,-0.9707000000000079],[0.2426666666666506,-0.3919999999999959],[0,2.053333333333327],[-2.015999999999963,0],[0,-14.13066666666667],[2.109333333333325,0],[0,6.122666666666667],[-0.3360000000000127,-0.4853333333333296],[1.120000000000005,-1.717300000000009],[1.867000000000075,-0.9706999999999937],[2.875,0],[2.314999999999941,1.381299999999996],[1.343999999999937,2.389300000000006],[0,2.986699999999999],[-1.30600000000004,2.389300000000006],[-2.27699999999993,1.381299999999996],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[181.87,43.06400000000001],[176.438,42],[172.854,38.976],[171.566,34.384],[172.63,29.960000000000008],[176.046,26.656000000000006],[181.814,24.752],[192.342,23.016000000000005],[192.342,28],[183.046,29.624],[179.35,31.248],[178.174,34.16],[179.462,37.016000000000005],[182.878,38.08],[187.35799999999995,36.96000000000001],[190.38199999999995,33.992],[191.446,29.792],[191.446,22.008],[189.766,18.36800000000001],[185.398,16.912000000000006],[180.974,18.256],[178.23,21.616],[172.966,18.98400000000001],[175.71,15.064000000000007],[180.134,12.376],[185.566,11.424000000000007],[191.894,12.768],[196.206,16.52],[197.774,22.008],[197.774,42.392],[191.726,42.392],[191.726,36.904],[193.014,37.072],[190.27,40.264],[186.518,42.336],[181.87,43.06400000000001],[181.87,43.06400000000001],[181.87,43.06400000000001]],"i":[[0,0],[1.567999999999984,0.7092999999999989],[0.8589999999999236,1.2693000000000012],[0,1.7547],[-0.7089999999999463,1.306699999999992],[-1.530000000000086,0.8960000000000008],[-2.313999999999965,0.3733000000000004],[-3.509333333333302,0.5786666666666633],[0,-1.661333333333332],[3.098666666666645,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8579999999999472,-0.7467000000000041],[-1.381000000000085,0],[-1.268999999999892,0.7466999999999899],[-0.7089999999999463,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829999999999927,0],[1.269999999999982,-0.8960000000000008],[0.5979999999999563,-1.381299999999996],[1.754666666666708,0.8773333333333255],[-1.269000000000005,1.11999999999999],[-1.680000000000064,0.6346999999999952],[-1.903999999999996,0],[-1.828999999999951,-0.8960000000000008],[-1.008000000000038,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999963,0],[0,1.829333333333338],[-0.4293333333333749,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.717999999999961,0],[0,0],[0,0]],"o":[[-2.052999999999997,0],[-1.529999999999973,-0.7467000000000041],[-0.8580000000000609,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999573,-1.306700000000006],[1.530999999999949,-0.8960000000000008],[3.509333333333302,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666645,0.541333333333327],[-1.680000000000064,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.8959999999999582,0.7092999999999989],[1.717999999999961,0],[1.307000000000016,-0.7467000000000041],[0.7100000000000364,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.081999999999994,-0.9707000000000079],[-1.680000000000064,0],[-1.232000000000085,0.8586999999999989],[-1.754666666666708,-0.8773333333333255],[0.5599999999999454,-1.493300000000005],[1.269999999999982,-1.157300000000006],[1.717999999999961,-0.6347000000000094],[2.389999999999986,0],[1.866999999999962,0.8960000000000008],[1.045999999999935,1.568000000000012],[0,6.794666666666672],[-2.015999999999963,0],[0,-1.829333333333338],[0.4293333333333749,0.05599999999999739],[-0.70900000000006,1.2319999999999993],[-1.081999999999994,0.8960000000000008],[-1.380999999999972,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[159.072,42.392],[159.072,0],[165.4,0],[165.4,42.392],[159.072,42.392],[159.072,42.392],[159.072,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-14.13066666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,14.13066666666667],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[138.952,43.06400000000001],[130.888,40.992],[125.456,35.28],[123.496,27.16],[125.456,19.040000000000006],[130.832,13.49600000000001],[138.448,11.424000000000007],[144.552,12.600000000000009],[149.088,15.848],[151.888,20.49600000000001],[152.896,26.096],[152.84,27.608],[152.616,29.064000000000007],[128.48,29.064000000000007],[128.48,24.024],[149.032,24.024],[146.008,26.320000000000007],[145.616,21.448000000000008],[142.816,18.032],[138.448,16.744],[133.968,18.032],[130.944,21.616],[130.104,27.216000000000008],[130.944,32.592],[134.192,36.176],[139.008,37.464],[143.65599999999995,36.232],[146.736,33.040000000000006],[151.888,35.56],[149.088,39.42400000000001],[144.60799999999995,42.11200000000001],[138.952,43.06400000000001],[138.952,43.06400000000001],[138.952,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.307000000000016,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.836999999999989,0],[-1.79200000000003,-0.784000000000006],[-1.231999999999971,-1.381299999999996],[-0.6349999999999909,-1.754700000000014],[0,-1.978700000000003],[0.03699999999992087,-0.5227000000000004],[0.1119999999999663,-0.4480000000000075],[8.04533333333336,0],[0,1.680000000000007],[-6.850666666666712,0],[1.008000000000038,-0.7653333333333308],[0.6349999999999909,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.680000000000064,0],[1.307000000000016,-0.8586999999999989],[0.70900000000006,-1.567999999999998],[-0.1490000000000009,-2.202700000000007],[-0.7469999999999573,-1.530699999999996],[-1.380999999999972,-0.8586999999999989],[-1.79200000000003,0],[-1.268999999999892,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333386,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.755000000000109,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.315000000000055,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999937,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.277000000000044,0],[1.79200000000003,0.7839999999999918],[1.232000000000085,1.344000000000008],[0.6720000000000255,1.7547],[0,0.4852999999999952],[-0.03700000000003456,0.5227000000000004],[-8.04533333333336,0],[0,-1.680000000000007],[6.850666666666712,0],[-1.008000000000038,0.7653333333333308],[0.3729999999999336,-1.829300000000003],[-0.59699999999998,-1.456000000000003],[-1.232000000000085,-0.8586999999999989],[-1.67999999999995,0],[-1.306999999999903,0.8213000000000079],[-0.7089999999999463,1.530699999999996],[-0.1870000000000118,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418999999999983,0.8586999999999989],[1.828999999999951,0],[1.307000000000016,-0.8212999999999937],[1.717333333333386,0.8400000000000034],[-0.59699999999998,1.4187000000000012],[-1.231999999999971,1.11999999999999],[-1.716999999999985,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,7.951999999999998],[111.001,0.6720000000000041],[117.329,0.6720000000000041],[117.329,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,42.392],[111.001,12.096],[117.329,12.096],[117.329,42.392],[111.001,42.392],[111.001,42.392],[111.001,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[101.41,42.72800000000001],[94.01800000000003,40.040000000000006],[91.38599999999997,32.48],[91.38599999999997,17.808000000000007],[86.06600000000003,17.808000000000007],[86.06600000000003,12.096],[86.90599999999995,12.096],[90.21000000000004,10.864],[91.38599999999997,7.504000000000005],[91.38599999999997,5.152000000000001],[97.71400000000006,5.152000000000001],[97.71400000000006,12.096],[104.602,12.096],[104.602,17.808000000000007],[97.71400000000006,17.808000000000007],[97.71400000000006,32.2],[98.21799999999996,34.888000000000005],[99.84199999999998,36.568],[102.754,37.128],[103.76200000000006,37.072],[104.826,36.96000000000001],[104.826,42.392],[103.09,42.616],[101.41,42.72800000000001],[101.41,42.72800000000001],[101.41,42.72800000000001]],"i":[[0,0],[1.754999999999995,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333312,0],[0,1.903999999999996],[-0.2799999999999727,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999935,0],[0,-1.903999999999996],[2.295999999999935,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7469999999999573,-0.4106999999999914],[-1.19500000000005,0],[-0.3730000000000473,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6349999999999909,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333312,0],[0,-1.903999999999996],[0.2799999999999727,0],[1.419000000000096,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999935,0],[0,1.903999999999996],[-2.295999999999935,0],[0,4.797333333333327],[0,1.045299999999997],[0.3360000000000127,0.7092999999999989],[0.7470000000000709,0.3733000000000004],[0.2989999999999782,0],[0.3729999999999336,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[78.71499999999997,42.72800000000001],[71.32299999999998,40.040000000000006],[68.69100000000003,32.48],[68.69100000000003,17.808000000000007],[63.37099999999998,17.808000000000007],[63.37099999999998,12.096],[64.21100000000001,12.096],[67.51499999999999,10.864],[68.69100000000003,7.504000000000005],[68.69100000000003,5.152000000000001],[75.019,5.152000000000001],[75.019,12.096],[81.90700000000004,12.096],[81.90700000000004,17.808000000000007],[75.019,17.808000000000007],[75.019,32.2],[75.52300000000002,34.888000000000005],[77.14699999999999,36.568],[80.05900000000003,37.128],[81.06700000000001,37.072],[82.13099999999997,36.96000000000001],[82.13099999999997,42.392],[80.39499999999998,42.616],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001]],"i":[[0,0],[1.754000000000019,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7470000000000141,-0.4106999999999914],[-1.19500000000005,0],[-0.3740000000000236,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6340000000000146,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173999999999978,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418000000000006,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7460000000000377,0.3733000000000004],[0.297999999999945,0],[0.3730000000000473,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,37.184],[48.94799999999998,35.896],[52.19600000000003,32.36800000000001],[53.428,27.216000000000008],[52.19600000000003,22.12],[48.94799999999998,18.592],[44.18799999999999,17.304],[39.428,18.592],[36.12400000000002,22.12],[34.94799999999998,27.216000000000008],[36.12400000000002,32.36800000000001],[39.428,35.896],[44.18799999999999,37.184],[44.18799999999999,37.184],[44.18799999999999,37.184]],"i":[[0,0],[-1.381999999999948,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.381000000000029,0.8586999999999989],[1.79200000000003,0],[1.418000000000006,-0.8586999999999989],[0.8209999999999695,-1.493300000000005],[0,-1.904000000000011],[-0.7840000000000487,-1.5307000000000102],[-1.382000000000005,-0.8586999999999989],[-1.754999999999995,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.381000000000029,-0.8586999999999989],[0.8209999999999695,-1.5307000000000102],[0,-1.904000000000011],[-0.7840000000000487,-1.493300000000005],[-1.381999999999948,-0.8586999999999989],[-1.754999999999995,0],[-1.382000000000005,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.418000000000006,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,43.06400000000001],[36.18000000000001,40.992],[30.46800000000002,35.336],[28.33999999999997,27.216000000000008],[30.46800000000002,19.096],[36.18000000000001,13.49600000000001],[44.18799999999999,11.424000000000007],[52.19600000000003,13.49600000000001],[57.85199999999998,19.096],[59.98000000000002,27.216000000000008],[57.85199999999998,35.392],[52.139999999999986,41.048],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001]],"i":[[0,0],[2.425999999999988,1.381299999999996],[1.418000000000006,2.389300000000006],[0,3.024000000000001],[-1.41900000000004,2.352000000000004],[-2.389999999999986,1.343999999999994],[-2.949999999999989,0],[-2.352000000000032,-1.3813000000000102],[-1.381999999999948,-2.389300000000006],[0,-3.061300000000003],[1.418000000000006,-2.389299999999992],[2.38900000000001,-1.381299999999996],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.911999999999978,0],[-2.389999999999986,-1.381299999999996],[-1.41900000000004,-2.389299999999992],[0,-3.061300000000003],[1.418000000000006,-2.389300000000006],[2.38900000000001,-1.3813000000000102],[2.98599999999999,0],[2.388999999999953,1.343999999999994],[1.418000000000006,2.352000000000004],[0,3.061299999999989],[-1.418999999999983,2.389300000000006],[-2.389999999999986,1.343999999999994],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.608000000000004,0.6720000000000041],[6.608000000000004,36.512],[24.639999999999986,36.512],[24.639999999999986,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.202666666666687,0],[0,-11.94666666666666],[-6.01066666666668,0],[0,-1.959999999999994],[8.21333333333331,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.202666666666687,0],[0,11.94666666666667],[6.01066666666668,0],[0,1.959999999999994],[-8.21333333333331,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[98.08047485351562,-21.67217254638672],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[246.681,42.392],[246.681,0],[253.009,0],[253.009,18.032],[252.001,17.248],[255.585,12.936000000000007],[261.297,11.424000000000007],[267.23299999999995,12.88],[271.265,16.912000000000006],[272.721,22.792],[272.721,42.392],[266.449,42.392],[266.449,24.528000000000006],[265.553,20.664],[263.201,18.2],[259.729,17.304],[256.25699999999995,18.2],[253.849,20.664],[253.009,24.528000000000006],[253.009,42.392],[246.681,42.392],[246.681,42.392],[246.681,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.010666666666665],[0.3360000000000127,0.2613333333333259],[-1.643000000000029,0.9706999999999937],[-2.166000000000054,0],[-1.717999999999961,-0.9706999999999937],[-0.9710000000000036,-1.717300000000009],[0,-2.202699999999993],[0,-6.533333333333331],[2.090666666666721,0],[0,5.954666666666668],[0.59699999999998,1.045299999999997],[1.007999999999925,0.5600000000000023],[1.305999999999926,0],[1.045000000000073,-0.5973000000000042],[0.59699999999998,-1.082700000000003],[0,-1.493300000000005],[0,-5.954666666666668],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,6.010666666666665],[-0.3360000000000127,-0.2613333333333259],[0.7459999999999809,-1.903999999999996],[1.641999999999967,-1.0080000000000098],[2.240000000000009,0],[1.717000000000098,0.9707000000000079],[0.9700000000000273,1.717299999999994],[0,6.533333333333331],[-2.090666666666721,0],[0,-5.954666666666668],[0,-1.5307000000000102],[-0.5599999999999454,-1.082700000000003],[-1.008000000000038,-0.5973000000000042],[-1.270000000000095,0],[-1.007999999999953,0.5600000000000023],[-0.5600000000000023,1.082700000000003],[0,5.954666666666668],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[237.089,42.72800000000001],[229.697,40.040000000000006],[227.065,32.48],[227.065,17.808000000000007],[221.745,17.808000000000007],[221.745,12.096],[222.585,12.096],[225.889,10.864],[227.065,7.504000000000005],[227.065,5.152000000000001],[233.393,5.152000000000001],[233.393,12.096],[240.281,12.096],[240.281,17.808000000000007],[233.393,17.808000000000007],[233.393,32.2],[233.897,34.888000000000005],[235.521,36.568],[238.433,37.128],[239.441,37.072],[240.505,36.96000000000001],[240.505,42.392],[238.769,42.616],[237.089,42.72800000000001],[237.089,42.72800000000001],[237.089,42.72800000000001]],"i":[[0,0],[1.755000000000052,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7459999999999809,-0.4106999999999914],[-1.194000000000017,0],[-0.3729999999999905,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6350000000000477,-0.07469999999999288],[0.48599999999999,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.753999999999962,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418999999999983,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7470000000000141,0.3733000000000004],[0.2989999999999782,0],[0.3740000000000236,-0.03730000000000189],[0,1.810666666666663],[-0.5220000000000482,0.0747000000000071],[-0.6339999999999577,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,7.951999999999998],[210.259,0.6720000000000041],[216.587,0.6720000000000041],[216.587,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,42.392],[210.259,12.096],[216.587,12.096],[216.587,42.392],[210.259,42.392],[210.259,42.392],[210.259,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[169.688,42.392],[159.272,12.096],[165.992,12.096],[173.944,36.232],[171.592,36.232],[179.712,12.096],[185.48,12.096],[193.544,36.232],[191.192,36.232],[199.2,12.096],[205.92,12.096],[195.448,42.392],[189.736,42.392],[181.56,17.696],[183.632,17.696],[175.456,42.392],[169.688,42.392],[169.688,42.392],[169.688,42.392]],"i":[[0,0],[3.47199999999998,10.09866666666667],[-2.240000000000009,0],[-2.650666666666666,-8.045333333333332],[0.7839999999999918,0],[-2.706666666666649,8.045333333333332],[-1.922666666666657,0],[-2.687999999999988,-8.045333333333332],[0.7839999999999918,0],[-2.669333333333327,8.045333333333332],[-2.240000000000009,0],[3.490666666666641,-10.09866666666667],[1.903999999999996,0],[2.725333333333367,8.232],[-0.6906666666666865,0],[2.72533333333331,-8.232],[1.922666666666657,0],[0,0],[0,0]],"o":[[-3.47199999999998,-10.09866666666666],[2.240000000000009,0],[2.650666666666666,8.045333333333332],[-0.7839999999999918,0],[2.706666666666649,-8.045333333333332],[1.922666666666657,0],[2.687999999999988,8.045333333333332],[-0.7839999999999918,0],[2.669333333333327,-8.045333333333332],[2.240000000000009,0],[-3.490666666666641,10.09866666666666],[-1.903999999999996,0],[-2.725333333333367,-8.232],[0.6906666666666865,0],[-2.72533333333331,8.232],[-1.922666666666657,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[155.86146545410156,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[132.444,43.06400000000001],[124.38,40.992],[118.948,35.28],[116.988,27.16],[118.948,19.040000000000006],[124.324,13.49600000000001],[131.94,11.424000000000007],[138.044,12.600000000000009],[142.58,15.848],[145.38,20.49600000000001],[146.388,26.096],[146.332,27.608],[146.108,29.064000000000007],[121.972,29.064000000000007],[121.972,24.024],[142.524,24.024],[139.5,26.320000000000007],[139.108,21.448000000000008],[136.308,18.032],[131.94,16.744],[127.46,18.032],[124.436,21.616],[123.596,27.216000000000008],[124.436,32.592],[127.684,36.176],[132.5,37.464],[137.148,36.232],[140.228,33.040000000000006],[145.38,35.56],[142.58,39.42400000000001],[138.1,42.11200000000001],[132.444,43.06400000000001],[132.444,43.06400000000001],[132.444,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.305999999999983,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.838000000000022,0],[-1.79200000000003,-0.784000000000006],[-1.232000000000028,-1.381299999999996],[-0.6350000000000477,-1.754700000000014],[0,-1.978700000000003],[0.03699999999997772,-0.5227000000000004],[0.1120000000000232,-0.4480000000000075],[8.045333333333303,0],[0,1.680000000000007],[-6.850666666666655,0],[1.007999999999981,-0.7653333333333308],[0.6340000000000146,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.67999999999995,0],[1.305999999999983,-0.8586999999999989],[0.7090000000000032,-1.567999999999998],[-0.1499999999999773,-2.202700000000007],[-0.7470000000000141,-1.530699999999996],[-1.382000000000005,-0.8586999999999989],[-1.791999999999973,0],[-1.269999999999982,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333329,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.754000000000019,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.314999999999998,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999994,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.276999999999987,0],[1.791999999999973,0.7839999999999918],[1.231999999999971,1.344000000000008],[0.6719999999999686,1.7547],[0,0.4852999999999952],[-0.03800000000001091,0.5227000000000004],[-8.045333333333303,0],[0,-1.680000000000007],[6.850666666666655,0],[-1.007999999999981,0.7653333333333308],[0.3730000000000473,-1.829300000000003],[-0.5979999999999563,-1.456000000000003],[-1.232000000000028,-0.8586999999999989],[-1.680000000000007,0],[-1.307000000000016,0.8213000000000079],[-0.7100000000000364,1.530699999999996],[-0.186999999999955,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418000000000006,0.8586999999999989],[1.829000000000008,0],[1.305999999999983,-0.8212999999999937],[1.717333333333329,0.8400000000000034],[-0.5980000000000132,1.4187000000000012],[-1.232000000000028,1.11999999999999],[-1.718000000000018,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[95.32,37.184],[100.024,35.896],[103.328,32.36800000000001],[104.56,27.216000000000008],[103.328,22.12],[100.024,18.592],[95.32,17.304],[90.56,18.592],[87.256,22.12],[86.08000000000001,27.216000000000008],[87.256,32.36800000000001],[90.50399999999999,35.896],[95.32,37.184],[95.32,37.184],[95.32,37.184]],"i":[[0,0],[-1.381,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.820999999999998,1.493299999999991],[1.419000000000011,0.8586999999999989],[1.754999999999995,0],[1.418999999999983,-0.8586999999999989],[0.7839999999999918,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.381,-0.8586999999999989],[-1.792000000000002,0],[0,0],[0,0]],"o":[[1.754999999999995,0],[1.419000000000011,-0.8586999999999989],[0.820999999999998,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.381,-0.8586999999999989],[-1.754999999999995,0],[-1.419000000000011,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.7839999999999918,1.493299999999991],[1.419000000000011,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[94.70400000000001,43.06400000000001],[86.864,40.992],[81.43199999999999,35.28],[79.47200000000001,27.216000000000008],[81.488,19.152],[86.91999999999999,13.49600000000001],[94.648,11.424000000000007],[101.088,12.88],[105.512,16.912000000000006],[104.56,18.36800000000001],[104.56,0],[110.832,0],[110.832,42.392],[104.84,42.392],[104.84,36.232],[105.568,37.408],[101.088,41.608],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001]],"i":[[0,0],[2.314999999999998,1.381299999999996],[1.344000000000023,2.389300000000006],[0,2.986699999999999],[-1.343999999999994,2.389300000000006],[-2.276999999999987,1.381299999999996],[-2.875,0],[-1.8669999999999902,-0.9706999999999937],[-1.082999999999998,-1.717300000000009],[0.3173333333333233,-0.4853333333333296],[0,6.122666666666674],[-2.090666666666664,0],[0,-14.13066666666667],[1.99733333333333,0],[0,2.053333333333327],[-0.242666666666679,-0.3919999999999959],[1.941000000000003,-0.9707000000000079],[2.314999999999998,0],[0,0],[0,0]],"o":[[-2.912000000000006,0],[-2.277000000000015,-1.4187000000000012],[-1.306999999999988,-2.389300000000006],[0,-2.986699999999999],[1.343999999999994,-2.389300000000006],[2.277000000000015,-1.3813000000000102],[2.426999999999992,0],[1.867000000000019,0.9707000000000079],[-0.3173333333333233,0.4853333333333296],[0,-6.122666666666674],[2.090666666666664,0],[0,14.13066666666667],[-1.99733333333333,0],[0,-2.053333333333327],[0.242666666666679,0.3919999999999959],[-1.045000000000016,1.829300000000003],[-1.941000000000003,0.9706999999999937],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[57.40100000000001,43.06400000000001],[51.968999999999994,42],[48.38499999999999,38.976],[47.09700000000001,34.384],[48.161,29.960000000000008],[51.577,26.656000000000006],[57.345,24.752],[67.87299999999999,23.016000000000005],[67.87299999999999,28],[58.577,29.624],[54.881,31.248],[53.70500000000001,34.16],[54.992999999999995,37.016000000000005],[58.40899999999999,38.08],[62.88900000000001,36.96000000000001],[65.91300000000001,33.992],[66.977,29.792],[66.977,22.008],[65.297,18.36800000000001],[60.929,16.912000000000006],[56.505,18.256],[53.761,21.616],[48.496999999999986,18.98400000000001],[51.240999999999985,15.064000000000007],[55.66499999999999,12.376],[61.09700000000001,11.424000000000007],[67.42500000000001,12.768],[71.737,16.52],[73.305,22.008],[73.305,42.392],[67.257,42.392],[67.257,36.904],[68.54499999999999,37.072],[65.80099999999999,40.264],[62.04900000000001,42.336],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001]],"i":[[0,0],[1.568000000000012,0.7092999999999989],[0.8590000000000089,1.2693000000000012],[0,1.7547],[-0.7090000000000032,1.306699999999992],[-1.531000000000006,0.8960000000000008],[-2.314999999999998,0.3733000000000004],[-3.509333333333331,0.5786666666666633],[0,-1.661333333333332],[3.098666666666674,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8590000000000089,-0.7467000000000041],[-1.381,0],[-1.269000000000005,0.7466999999999899],[-0.7090000000000032,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829000000000008,0],[1.269000000000005,-0.8960000000000008],[0.5970000000000084,-1.381299999999996],[1.754666666666679,0.8773333333333255],[-1.268999999999977,1.11999999999999],[-1.680000000000007,0.6346999999999952],[-1.903999999999996,0],[-1.829000000000008,-0.8960000000000008],[-1.0080000000000098,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999991,0],[0,1.829333333333338],[-0.429333333333318,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.716999999999985,0],[0,0],[0,0]],"o":[[-2.053000000000026,0],[-1.531000000000006,-0.7467000000000041],[-0.8589999999999804,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999857,-1.306700000000006],[1.531000000000006,-0.8960000000000008],[3.509333333333331,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666674,0.541333333333327],[-1.680000000000007,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.896000000000015,0.7092999999999989],[1.717000000000013,0],[1.306999999999988,-0.7467000000000041],[0.7089999999999748,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.082999999999998,-0.9707000000000079],[-1.680000000000007,0],[-1.2319999999999993,0.8586999999999989],[-1.754666666666679,-0.8773333333333255],[0.5600000000000023,-1.493300000000005],[1.269000000000005,-1.157300000000006],[1.717000000000013,-0.6347000000000094],[2.388999999999982,0],[1.86699999999999,0.8960000000000008],[1.045000000000016,1.568000000000012],[0,6.794666666666672],[-2.015999999999991,0],[0,-1.829333333333338],[0.429333333333318,0.05599999999999739],[-0.7089999999999748,1.2319999999999993],[-1.082999999999998,0.8960000000000008],[-1.381,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.159999999999997,0.6720000000000041],[21.84,22.400000000000006],[18.75999999999999,22.400000000000006],[34.16,0.6720000000000041],[40.31999999999999,0.6720000000000041],[40.31999999999999,42.392],[33.768,42.392],[33.768,8.456000000000003],[36.232,9.128],[20.49600000000001,30.632000000000005],[19.824000000000012,30.632000000000005],[4.424000000000007,9.128],[6.608000000000004,8.456000000000003],[6.608000000000004,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.053333333333342,0],[-5.226666666666659,-7.242666666666665],[1.026666666666671,0],[-5.133333333333326,7.242666666666672],[-2.053333333333342,0],[0,-13.90666666666667],[2.183999999999997,0],[0,11.312],[-0.8213333333333424,-0.2240000000000038],[5.245333333333321,-7.168000000000006],[0.2239999999999895,0],[5.133333333333326,7.168000000000006],[-0.7280000000000086,0.2240000000000038],[0,-11.312],[2.202666666666659,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.053333333333342,0],[5.226666666666659,7.242666666666672],[-1.026666666666671,0],[5.133333333333326,-7.242666666666665],[2.053333333333342,0],[0,13.90666666666666],[-2.183999999999997,0],[0,-11.312],[0.8213333333333424,0.2240000000000038],[-5.245333333333321,7.168000000000006],[-0.2239999999999895,0],[-5.133333333333326,-7.168000000000006],[0.7280000000000086,-0.2240000000000038],[0,11.312],[-2.202666666666659,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[2.91259765625,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[702.6863719370097,144],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":72,"ix":2}},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[56.54167175292969,-0.000022762338630855083],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":80,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[122.0000003294881,25.00000012138912],"ix":2},"a":{"a":0,"k":[56.54167175292969,-0.00002288818359375],"ix":2},"s":{"a":0,"k":[34.403572049765366,34.403572049765366],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":101,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[-29.82349395751953,-25.66876983642578],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[119.882333278656,119.882333278656],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":101,"st":0,"bm":0},{"ddd":0,"refId":"0","w":52,"h":93,"ind":8,"ty":0,"nm":"ball4 (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[197,103],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":52,"s":[100],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":101,"st":0,"bm":0,"parent":7},{"ddd":0,"ind":9,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[-29.82349395751953,-25.66876983642578],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[119.882333278656,119.882333278656],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":101,"st":0,"bm":0},{"ddd":0,"refId":"1","w":52,"h":93,"ind":10,"ty":0,"nm":"ball 4 (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[124,103],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":43,"s":[100],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":101,"st":0,"bm":0,"parent":9},{"ddd":0,"ind":11,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[-29.82349395751953,-25.66876983642578],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[119.882333278656,119.882333278656],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":101,"st":0,"bm":0},{"ddd":0,"refId":"2","w":52,"h":93,"ind":12,"ty":0,"nm":"ball 3 (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[51,103],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":35,"s":[100],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":101,"st":0,"bm":0,"parent":11},{"ddd":0,"ind":13,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[-29.82349395751953,-25.66876983642578],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[119.882333278656,119.882333278656],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":101,"st":0,"bm":0},{"ddd":0,"refId":"3","w":52,"h":93,"ind":14,"ty":0,"nm":"ball3 (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[197,103],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"h":1},{"t":52,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":101,"st":0,"bm":0,"parent":13},{"ddd":0,"ind":15,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[-29.82349395751953,-25.66876983642578],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[119.882333278656,119.882333278656],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":101,"st":0,"bm":0},{"ddd":0,"refId":"4","w":52,"h":93,"ind":16,"ty":0,"nm":"ball 2 (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[124,103],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":-8,"s":[100],"h":1},{"t":43,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":101,"st":0,"bm":0,"parent":15},{"ddd":0,"ind":17,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[-29.82349395751953,-25.66876983642578],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[119.882333278656,119.882333278656],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":101,"st":0,"bm":0},{"ddd":0,"refId":"5","w":52,"h":93,"ind":18,"ty":0,"nm":"ball 1 (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[51,103],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":-17,"s":[100],"h":1},{"t":35,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":101,"st":0,"bm":0,"parent":17}],"markers":[]} \ No newline at end of file diff --git a/frontend/public/lotties/toggle-settings.json b/frontend/public/lotties/toggle-settings.json new file mode 100644 index 000000000..38e64147f --- /dev/null +++ b/frontend/public/lotties/toggle-settings.json @@ -0,0 +1 @@ +{"v":"5.7.5","fr":100,"ip":0,"op":52,"w":500,"h":500,"nm":"Comp 1","ddd":0,"metadata":{},"assets":[{"id":"0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":53,"st":0,"bm":0},{"ddd":0,"refId":"1","w":376,"h":377,"ind":2,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[62,62],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":48,"s":[100],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":53,"st":0,"bm":0,"parent":1},{"ddd":0,"ind":3,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":53,"st":0,"bm":0},{"ddd":0,"refId":"2","w":376,"h":377,"ind":4,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[62,62],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"h":1},{"t":2,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":53,"st":0,"bm":0,"parent":3},{"ddd":0,"ind":5,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":53,"st":0,"bm":0},{"ddd":0,"refId":"3","w":470,"h":220,"ind":6,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[15,266],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":48,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":53,"st":0,"bm":0,"parent":5},{"ddd":0,"ind":7,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":53,"st":0,"bm":0},{"ddd":0,"refId":"4","w":468,"h":220,"ind":8,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[17,140],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":48,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":53,"st":0,"bm":0,"parent":7},{"ddd":0,"ind":9,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":53,"st":0,"bm":0},{"ddd":0,"refId":"5","w":470,"h":220,"ind":10,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[15,15],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":48,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":53,"st":0,"bm":0,"parent":9}]},{"id":"1","layers":[{"ddd":0,"ind":11,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-62,-62],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[61.63,31.225],[30.405,0],[61.63,-31.225],[92.854,0],[61.63,31.225]],"i":[[0,0],[0,17.218],[-17.218,0],[0,-17.217],[17.218,0]],"o":[[-17.218,0],[0,-17.217],[17.218,0],[0,17.218],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[170.998,-15.65],[122.171,-15.65],[61.63,-62.525],[1.088,-15.65],[-170.998,-15.65],[-186.648,0],[-170.998,15.65],[1.088,15.65],[61.63,62.525],[122.171,15.65],[170.998,15.65],[186.648,0],[170.998,-15.65]],"i":[[0,0],[0,0],[29.074,0],[6.965,-26.928],[0,0],[0,-8.643],[-8.644,0],[0,0],[-29.074,0],[-6.965,26.928],[0,0],[0,8.643],[8.644,0]],"o":[[0,0],[-6.965,-26.928],[-29.074,0],[0,0],[-8.644,0],[0,8.643],[0,0],[6.965,26.928],[29.074,0],[0,0],[8.644,0],[0,-8.643],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.874,249.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","nm":"Group 2","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-124.781,31.225],[-156.005,0],[-124.781,-31.225],[-93.556,0],[-124.781,31.225]],"i":[[0,0],[0,17.217],[-17.218,0],[0,-17.218],[17.218,0]],"o":[[-17.218,0],[0,-17.218],[17.218,0],[0,17.217],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[171.656,-15.65],[-64.239,-15.65],[-124.781,-62.525],[-187.306,0],[-124.781,62.525],[-64.239,15.65],[171.656,15.65],[187.306,0],[171.656,-15.65]],"i":[[0,0],[0,0],[29.074,0],[0,-34.477],[-34.477,0],[-6.965,26.928],[0,0],[0,8.643],[8.644,0]],"o":[[0,0],[-6.965,-26.928],[-34.477,0],[0,34.477],[29.074,0],[0,0],[8.644,0],[0,-8.643],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[249.779,376.306],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","nm":"Group 3","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-124.781,-31.225],[-93.556,0],[-124.781,31.225],[-156.005,0],[-124.781,-31.225]],"i":[[0,0],[0,-17.217],[17.218,0],[0,17.218],[-17.218,0]],"o":[[17.218,0],[0,17.218],[-17.218,0],[0,-17.217],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-124.781,62.525],[-64.239,15.65],[171.656,15.65],[187.306,0],[171.656,-15.65],[-64.239,-15.65],[-124.781,-62.525],[-187.306,0],[-124.781,62.525]],"i":[[0,0],[-6.965,26.928],[0,0],[0,8.643],[8.644,0],[0,0],[29.074,0],[0,-34.477],[-34.477,0]],"o":[[29.074,0],[0,0],[8.644,0],[0,-8.643],[0,0],[-6.965,-26.928],[-34.477,0],[0,34.477],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[249.784,125.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[249.998,250.654],"ix":2},"a":{"a":0,"k":[249.998,250.654],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":53,"st":0,"bm":0}]},{"id":"2","layers":[{"ddd":0,"ind":12,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-62,-62],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[61.63,31.225],[30.405,0],[61.63,-31.225],[92.854,0],[61.63,31.225]],"i":[[0,0],[0,17.218],[-17.218,0],[0,-17.217],[17.218,0]],"o":[[-17.218,0],[0,-17.217],[17.218,0],[0,17.218],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[170.998,-15.65],[122.171,-15.65],[61.63,-62.525],[1.088,-15.65],[-170.998,-15.65],[-186.648,0],[-170.998,15.65],[1.088,15.65],[61.63,62.525],[122.171,15.65],[170.998,15.65],[186.648,0],[170.998,-15.65]],"i":[[0,0],[0,0],[29.074,0],[6.965,-26.928],[0,0],[0,-8.643],[-8.644,0],[0,0],[-29.074,0],[-6.965,26.928],[0,0],[0,8.643],[8.644,0]],"o":[[0,0],[-6.965,-26.928],[-29.074,0],[0,0],[-8.644,0],[0,8.643],[0,0],[6.965,26.928],[29.074,0],[0,0],[8.644,0],[0,-8.643],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.874,249.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","nm":"Group 2","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-124.781,31.225],[-156.005,0],[-124.781,-31.225],[-93.556,0],[-124.781,31.225]],"i":[[0,0],[0,17.217],[-17.218,0],[0,-17.218],[17.218,0]],"o":[[-17.218,0],[0,-17.218],[17.218,0],[0,17.217],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[171.656,-15.65],[-64.239,-15.65],[-124.781,-62.525],[-187.306,0],[-124.781,62.525],[-64.239,15.65],[171.656,15.65],[187.306,0],[171.656,-15.65]],"i":[[0,0],[0,0],[29.074,0],[0,-34.477],[-34.477,0],[-6.965,26.928],[0,0],[0,8.643],[8.644,0]],"o":[[0,0],[-6.965,-26.928],[-34.477,0],[0,34.477],[29.074,0],[0,0],[8.644,0],[0,-8.643],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[249.779,376.306],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","nm":"Group 3","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-124.781,-31.225],[-93.556,0],[-124.781,31.225],[-156.005,0],[-124.781,-31.225]],"i":[[0,0],[0,-17.217],[17.218,0],[0,17.218],[-17.218,0]],"o":[[17.218,0],[0,17.218],[-17.218,0],[0,-17.217],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-124.781,62.525],[-64.239,15.65],[171.656,15.65],[187.306,0],[171.656,-15.65],[-64.239,-15.65],[-124.781,-62.525],[-187.306,0],[-124.781,62.525]],"i":[[0,0],[-6.965,26.928],[0,0],[0,8.643],[8.644,0],[0,0],[29.074,0],[0,-34.477],[-34.477,0]],"o":[[29.074,0],[0,0],[8.644,0],[0,-8.643],[0,0],[-6.965,-26.928],[-34.477,0],[0,34.477],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[249.784,125.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[249.998,250.654],"ix":2},"a":{"a":0,"k":[249.998,250.654],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":53,"st":0,"bm":0}]},{"id":"3","layers":[{"ddd":0,"ind":13,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-15,-266],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","nm":"Shape 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[326.5,376.25],[78.5,376.25]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}}},{"ty":"tm","s":{"a":1,"k":[{"t":2,"s":[100],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":25,"s":[68.292],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":50,"s":[100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Group 1","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":2,"s":[{"c":false,"v":[[124.567,0],[-124.567,0]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":25,"s":[{"c":false,"v":[[124.567,0],[-45.297,0]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":50,"s":[{"c":false,"v":[[124.567,0],[-124.567,0]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[296.868,376.306],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Group 2","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":2,"s":[{"c":true,"v":[[46.875,0],[0,46.875],[-46.875,0],[0,-46.875],[46.875,0]],"i":[[0,0],[25.888,0],[0,25.888],[-25.888,0],[0,-25.888]],"o":[[0,25.888],[-25.888,0],[0,-25.888],[25.888,0],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":25,"s":[{"c":true,"v":[[126.145,0],[79.27,46.875],[32.394,0],[79.27,-46.875],[126.145,0]],"i":[[0,0],[25.888,0],[0,25.888],[-25.888,0],[0,-25.888]],"o":[[0,25.888],[-25.888,0],[0,-25.888],[25.888,0],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":50,"s":[{"c":true,"v":[[46.875,0],[0,46.875],[-46.875,0],[0,-46.875],[46.875,0]],"i":[[0,0],[25.888,0],[0,25.888],[-25.888,0],[0,-25.888]],"o":[[0,25.888],[-25.888,0],[0,-25.888],[25.888,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[124.998,376.306],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[249.779,376.306],"ix":2},"a":{"a":0,"k":[249.779,376.306],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":53,"st":0,"bm":0}]},{"id":"4","layers":[{"ddd":0,"ind":14,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-17,-140],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":2,"s":[{"c":false,"v":[[-31.247,0],[31.247,0]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":22,"s":[{"c":false,"v":[[24.753,0],[31.247,0]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":50,"s":[{"c":false,"v":[[-31.247,0],[31.247,0]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[390.626,249.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Group 2","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":2,"s":[{"c":false,"v":[[-90.712,0],[90.712,0]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":22,"s":[{"c":false,"v":[[-90.712,0],[146.712,0]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":50,"s":[{"c":false,"v":[[-90.712,0],[90.712,0]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[170.589,249.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Group 3","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":2,"s":[{"c":true,"v":[[-46.875,0],[0,46.875],[46.875,0],[0,-46.875],[-46.875,0]],"i":[[0,0],[-25.888,0],[0,25.888],[25.888,0],[0,-25.888]],"o":[[0,25.888],[25.888,0],[0,-25.888],[-25.888,0],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":22,"s":[{"c":true,"v":[[9.125,0],[56,46.875],[102.875,0],[56,-46.875],[9.125,0]],"i":[[0,0],[-25.888,0],[0,25.888],[25.888,0],[0,-25.888]],"o":[[0,25.888],[25.888,0],[0,-25.888],[-25.888,0],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":50,"s":[{"c":true,"v":[[-46.875,0],[0,46.875],[46.875,0],[0,-46.875],[-46.875,0]],"i":[[0,0],[-25.888,0],[0,25.888],[25.888,0],[0,-25.888]],"o":[[0,25.888],[25.888,0],[0,-25.888],[-25.888,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[312.504,249.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.874,249.998],"ix":2},"a":{"a":0,"k":[250.874,249.998],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":53,"st":0,"bm":0}]},{"id":"5","layers":[{"ddd":0,"ind":15,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-15,-15],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":2,"s":[{"c":false,"v":[[124.567,0],[-124.567,0]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":27,"s":[{"c":false,"v":[[124.567,0],[29.714,0]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":50,"s":[{"c":false,"v":[[124.567,0],[-124.567,0]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[296.873,125.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Group 2","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":2,"s":[{"c":true,"v":[[46.875,0],[0,46.875],[-46.875,0],[0,-46.875],[46.875,0]],"i":[[0,0],[25.888,0],[0,25.888],[-25.888,0],[0,-25.888]],"o":[[0,25.888],[-25.888,0],[0,-25.888],[25.888,0],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":27,"s":[{"c":true,"v":[[201.156,0],[154.28,46.876],[107.405,0],[154.28,-46.875],[201.156,0]],"i":[[0,0],[25.888,0],[0,25.888],[-25.888,0],[0,-25.888]],"o":[[0,25.888],[-25.888,0],[0,-25.888],[25.888,0],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":50,"s":[{"c":true,"v":[[46.875,0],[0,46.875],[-46.875,0],[0,-46.875],[46.875,0]],"i":[[0,0],[25.888,0],[0,25.888],[-25.888,0],[0,-25.888]],"o":[[0,25.888],[-25.888,0],[0,-25.888],[25.888,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[125.003,125.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Shape 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[326.5,125.25],[78.5,125.25]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}}},{"ty":"tm","s":{"a":1,"k":[{"t":2,"s":[100],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":27,"s":[40.292],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":50,"s":[100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[249.784,125.002],"ix":2},"a":{"a":0,"k":[249.784,125.002],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":53,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":12345679,"ty":4,"nm":"Group Layer 8","sr":1,"ks":{"p":{"a":0,"k":[365,464.38524590163934,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[51.229508196721305,51.229508196721305,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[220.741,37.184],[225.501,35.896],[228.749,32.36800000000001],[229.981,27.216000000000008],[228.749,22.12],[225.501,18.592],[220.741,17.304],[215.981,18.592],[212.677,22.12],[211.501,27.216000000000008],[212.677,32.36800000000001],[215.981,35.896],[220.741,37.184],[220.741,37.184],[220.741,37.184]],"i":[[0,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.382000000000062,0.8586999999999989],[1.79200000000003,0],[1.418999999999983,-0.8586999999999989],[0.8220000000000027,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.382000000000062,-0.8586999999999989],[0.8220000000000027,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.418999999999983,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[221.357,43.06400000000001],[214.917,41.608],[210.49300000000005,37.408],[211.221,36.232],[211.221,42.392],[205.173,42.392],[205.173,0],[211.501,0],[211.501,18.36800000000001],[210.49300000000005,16.912000000000006],[214.973,12.88],[221.357,11.424000000000007],[229.085,13.49600000000001],[234.51700000000005,19.152],[236.533,27.216000000000008],[234.51700000000005,35.28],[229.141,40.992],[221.357,43.06400000000001],[221.357,43.06400000000001],[221.357,43.06400000000001]],"i":[[0,0],[1.942000000000007,0.9706999999999937],[1.045999999999935,1.829300000000003],[-0.2426666666666506,0.3919999999999959],[0,-2.053333333333327],[2.015999999999963,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.122666666666674],[0.3360000000000127,0.4853333333333296],[-1.865999999999985,0.9707000000000079],[-2.38900000000001,0],[-2.277000000000044,-1.3813000000000102],[-1.30600000000004,-2.389300000000006],[0,-2.986699999999999],[1.343999999999937,-2.389300000000006],[2.27800000000002,-1.4187000000000012],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.351999999999975,0],[-1.903999999999996,-0.9707000000000079],[0.2426666666666506,-0.3919999999999959],[0,2.053333333333327],[-2.015999999999963,0],[0,-14.13066666666667],[2.109333333333325,0],[0,6.122666666666667],[-0.3360000000000127,-0.4853333333333296],[1.120000000000005,-1.717300000000009],[1.867000000000075,-0.9706999999999937],[2.875,0],[2.314999999999941,1.381299999999996],[1.343999999999937,2.389300000000006],[0,2.986699999999999],[-1.30600000000004,2.389300000000006],[-2.27699999999993,1.381299999999996],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[181.87,43.06400000000001],[176.438,42],[172.854,38.976],[171.566,34.384],[172.63,29.960000000000008],[176.046,26.656000000000006],[181.814,24.752],[192.342,23.016000000000005],[192.342,28],[183.046,29.624],[179.35,31.248],[178.174,34.16],[179.462,37.016000000000005],[182.878,38.08],[187.35799999999995,36.96000000000001],[190.38199999999995,33.992],[191.446,29.792],[191.446,22.008],[189.766,18.36800000000001],[185.398,16.912000000000006],[180.974,18.256],[178.23,21.616],[172.966,18.98400000000001],[175.71,15.064000000000007],[180.134,12.376],[185.566,11.424000000000007],[191.894,12.768],[196.206,16.52],[197.774,22.008],[197.774,42.392],[191.726,42.392],[191.726,36.904],[193.014,37.072],[190.27,40.264],[186.518,42.336],[181.87,43.06400000000001],[181.87,43.06400000000001],[181.87,43.06400000000001]],"i":[[0,0],[1.567999999999984,0.7092999999999989],[0.8589999999999236,1.2693000000000012],[0,1.7547],[-0.7089999999999463,1.306699999999992],[-1.530000000000086,0.8960000000000008],[-2.313999999999965,0.3733000000000004],[-3.509333333333302,0.5786666666666633],[0,-1.661333333333332],[3.098666666666645,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8579999999999472,-0.7467000000000041],[-1.381000000000085,0],[-1.268999999999892,0.7466999999999899],[-0.7089999999999463,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829999999999927,0],[1.269999999999982,-0.8960000000000008],[0.5979999999999563,-1.381299999999996],[1.754666666666708,0.8773333333333255],[-1.269000000000005,1.11999999999999],[-1.680000000000064,0.6346999999999952],[-1.903999999999996,0],[-1.828999999999951,-0.8960000000000008],[-1.008000000000038,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999963,0],[0,1.829333333333338],[-0.4293333333333749,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.717999999999961,0],[0,0],[0,0]],"o":[[-2.052999999999997,0],[-1.529999999999973,-0.7467000000000041],[-0.8580000000000609,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999573,-1.306700000000006],[1.530999999999949,-0.8960000000000008],[3.509333333333302,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666645,0.541333333333327],[-1.680000000000064,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.8959999999999582,0.7092999999999989],[1.717999999999961,0],[1.307000000000016,-0.7467000000000041],[0.7100000000000364,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.081999999999994,-0.9707000000000079],[-1.680000000000064,0],[-1.232000000000085,0.8586999999999989],[-1.754666666666708,-0.8773333333333255],[0.5599999999999454,-1.493300000000005],[1.269999999999982,-1.157300000000006],[1.717999999999961,-0.6347000000000094],[2.389999999999986,0],[1.866999999999962,0.8960000000000008],[1.045999999999935,1.568000000000012],[0,6.794666666666672],[-2.015999999999963,0],[0,-1.829333333333338],[0.4293333333333749,0.05599999999999739],[-0.70900000000006,1.2319999999999993],[-1.081999999999994,0.8960000000000008],[-1.380999999999972,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[159.072,42.392],[159.072,0],[165.4,0],[165.4,42.392],[159.072,42.392],[159.072,42.392],[159.072,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-14.13066666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,14.13066666666667],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[138.952,43.06400000000001],[130.888,40.992],[125.456,35.28],[123.496,27.16],[125.456,19.040000000000006],[130.832,13.49600000000001],[138.448,11.424000000000007],[144.552,12.600000000000009],[149.088,15.848],[151.888,20.49600000000001],[152.896,26.096],[152.84,27.608],[152.616,29.064000000000007],[128.48,29.064000000000007],[128.48,24.024],[149.032,24.024],[146.008,26.320000000000007],[145.616,21.448000000000008],[142.816,18.032],[138.448,16.744],[133.968,18.032],[130.944,21.616],[130.104,27.216000000000008],[130.944,32.592],[134.192,36.176],[139.008,37.464],[143.65599999999995,36.232],[146.736,33.040000000000006],[151.888,35.56],[149.088,39.42400000000001],[144.60799999999995,42.11200000000001],[138.952,43.06400000000001],[138.952,43.06400000000001],[138.952,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.307000000000016,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.836999999999989,0],[-1.79200000000003,-0.784000000000006],[-1.231999999999971,-1.381299999999996],[-0.6349999999999909,-1.754700000000014],[0,-1.978700000000003],[0.03699999999992087,-0.5227000000000004],[0.1119999999999663,-0.4480000000000075],[8.04533333333336,0],[0,1.680000000000007],[-6.850666666666712,0],[1.008000000000038,-0.7653333333333308],[0.6349999999999909,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.680000000000064,0],[1.307000000000016,-0.8586999999999989],[0.70900000000006,-1.567999999999998],[-0.1490000000000009,-2.202700000000007],[-0.7469999999999573,-1.530699999999996],[-1.380999999999972,-0.8586999999999989],[-1.79200000000003,0],[-1.268999999999892,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333386,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.755000000000109,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.315000000000055,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999937,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.277000000000044,0],[1.79200000000003,0.7839999999999918],[1.232000000000085,1.344000000000008],[0.6720000000000255,1.7547],[0,0.4852999999999952],[-0.03700000000003456,0.5227000000000004],[-8.04533333333336,0],[0,-1.680000000000007],[6.850666666666712,0],[-1.008000000000038,0.7653333333333308],[0.3729999999999336,-1.829300000000003],[-0.59699999999998,-1.456000000000003],[-1.232000000000085,-0.8586999999999989],[-1.67999999999995,0],[-1.306999999999903,0.8213000000000079],[-0.7089999999999463,1.530699999999996],[-0.1870000000000118,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418999999999983,0.8586999999999989],[1.828999999999951,0],[1.307000000000016,-0.8212999999999937],[1.717333333333386,0.8400000000000034],[-0.59699999999998,1.4187000000000012],[-1.231999999999971,1.11999999999999],[-1.716999999999985,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,7.951999999999998],[111.001,0.6720000000000041],[117.329,0.6720000000000041],[117.329,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,42.392],[111.001,12.096],[117.329,12.096],[117.329,42.392],[111.001,42.392],[111.001,42.392],[111.001,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[101.41,42.72800000000001],[94.01800000000003,40.040000000000006],[91.38599999999997,32.48],[91.38599999999997,17.808000000000007],[86.06600000000003,17.808000000000007],[86.06600000000003,12.096],[86.90599999999995,12.096],[90.21000000000004,10.864],[91.38599999999997,7.504000000000005],[91.38599999999997,5.152000000000001],[97.71400000000006,5.152000000000001],[97.71400000000006,12.096],[104.602,12.096],[104.602,17.808000000000007],[97.71400000000006,17.808000000000007],[97.71400000000006,32.2],[98.21799999999996,34.888000000000005],[99.84199999999998,36.568],[102.754,37.128],[103.76200000000006,37.072],[104.826,36.96000000000001],[104.826,42.392],[103.09,42.616],[101.41,42.72800000000001],[101.41,42.72800000000001],[101.41,42.72800000000001]],"i":[[0,0],[1.754999999999995,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333312,0],[0,1.903999999999996],[-0.2799999999999727,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999935,0],[0,-1.903999999999996],[2.295999999999935,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7469999999999573,-0.4106999999999914],[-1.19500000000005,0],[-0.3730000000000473,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6349999999999909,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333312,0],[0,-1.903999999999996],[0.2799999999999727,0],[1.419000000000096,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999935,0],[0,1.903999999999996],[-2.295999999999935,0],[0,4.797333333333327],[0,1.045299999999997],[0.3360000000000127,0.7092999999999989],[0.7470000000000709,0.3733000000000004],[0.2989999999999782,0],[0.3729999999999336,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[78.71499999999997,42.72800000000001],[71.32299999999998,40.040000000000006],[68.69100000000003,32.48],[68.69100000000003,17.808000000000007],[63.37099999999998,17.808000000000007],[63.37099999999998,12.096],[64.21100000000001,12.096],[67.51499999999999,10.864],[68.69100000000003,7.504000000000005],[68.69100000000003,5.152000000000001],[75.019,5.152000000000001],[75.019,12.096],[81.90700000000004,12.096],[81.90700000000004,17.808000000000007],[75.019,17.808000000000007],[75.019,32.2],[75.52300000000002,34.888000000000005],[77.14699999999999,36.568],[80.05900000000003,37.128],[81.06700000000001,37.072],[82.13099999999997,36.96000000000001],[82.13099999999997,42.392],[80.39499999999998,42.616],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001]],"i":[[0,0],[1.754000000000019,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7470000000000141,-0.4106999999999914],[-1.19500000000005,0],[-0.3740000000000236,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6340000000000146,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173999999999978,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418000000000006,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7460000000000377,0.3733000000000004],[0.297999999999945,0],[0.3730000000000473,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,37.184],[48.94799999999998,35.896],[52.19600000000003,32.36800000000001],[53.428,27.216000000000008],[52.19600000000003,22.12],[48.94799999999998,18.592],[44.18799999999999,17.304],[39.428,18.592],[36.12400000000002,22.12],[34.94799999999998,27.216000000000008],[36.12400000000002,32.36800000000001],[39.428,35.896],[44.18799999999999,37.184],[44.18799999999999,37.184],[44.18799999999999,37.184]],"i":[[0,0],[-1.381999999999948,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.381000000000029,0.8586999999999989],[1.79200000000003,0],[1.418000000000006,-0.8586999999999989],[0.8209999999999695,-1.493300000000005],[0,-1.904000000000011],[-0.7840000000000487,-1.5307000000000102],[-1.382000000000005,-0.8586999999999989],[-1.754999999999995,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.381000000000029,-0.8586999999999989],[0.8209999999999695,-1.5307000000000102],[0,-1.904000000000011],[-0.7840000000000487,-1.493300000000005],[-1.381999999999948,-0.8586999999999989],[-1.754999999999995,0],[-1.382000000000005,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.418000000000006,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,43.06400000000001],[36.18000000000001,40.992],[30.46800000000002,35.336],[28.33999999999997,27.216000000000008],[30.46800000000002,19.096],[36.18000000000001,13.49600000000001],[44.18799999999999,11.424000000000007],[52.19600000000003,13.49600000000001],[57.85199999999998,19.096],[59.98000000000002,27.216000000000008],[57.85199999999998,35.392],[52.139999999999986,41.048],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001]],"i":[[0,0],[2.425999999999988,1.381299999999996],[1.418000000000006,2.389300000000006],[0,3.024000000000001],[-1.41900000000004,2.352000000000004],[-2.389999999999986,1.343999999999994],[-2.949999999999989,0],[-2.352000000000032,-1.3813000000000102],[-1.381999999999948,-2.389300000000006],[0,-3.061300000000003],[1.418000000000006,-2.389299999999992],[2.38900000000001,-1.381299999999996],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.911999999999978,0],[-2.389999999999986,-1.381299999999996],[-1.41900000000004,-2.389299999999992],[0,-3.061300000000003],[1.418000000000006,-2.389300000000006],[2.38900000000001,-1.3813000000000102],[2.98599999999999,0],[2.388999999999953,1.343999999999994],[1.418000000000006,2.352000000000004],[0,3.061299999999989],[-1.418999999999983,2.389300000000006],[-2.389999999999986,1.343999999999994],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.608000000000004,0.6720000000000041],[6.608000000000004,36.512],[24.639999999999986,36.512],[24.639999999999986,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.202666666666687,0],[0,-11.94666666666666],[-6.01066666666668,0],[0,-1.959999999999994],[8.21333333333331,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.202666666666687,0],[0,11.94666666666667],[6.01066666666668,0],[0,1.959999999999994],[-8.21333333333331,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[98.08047485351562,-21.67217254638672],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[246.681,42.392],[246.681,0],[253.009,0],[253.009,18.032],[252.001,17.248],[255.585,12.936000000000007],[261.297,11.424000000000007],[267.23299999999995,12.88],[271.265,16.912000000000006],[272.721,22.792],[272.721,42.392],[266.449,42.392],[266.449,24.528000000000006],[265.553,20.664],[263.201,18.2],[259.729,17.304],[256.25699999999995,18.2],[253.849,20.664],[253.009,24.528000000000006],[253.009,42.392],[246.681,42.392],[246.681,42.392],[246.681,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.010666666666665],[0.3360000000000127,0.2613333333333259],[-1.643000000000029,0.9706999999999937],[-2.166000000000054,0],[-1.717999999999961,-0.9706999999999937],[-0.9710000000000036,-1.717300000000009],[0,-2.202699999999993],[0,-6.533333333333331],[2.090666666666721,0],[0,5.954666666666668],[0.59699999999998,1.045299999999997],[1.007999999999925,0.5600000000000023],[1.305999999999926,0],[1.045000000000073,-0.5973000000000042],[0.59699999999998,-1.082700000000003],[0,-1.493300000000005],[0,-5.954666666666668],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,6.010666666666665],[-0.3360000000000127,-0.2613333333333259],[0.7459999999999809,-1.903999999999996],[1.641999999999967,-1.0080000000000098],[2.240000000000009,0],[1.717000000000098,0.9707000000000079],[0.9700000000000273,1.717299999999994],[0,6.533333333333331],[-2.090666666666721,0],[0,-5.954666666666668],[0,-1.5307000000000102],[-0.5599999999999454,-1.082700000000003],[-1.008000000000038,-0.5973000000000042],[-1.270000000000095,0],[-1.007999999999953,0.5600000000000023],[-0.5600000000000023,1.082700000000003],[0,5.954666666666668],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[237.089,42.72800000000001],[229.697,40.040000000000006],[227.065,32.48],[227.065,17.808000000000007],[221.745,17.808000000000007],[221.745,12.096],[222.585,12.096],[225.889,10.864],[227.065,7.504000000000005],[227.065,5.152000000000001],[233.393,5.152000000000001],[233.393,12.096],[240.281,12.096],[240.281,17.808000000000007],[233.393,17.808000000000007],[233.393,32.2],[233.897,34.888000000000005],[235.521,36.568],[238.433,37.128],[239.441,37.072],[240.505,36.96000000000001],[240.505,42.392],[238.769,42.616],[237.089,42.72800000000001],[237.089,42.72800000000001],[237.089,42.72800000000001]],"i":[[0,0],[1.755000000000052,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7459999999999809,-0.4106999999999914],[-1.194000000000017,0],[-0.3729999999999905,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6350000000000477,-0.07469999999999288],[0.48599999999999,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.753999999999962,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418999999999983,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7470000000000141,0.3733000000000004],[0.2989999999999782,0],[0.3740000000000236,-0.03730000000000189],[0,1.810666666666663],[-0.5220000000000482,0.0747000000000071],[-0.6339999999999577,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,7.951999999999998],[210.259,0.6720000000000041],[216.587,0.6720000000000041],[216.587,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,42.392],[210.259,12.096],[216.587,12.096],[216.587,42.392],[210.259,42.392],[210.259,42.392],[210.259,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[169.688,42.392],[159.272,12.096],[165.992,12.096],[173.944,36.232],[171.592,36.232],[179.712,12.096],[185.48,12.096],[193.544,36.232],[191.192,36.232],[199.2,12.096],[205.92,12.096],[195.448,42.392],[189.736,42.392],[181.56,17.696],[183.632,17.696],[175.456,42.392],[169.688,42.392],[169.688,42.392],[169.688,42.392]],"i":[[0,0],[3.47199999999998,10.09866666666667],[-2.240000000000009,0],[-2.650666666666666,-8.045333333333332],[0.7839999999999918,0],[-2.706666666666649,8.045333333333332],[-1.922666666666657,0],[-2.687999999999988,-8.045333333333332],[0.7839999999999918,0],[-2.669333333333327,8.045333333333332],[-2.240000000000009,0],[3.490666666666641,-10.09866666666667],[1.903999999999996,0],[2.725333333333367,8.232],[-0.6906666666666865,0],[2.72533333333331,-8.232],[1.922666666666657,0],[0,0],[0,0]],"o":[[-3.47199999999998,-10.09866666666666],[2.240000000000009,0],[2.650666666666666,8.045333333333332],[-0.7839999999999918,0],[2.706666666666649,-8.045333333333332],[1.922666666666657,0],[2.687999999999988,8.045333333333332],[-0.7839999999999918,0],[2.669333333333327,-8.045333333333332],[2.240000000000009,0],[-3.490666666666641,10.09866666666666],[-1.903999999999996,0],[-2.725333333333367,-8.232],[0.6906666666666865,0],[-2.72533333333331,8.232],[-1.922666666666657,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[155.86146545410156,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[132.444,43.06400000000001],[124.38,40.992],[118.948,35.28],[116.988,27.16],[118.948,19.040000000000006],[124.324,13.49600000000001],[131.94,11.424000000000007],[138.044,12.600000000000009],[142.58,15.848],[145.38,20.49600000000001],[146.388,26.096],[146.332,27.608],[146.108,29.064000000000007],[121.972,29.064000000000007],[121.972,24.024],[142.524,24.024],[139.5,26.320000000000007],[139.108,21.448000000000008],[136.308,18.032],[131.94,16.744],[127.46,18.032],[124.436,21.616],[123.596,27.216000000000008],[124.436,32.592],[127.684,36.176],[132.5,37.464],[137.148,36.232],[140.228,33.040000000000006],[145.38,35.56],[142.58,39.42400000000001],[138.1,42.11200000000001],[132.444,43.06400000000001],[132.444,43.06400000000001],[132.444,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.305999999999983,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.838000000000022,0],[-1.79200000000003,-0.784000000000006],[-1.232000000000028,-1.381299999999996],[-0.6350000000000477,-1.754700000000014],[0,-1.978700000000003],[0.03699999999997772,-0.5227000000000004],[0.1120000000000232,-0.4480000000000075],[8.045333333333303,0],[0,1.680000000000007],[-6.850666666666655,0],[1.007999999999981,-0.7653333333333308],[0.6340000000000146,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.67999999999995,0],[1.305999999999983,-0.8586999999999989],[0.7090000000000032,-1.567999999999998],[-0.1499999999999773,-2.202700000000007],[-0.7470000000000141,-1.530699999999996],[-1.382000000000005,-0.8586999999999989],[-1.791999999999973,0],[-1.269999999999982,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333329,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.754000000000019,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.314999999999998,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999994,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.276999999999987,0],[1.791999999999973,0.7839999999999918],[1.231999999999971,1.344000000000008],[0.6719999999999686,1.7547],[0,0.4852999999999952],[-0.03800000000001091,0.5227000000000004],[-8.045333333333303,0],[0,-1.680000000000007],[6.850666666666655,0],[-1.007999999999981,0.7653333333333308],[0.3730000000000473,-1.829300000000003],[-0.5979999999999563,-1.456000000000003],[-1.232000000000028,-0.8586999999999989],[-1.680000000000007,0],[-1.307000000000016,0.8213000000000079],[-0.7100000000000364,1.530699999999996],[-0.186999999999955,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418000000000006,0.8586999999999989],[1.829000000000008,0],[1.305999999999983,-0.8212999999999937],[1.717333333333329,0.8400000000000034],[-0.5980000000000132,1.4187000000000012],[-1.232000000000028,1.11999999999999],[-1.718000000000018,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[95.32,37.184],[100.024,35.896],[103.328,32.36800000000001],[104.56,27.216000000000008],[103.328,22.12],[100.024,18.592],[95.32,17.304],[90.56,18.592],[87.256,22.12],[86.08000000000001,27.216000000000008],[87.256,32.36800000000001],[90.50399999999999,35.896],[95.32,37.184],[95.32,37.184],[95.32,37.184]],"i":[[0,0],[-1.381,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.820999999999998,1.493299999999991],[1.419000000000011,0.8586999999999989],[1.754999999999995,0],[1.418999999999983,-0.8586999999999989],[0.7839999999999918,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.381,-0.8586999999999989],[-1.792000000000002,0],[0,0],[0,0]],"o":[[1.754999999999995,0],[1.419000000000011,-0.8586999999999989],[0.820999999999998,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.381,-0.8586999999999989],[-1.754999999999995,0],[-1.419000000000011,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.7839999999999918,1.493299999999991],[1.419000000000011,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[94.70400000000001,43.06400000000001],[86.864,40.992],[81.43199999999999,35.28],[79.47200000000001,27.216000000000008],[81.488,19.152],[86.91999999999999,13.49600000000001],[94.648,11.424000000000007],[101.088,12.88],[105.512,16.912000000000006],[104.56,18.36800000000001],[104.56,0],[110.832,0],[110.832,42.392],[104.84,42.392],[104.84,36.232],[105.568,37.408],[101.088,41.608],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001]],"i":[[0,0],[2.314999999999998,1.381299999999996],[1.344000000000023,2.389300000000006],[0,2.986699999999999],[-1.343999999999994,2.389300000000006],[-2.276999999999987,1.381299999999996],[-2.875,0],[-1.8669999999999902,-0.9706999999999937],[-1.082999999999998,-1.717300000000009],[0.3173333333333233,-0.4853333333333296],[0,6.122666666666674],[-2.090666666666664,0],[0,-14.13066666666667],[1.99733333333333,0],[0,2.053333333333327],[-0.242666666666679,-0.3919999999999959],[1.941000000000003,-0.9707000000000079],[2.314999999999998,0],[0,0],[0,0]],"o":[[-2.912000000000006,0],[-2.277000000000015,-1.4187000000000012],[-1.306999999999988,-2.389300000000006],[0,-2.986699999999999],[1.343999999999994,-2.389300000000006],[2.277000000000015,-1.3813000000000102],[2.426999999999992,0],[1.867000000000019,0.9707000000000079],[-0.3173333333333233,0.4853333333333296],[0,-6.122666666666674],[2.090666666666664,0],[0,14.13066666666667],[-1.99733333333333,0],[0,-2.053333333333327],[0.242666666666679,0.3919999999999959],[-1.045000000000016,1.829300000000003],[-1.941000000000003,0.9706999999999937],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[57.40100000000001,43.06400000000001],[51.968999999999994,42],[48.38499999999999,38.976],[47.09700000000001,34.384],[48.161,29.960000000000008],[51.577,26.656000000000006],[57.345,24.752],[67.87299999999999,23.016000000000005],[67.87299999999999,28],[58.577,29.624],[54.881,31.248],[53.70500000000001,34.16],[54.992999999999995,37.016000000000005],[58.40899999999999,38.08],[62.88900000000001,36.96000000000001],[65.91300000000001,33.992],[66.977,29.792],[66.977,22.008],[65.297,18.36800000000001],[60.929,16.912000000000006],[56.505,18.256],[53.761,21.616],[48.496999999999986,18.98400000000001],[51.240999999999985,15.064000000000007],[55.66499999999999,12.376],[61.09700000000001,11.424000000000007],[67.42500000000001,12.768],[71.737,16.52],[73.305,22.008],[73.305,42.392],[67.257,42.392],[67.257,36.904],[68.54499999999999,37.072],[65.80099999999999,40.264],[62.04900000000001,42.336],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001]],"i":[[0,0],[1.568000000000012,0.7092999999999989],[0.8590000000000089,1.2693000000000012],[0,1.7547],[-0.7090000000000032,1.306699999999992],[-1.531000000000006,0.8960000000000008],[-2.314999999999998,0.3733000000000004],[-3.509333333333331,0.5786666666666633],[0,-1.661333333333332],[3.098666666666674,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8590000000000089,-0.7467000000000041],[-1.381,0],[-1.269000000000005,0.7466999999999899],[-0.7090000000000032,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829000000000008,0],[1.269000000000005,-0.8960000000000008],[0.5970000000000084,-1.381299999999996],[1.754666666666679,0.8773333333333255],[-1.268999999999977,1.11999999999999],[-1.680000000000007,0.6346999999999952],[-1.903999999999996,0],[-1.829000000000008,-0.8960000000000008],[-1.0080000000000098,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999991,0],[0,1.829333333333338],[-0.429333333333318,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.716999999999985,0],[0,0],[0,0]],"o":[[-2.053000000000026,0],[-1.531000000000006,-0.7467000000000041],[-0.8589999999999804,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999857,-1.306700000000006],[1.531000000000006,-0.8960000000000008],[3.509333333333331,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666674,0.541333333333327],[-1.680000000000007,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.896000000000015,0.7092999999999989],[1.717000000000013,0],[1.306999999999988,-0.7467000000000041],[0.7089999999999748,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.082999999999998,-0.9707000000000079],[-1.680000000000007,0],[-1.2319999999999993,0.8586999999999989],[-1.754666666666679,-0.8773333333333255],[0.5600000000000023,-1.493300000000005],[1.269000000000005,-1.157300000000006],[1.717000000000013,-0.6347000000000094],[2.388999999999982,0],[1.86699999999999,0.8960000000000008],[1.045000000000016,1.568000000000012],[0,6.794666666666672],[-2.015999999999991,0],[0,-1.829333333333338],[0.429333333333318,0.05599999999999739],[-0.7089999999999748,1.2319999999999993],[-1.082999999999998,0.8960000000000008],[-1.381,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.159999999999997,0.6720000000000041],[21.84,22.400000000000006],[18.75999999999999,22.400000000000006],[34.16,0.6720000000000041],[40.31999999999999,0.6720000000000041],[40.31999999999999,42.392],[33.768,42.392],[33.768,8.456000000000003],[36.232,9.128],[20.49600000000001,30.632000000000005],[19.824000000000012,30.632000000000005],[4.424000000000007,9.128],[6.608000000000004,8.456000000000003],[6.608000000000004,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.053333333333342,0],[-5.226666666666659,-7.242666666666665],[1.026666666666671,0],[-5.133333333333326,7.242666666666672],[-2.053333333333342,0],[0,-13.90666666666667],[2.183999999999997,0],[0,11.312],[-0.8213333333333424,-0.2240000000000038],[5.245333333333321,-7.168000000000006],[0.2239999999999895,0],[5.133333333333326,7.168000000000006],[-0.7280000000000086,0.2240000000000038],[0,-11.312],[2.202666666666659,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.053333333333342,0],[5.226666666666659,7.242666666666672],[-1.026666666666671,0],[5.133333333333326,-7.242666666666665],[2.053333333333342,0],[0,13.90666666666666],[-2.183999999999997,0],[0,-11.312],[0.8213333333333424,0.2240000000000038],[-5.245333333333321,7.168000000000006],[-0.2239999999999895,0],[-5.133333333333326,-7.168000000000006],[0.7280000000000086,-0.2240000000000038],[0,11.312],[-2.202666666666659,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[2.91259765625,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[702.6863719370097,144],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":72,"ix":2}},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[56.54167175292969,-0.000022762338630855083],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":80,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[122.0000003294881,25.00000012138912],"ix":2},"a":{"a":0,"k":[56.54167175292969,-0.00002288818359375],"ix":2},"s":{"a":0,"k":[34.403572049765366,34.403572049765366],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":53,"st":0,"bm":0},{"ddd":0,"ind":16,"ty":3,"nm":"Color & Stroke Change","sr":1,"ks":{"p":{"a":0,"k":[250,250],"ix":2},"a":{"a":0,"k":[50,50],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":53,"st":0,"bm":0},{"ddd":0,"refId":"0","w":500,"h":500,"ind":17,"ty":0,"nm":"hover-slider","sr":1,"ks":{"p":{"a":0,"k":[50,50],"ix":2},"a":{"a":0,"k":[250,250],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":53,"st":0,"bm":0,"parent":16}],"markers":[]} \ No newline at end of file diff --git a/frontend/public/lotties/unlock.json b/frontend/public/lotties/unlock.json new file mode 100644 index 000000000..4356593e9 --- /dev/null +++ b/frontend/public/lotties/unlock.json @@ -0,0 +1,470 @@ +{ + "v": "5.12.2", + "fr": 29.9700012207031, + "ip": 0, + "op": 45.0000018328876, + "w": 48, + "h": 48, + "nm": "unlock", + "ddd": 0, + "assets": [], + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 4, + "nm": "unlock-outline-top_s1g1_s2g2_s3g1_s4g1_background Outlines", + "parent": 2, + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { + "a": 1, + "k": [ + { "i": { "x": [0.667], "y": [1] }, "o": { "x": [0.333], "y": [0] }, "t": 12, "s": [0] }, + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 28, + "s": [-16] + }, + { "t": 40.0000016292334, "s": [0] } + ], + "ix": 10 + }, + "p": { "a": 0, "k": [19, 19.473, 0], "ix": 2, "l": 2 }, + "a": { "a": 0, "k": [8.5, 11.125, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0, 1.292], + [1.933, 0], + [0, -1.933], + [-1.042, -0.607], + [0, 0], + [-0.966, 0], + [0, 0.966], + [0, 0] + ], + "o": [ + [0, -1.933], + [-1.933, 0], + [0, 1.292], + [0, 0], + [0, 0.966], + [0.966, 0], + [0, 0], + [1.042, -0.607] + ], + "v": [ + [3.5, -2.625], + [0, -6.125], + [-3.5, -2.625], + [-1.75, 0.39], + [-1.75, 4.375], + [0, 6.125], + [1.75, 4.375], + [1.75, 0.39] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [1, 1, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { "a": 0, "k": 2.5, "ix": 5 }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [1, 1, 1], "ix": 4 }, + "o": { "a": 0, "k": 0, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [8.5, 11.125], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 45.0000018328876, + "st": 0, + "ct": 1, + "bm": 0 + }, + { + "ddd": 0, + "ind": 2, + "ty": 4, + "nm": "unlock-outline-bot_s1g1_s2g1_s3g1_s4g1_background Outlines", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [24, 29.826, 0], + "to": [0, 0.313, 0], + "ti": [0, 0, 0] + }, + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 12, + "s": [24, 31.701, 0], + "to": [0, 0, 0], + "ti": [0, 0.313, 0] + }, + { "t": 28.0000011404634, "s": [24, 29.826, 0] } + ], + "ix": 2, + "l": 2 + }, + "a": { "a": 0, "k": [19, 19.523, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [-1.43, 0.576], + [0, 0], + [-3.266, -1.28], + [0, 0], + [0, -1.555], + [0, 0], + [7.732, 0], + [0, 8.353], + [0, 0] + ], + "o": [ + [0, 0], + [3.253, -1.31], + [0, 0], + [1.448, 0.567], + [0, 0], + [0, 8.353], + [-7.732, 0], + [0, 0], + [0, -1.542] + ], + "v": [ + [-11.633, -10.628], + [-5.258, -13.195], + [4.892, -13.243], + [11.6, -10.615], + [14, -7.097], + [14, -0.602], + [0, 14.523], + [-14, -0.602], + [-14, -7.123] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [1, 1, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { "a": 0, "k": 2.5, "ix": 5 }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [1, 1, 1], "ix": 4 }, + "o": { "a": 0, "k": 0, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [19, 19.523], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 45.0000018328876, + "st": 0, + "ct": 1, + "bm": 0 + }, + { + "ddd": 0, + "ind": 3, + "ty": 4, + "nm": "unlockoutline-bot_s1g1_s2g1_s3g1_s4g2 Outlines", + "parent": 2, + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { + "a": 1, + "k": [ + { "i": { "x": [0.667], "y": [1] }, "o": { "x": [0.333], "y": [0] }, "t": 0, "s": [0] }, + { "i": { "x": [0.667], "y": [1] }, "o": { "x": [0.333], "y": [0] }, "t": 12, "s": [9] }, + { "i": { "x": [0.667], "y": [1] }, "o": { "x": [0.333], "y": [0] }, "t": 28, "s": [9] }, + { "t": 40.0000016292334, "s": [0] } + ], + "ix": 10 + }, + "p": { "a": 0, "k": [17.628, 0.002, 0], "ix": 2, "l": 2 }, + "a": { "a": 0, "k": [15.029, 13.585, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [ + { + "i": [ + [0, 0], + [5.305, -0.847], + [-0.847, -5.305], + [0, 0] + ], + "o": [ + [-0.847, -5.305], + [-5.305, 0.846], + [0, 0], + [0, 0] + ], + "v": [ + [10.029, 0.334], + [-1.11, -7.738], + [-9.182, 3.401], + [-8.355, 8.585] + ], + "c": false + } + ] + }, + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 12, + "s": [ + { + "i": [ + [0, 0], + [5.341, -0.581], + [-0.582, -5.341], + [0, 0] + ], + "o": [ + [-0.582, -5.341], + [-5.341, 0.581], + [0, 0], + [0, 0] + ], + "v": [ + [11.833, 5.021], + [1.11, -3.596], + [-7.507, 7.127], + [-7.094, 9.966] + ], + "c": false + } + ] + }, + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 28, + "s": [ + { + "i": [ + [0, 0], + [5.341, -0.581], + [-0.582, -5.341], + [0, 0] + ], + "o": [ + [-0.582, -5.341], + [-5.341, 0.581], + [0, 0], + [0, 0] + ], + "v": [ + [11.833, 5.021], + [1.11, -3.596], + [-7.507, 7.127], + [-7.094, 9.966] + ], + "c": false + } + ] + }, + { + "t": 40.0000016292334, + "s": [ + { + "i": [ + [0, 0], + [5.305, -0.847], + [-0.847, -5.305], + [0, 0] + ], + "o": [ + [-0.847, -5.305], + [-5.305, 0.846], + [0, 0], + [0, 0] + ], + "v": [ + [10.029, 0.334], + [-1.11, -7.738], + [-9.182, 3.401], + [-8.355, 8.585] + ], + "c": false + } + ] + } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [1, 1, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { "a": 0, "k": 2.5, "ix": 5 }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [15.029, 13.585], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 45.0000018328876, + "st": 0, + "ct": 1, + "bm": 0 + } + ], + "markers": [], + "props": {} +} diff --git a/frontend/public/lotties/user.json b/frontend/public/lotties/user.json new file mode 100644 index 000000000..7a58c45f8 --- /dev/null +++ b/frontend/public/lotties/user.json @@ -0,0 +1 @@ +{"v":"5.7.5","fr":100,"ip":0,"op":150,"w":500,"h":500,"nm":"Comp 1","ddd":0,"metadata":{},"assets":[{"id":"0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Body","sr":1,"ks":{"p":{"a":0,"k":[-399,-399],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":"Body","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[-207.06,75.05],[0,-75.06],[207.06,75.05],[207.06,75.06]],"i":[[0,0],[-96.65,0],[-28.46,-87.15],[0,0]],"o":[[28.46,-87.15],[96.65,0],[0,0],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":40,"ix":2},"lc":1,"lj":1,"ml":4},{"ty":"tr","p":{"a":1,"k":[{"t":70,"s":[750,957.834],"i":{"x":[0.27],"y":[1]},"o":{"x":[0.68],"y":[0]}},{"t":130,"s":[1277,957.834],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":151,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Layer 2 - box","sr":1,"ks":{"p":{"a":0,"k":[-399,-399],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"rc","d":1,"s":{"a":0,"k":[702,702],"ix":2},"p":{"a":0,"k":[750,750],"ix":2},"r":{"a":0,"k":0,"ix":2}},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":0,"ix":2},"r":1,"bm":0}],"ip":0,"op":151,"st":0,"bm":0}]},{"id":"1","layers":[{"ddd":0,"ind":3,"ty":4,"nm":"Layer 2","sr":1,"ks":{"p":{"a":0,"k":[-399,-399],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"td":1,"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"el","d":1,"s":{"a":0,"k":[701.102,701.102],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[750,750],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":151,"st":0,"bm":0},{"ddd":0,"refId":"0","w":702,"h":702,"ind":4,"ty":0,"nm":"Body (Masked)","sr":1,"ks":{"p":{"a":0,"k":[750,750],"ix":2},"a":{"a":0,"k":[750,750],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":151,"st":0,"bm":0,"tt":1}]},{"id":"2","layers":[{"ddd":0,"ind":5,"ty":4,"nm":"Head","sr":1,"ks":{"p":{"a":0,"k":[-399,-399],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":"Head","it":[{"ty":"el","d":1,"s":{"a":0,"k":[238.478,238.478],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":40,"ix":2},"lc":1,"lj":1,"ml":4},{"ty":"tr","p":{"a":1,"k":[{"t":39,"s":[750,702.735],"i":{"x":[0.27],"y":[1]},"o":{"x":[0.68],"y":[0]}},{"t":101,"s":[1258,702.735],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":151,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Layer 1 - box","sr":1,"ks":{"p":{"a":0,"k":[-399,-399],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"rc","d":1,"s":{"a":0,"k":[702,702],"ix":2},"p":{"a":0,"k":[750,750],"ix":2},"r":{"a":0,"k":0,"ix":2}},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":0,"ix":2},"r":1,"bm":0}],"ip":0,"op":151,"st":0,"bm":0}]},{"id":"3","layers":[{"ddd":0,"ind":7,"ty":4,"nm":"Layer 1","sr":1,"ks":{"p":{"a":0,"k":[-399,-399],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"td":1,"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"el","d":1,"s":{"a":0,"k":[701.102,701.102],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[750,750],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":151,"st":0,"bm":0},{"ddd":0,"refId":"2","w":702,"h":702,"ind":8,"ty":0,"nm":"Head (Masked)","sr":1,"ks":{"p":{"a":0,"k":[750,750],"ix":2},"a":{"a":0,"k":[750,750],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":151,"st":0,"bm":0,"tt":1}]}],"layers":[{"ddd":0,"ind":12345679,"ty":4,"nm":"Group Layer 8","sr":1,"ks":{"p":{"a":0,"k":[365,464.38524590163934,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[51.229508196721305,51.229508196721305,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[220.741,37.184],[225.501,35.896],[228.749,32.36800000000001],[229.981,27.216000000000008],[228.749,22.12],[225.501,18.592],[220.741,17.304],[215.981,18.592],[212.677,22.12],[211.501,27.216000000000008],[212.677,32.36800000000001],[215.981,35.896],[220.741,37.184],[220.741,37.184],[220.741,37.184]],"i":[[0,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.382000000000062,0.8586999999999989],[1.79200000000003,0],[1.418999999999983,-0.8586999999999989],[0.8220000000000027,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.382000000000062,-0.8586999999999989],[0.8220000000000027,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.418999999999983,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[221.357,43.06400000000001],[214.917,41.608],[210.49300000000005,37.408],[211.221,36.232],[211.221,42.392],[205.173,42.392],[205.173,0],[211.501,0],[211.501,18.36800000000001],[210.49300000000005,16.912000000000006],[214.973,12.88],[221.357,11.424000000000007],[229.085,13.49600000000001],[234.51700000000005,19.152],[236.533,27.216000000000008],[234.51700000000005,35.28],[229.141,40.992],[221.357,43.06400000000001],[221.357,43.06400000000001],[221.357,43.06400000000001]],"i":[[0,0],[1.942000000000007,0.9706999999999937],[1.045999999999935,1.829300000000003],[-0.2426666666666506,0.3919999999999959],[0,-2.053333333333327],[2.015999999999963,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.122666666666674],[0.3360000000000127,0.4853333333333296],[-1.865999999999985,0.9707000000000079],[-2.38900000000001,0],[-2.277000000000044,-1.3813000000000102],[-1.30600000000004,-2.389300000000006],[0,-2.986699999999999],[1.343999999999937,-2.389300000000006],[2.27800000000002,-1.4187000000000012],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.351999999999975,0],[-1.903999999999996,-0.9707000000000079],[0.2426666666666506,-0.3919999999999959],[0,2.053333333333327],[-2.015999999999963,0],[0,-14.13066666666667],[2.109333333333325,0],[0,6.122666666666667],[-0.3360000000000127,-0.4853333333333296],[1.120000000000005,-1.717300000000009],[1.867000000000075,-0.9706999999999937],[2.875,0],[2.314999999999941,1.381299999999996],[1.343999999999937,2.389300000000006],[0,2.986699999999999],[-1.30600000000004,2.389300000000006],[-2.27699999999993,1.381299999999996],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[181.87,43.06400000000001],[176.438,42],[172.854,38.976],[171.566,34.384],[172.63,29.960000000000008],[176.046,26.656000000000006],[181.814,24.752],[192.342,23.016000000000005],[192.342,28],[183.046,29.624],[179.35,31.248],[178.174,34.16],[179.462,37.016000000000005],[182.878,38.08],[187.35799999999995,36.96000000000001],[190.38199999999995,33.992],[191.446,29.792],[191.446,22.008],[189.766,18.36800000000001],[185.398,16.912000000000006],[180.974,18.256],[178.23,21.616],[172.966,18.98400000000001],[175.71,15.064000000000007],[180.134,12.376],[185.566,11.424000000000007],[191.894,12.768],[196.206,16.52],[197.774,22.008],[197.774,42.392],[191.726,42.392],[191.726,36.904],[193.014,37.072],[190.27,40.264],[186.518,42.336],[181.87,43.06400000000001],[181.87,43.06400000000001],[181.87,43.06400000000001]],"i":[[0,0],[1.567999999999984,0.7092999999999989],[0.8589999999999236,1.2693000000000012],[0,1.7547],[-0.7089999999999463,1.306699999999992],[-1.530000000000086,0.8960000000000008],[-2.313999999999965,0.3733000000000004],[-3.509333333333302,0.5786666666666633],[0,-1.661333333333332],[3.098666666666645,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8579999999999472,-0.7467000000000041],[-1.381000000000085,0],[-1.268999999999892,0.7466999999999899],[-0.7089999999999463,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829999999999927,0],[1.269999999999982,-0.8960000000000008],[0.5979999999999563,-1.381299999999996],[1.754666666666708,0.8773333333333255],[-1.269000000000005,1.11999999999999],[-1.680000000000064,0.6346999999999952],[-1.903999999999996,0],[-1.828999999999951,-0.8960000000000008],[-1.008000000000038,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999963,0],[0,1.829333333333338],[-0.4293333333333749,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.717999999999961,0],[0,0],[0,0]],"o":[[-2.052999999999997,0],[-1.529999999999973,-0.7467000000000041],[-0.8580000000000609,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999573,-1.306700000000006],[1.530999999999949,-0.8960000000000008],[3.509333333333302,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666645,0.541333333333327],[-1.680000000000064,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.8959999999999582,0.7092999999999989],[1.717999999999961,0],[1.307000000000016,-0.7467000000000041],[0.7100000000000364,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.081999999999994,-0.9707000000000079],[-1.680000000000064,0],[-1.232000000000085,0.8586999999999989],[-1.754666666666708,-0.8773333333333255],[0.5599999999999454,-1.493300000000005],[1.269999999999982,-1.157300000000006],[1.717999999999961,-0.6347000000000094],[2.389999999999986,0],[1.866999999999962,0.8960000000000008],[1.045999999999935,1.568000000000012],[0,6.794666666666672],[-2.015999999999963,0],[0,-1.829333333333338],[0.4293333333333749,0.05599999999999739],[-0.70900000000006,1.2319999999999993],[-1.081999999999994,0.8960000000000008],[-1.380999999999972,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[159.072,42.392],[159.072,0],[165.4,0],[165.4,42.392],[159.072,42.392],[159.072,42.392],[159.072,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-14.13066666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,14.13066666666667],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[138.952,43.06400000000001],[130.888,40.992],[125.456,35.28],[123.496,27.16],[125.456,19.040000000000006],[130.832,13.49600000000001],[138.448,11.424000000000007],[144.552,12.600000000000009],[149.088,15.848],[151.888,20.49600000000001],[152.896,26.096],[152.84,27.608],[152.616,29.064000000000007],[128.48,29.064000000000007],[128.48,24.024],[149.032,24.024],[146.008,26.320000000000007],[145.616,21.448000000000008],[142.816,18.032],[138.448,16.744],[133.968,18.032],[130.944,21.616],[130.104,27.216000000000008],[130.944,32.592],[134.192,36.176],[139.008,37.464],[143.65599999999995,36.232],[146.736,33.040000000000006],[151.888,35.56],[149.088,39.42400000000001],[144.60799999999995,42.11200000000001],[138.952,43.06400000000001],[138.952,43.06400000000001],[138.952,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.307000000000016,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.836999999999989,0],[-1.79200000000003,-0.784000000000006],[-1.231999999999971,-1.381299999999996],[-0.6349999999999909,-1.754700000000014],[0,-1.978700000000003],[0.03699999999992087,-0.5227000000000004],[0.1119999999999663,-0.4480000000000075],[8.04533333333336,0],[0,1.680000000000007],[-6.850666666666712,0],[1.008000000000038,-0.7653333333333308],[0.6349999999999909,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.680000000000064,0],[1.307000000000016,-0.8586999999999989],[0.70900000000006,-1.567999999999998],[-0.1490000000000009,-2.202700000000007],[-0.7469999999999573,-1.530699999999996],[-1.380999999999972,-0.8586999999999989],[-1.79200000000003,0],[-1.268999999999892,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333386,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.755000000000109,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.315000000000055,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999937,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.277000000000044,0],[1.79200000000003,0.7839999999999918],[1.232000000000085,1.344000000000008],[0.6720000000000255,1.7547],[0,0.4852999999999952],[-0.03700000000003456,0.5227000000000004],[-8.04533333333336,0],[0,-1.680000000000007],[6.850666666666712,0],[-1.008000000000038,0.7653333333333308],[0.3729999999999336,-1.829300000000003],[-0.59699999999998,-1.456000000000003],[-1.232000000000085,-0.8586999999999989],[-1.67999999999995,0],[-1.306999999999903,0.8213000000000079],[-0.7089999999999463,1.530699999999996],[-0.1870000000000118,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418999999999983,0.8586999999999989],[1.828999999999951,0],[1.307000000000016,-0.8212999999999937],[1.717333333333386,0.8400000000000034],[-0.59699999999998,1.4187000000000012],[-1.231999999999971,1.11999999999999],[-1.716999999999985,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,7.951999999999998],[111.001,0.6720000000000041],[117.329,0.6720000000000041],[117.329,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,42.392],[111.001,12.096],[117.329,12.096],[117.329,42.392],[111.001,42.392],[111.001,42.392],[111.001,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[101.41,42.72800000000001],[94.01800000000003,40.040000000000006],[91.38599999999997,32.48],[91.38599999999997,17.808000000000007],[86.06600000000003,17.808000000000007],[86.06600000000003,12.096],[86.90599999999995,12.096],[90.21000000000004,10.864],[91.38599999999997,7.504000000000005],[91.38599999999997,5.152000000000001],[97.71400000000006,5.152000000000001],[97.71400000000006,12.096],[104.602,12.096],[104.602,17.808000000000007],[97.71400000000006,17.808000000000007],[97.71400000000006,32.2],[98.21799999999996,34.888000000000005],[99.84199999999998,36.568],[102.754,37.128],[103.76200000000006,37.072],[104.826,36.96000000000001],[104.826,42.392],[103.09,42.616],[101.41,42.72800000000001],[101.41,42.72800000000001],[101.41,42.72800000000001]],"i":[[0,0],[1.754999999999995,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333312,0],[0,1.903999999999996],[-0.2799999999999727,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999935,0],[0,-1.903999999999996],[2.295999999999935,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7469999999999573,-0.4106999999999914],[-1.19500000000005,0],[-0.3730000000000473,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6349999999999909,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333312,0],[0,-1.903999999999996],[0.2799999999999727,0],[1.419000000000096,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999935,0],[0,1.903999999999996],[-2.295999999999935,0],[0,4.797333333333327],[0,1.045299999999997],[0.3360000000000127,0.7092999999999989],[0.7470000000000709,0.3733000000000004],[0.2989999999999782,0],[0.3729999999999336,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[78.71499999999997,42.72800000000001],[71.32299999999998,40.040000000000006],[68.69100000000003,32.48],[68.69100000000003,17.808000000000007],[63.37099999999998,17.808000000000007],[63.37099999999998,12.096],[64.21100000000001,12.096],[67.51499999999999,10.864],[68.69100000000003,7.504000000000005],[68.69100000000003,5.152000000000001],[75.019,5.152000000000001],[75.019,12.096],[81.90700000000004,12.096],[81.90700000000004,17.808000000000007],[75.019,17.808000000000007],[75.019,32.2],[75.52300000000002,34.888000000000005],[77.14699999999999,36.568],[80.05900000000003,37.128],[81.06700000000001,37.072],[82.13099999999997,36.96000000000001],[82.13099999999997,42.392],[80.39499999999998,42.616],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001]],"i":[[0,0],[1.754000000000019,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7470000000000141,-0.4106999999999914],[-1.19500000000005,0],[-0.3740000000000236,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6340000000000146,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173999999999978,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418000000000006,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7460000000000377,0.3733000000000004],[0.297999999999945,0],[0.3730000000000473,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,37.184],[48.94799999999998,35.896],[52.19600000000003,32.36800000000001],[53.428,27.216000000000008],[52.19600000000003,22.12],[48.94799999999998,18.592],[44.18799999999999,17.304],[39.428,18.592],[36.12400000000002,22.12],[34.94799999999998,27.216000000000008],[36.12400000000002,32.36800000000001],[39.428,35.896],[44.18799999999999,37.184],[44.18799999999999,37.184],[44.18799999999999,37.184]],"i":[[0,0],[-1.381999999999948,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.381000000000029,0.8586999999999989],[1.79200000000003,0],[1.418000000000006,-0.8586999999999989],[0.8209999999999695,-1.493300000000005],[0,-1.904000000000011],[-0.7840000000000487,-1.5307000000000102],[-1.382000000000005,-0.8586999999999989],[-1.754999999999995,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.381000000000029,-0.8586999999999989],[0.8209999999999695,-1.5307000000000102],[0,-1.904000000000011],[-0.7840000000000487,-1.493300000000005],[-1.381999999999948,-0.8586999999999989],[-1.754999999999995,0],[-1.382000000000005,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.418000000000006,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,43.06400000000001],[36.18000000000001,40.992],[30.46800000000002,35.336],[28.33999999999997,27.216000000000008],[30.46800000000002,19.096],[36.18000000000001,13.49600000000001],[44.18799999999999,11.424000000000007],[52.19600000000003,13.49600000000001],[57.85199999999998,19.096],[59.98000000000002,27.216000000000008],[57.85199999999998,35.392],[52.139999999999986,41.048],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001]],"i":[[0,0],[2.425999999999988,1.381299999999996],[1.418000000000006,2.389300000000006],[0,3.024000000000001],[-1.41900000000004,2.352000000000004],[-2.389999999999986,1.343999999999994],[-2.949999999999989,0],[-2.352000000000032,-1.3813000000000102],[-1.381999999999948,-2.389300000000006],[0,-3.061300000000003],[1.418000000000006,-2.389299999999992],[2.38900000000001,-1.381299999999996],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.911999999999978,0],[-2.389999999999986,-1.381299999999996],[-1.41900000000004,-2.389299999999992],[0,-3.061300000000003],[1.418000000000006,-2.389300000000006],[2.38900000000001,-1.3813000000000102],[2.98599999999999,0],[2.388999999999953,1.343999999999994],[1.418000000000006,2.352000000000004],[0,3.061299999999989],[-1.418999999999983,2.389300000000006],[-2.389999999999986,1.343999999999994],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.608000000000004,0.6720000000000041],[6.608000000000004,36.512],[24.639999999999986,36.512],[24.639999999999986,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.202666666666687,0],[0,-11.94666666666666],[-6.01066666666668,0],[0,-1.959999999999994],[8.21333333333331,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.202666666666687,0],[0,11.94666666666667],[6.01066666666668,0],[0,1.959999999999994],[-8.21333333333331,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[98.08047485351562,-21.67217254638672],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[246.681,42.392],[246.681,0],[253.009,0],[253.009,18.032],[252.001,17.248],[255.585,12.936000000000007],[261.297,11.424000000000007],[267.23299999999995,12.88],[271.265,16.912000000000006],[272.721,22.792],[272.721,42.392],[266.449,42.392],[266.449,24.528000000000006],[265.553,20.664],[263.201,18.2],[259.729,17.304],[256.25699999999995,18.2],[253.849,20.664],[253.009,24.528000000000006],[253.009,42.392],[246.681,42.392],[246.681,42.392],[246.681,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.010666666666665],[0.3360000000000127,0.2613333333333259],[-1.643000000000029,0.9706999999999937],[-2.166000000000054,0],[-1.717999999999961,-0.9706999999999937],[-0.9710000000000036,-1.717300000000009],[0,-2.202699999999993],[0,-6.533333333333331],[2.090666666666721,0],[0,5.954666666666668],[0.59699999999998,1.045299999999997],[1.007999999999925,0.5600000000000023],[1.305999999999926,0],[1.045000000000073,-0.5973000000000042],[0.59699999999998,-1.082700000000003],[0,-1.493300000000005],[0,-5.954666666666668],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,6.010666666666665],[-0.3360000000000127,-0.2613333333333259],[0.7459999999999809,-1.903999999999996],[1.641999999999967,-1.0080000000000098],[2.240000000000009,0],[1.717000000000098,0.9707000000000079],[0.9700000000000273,1.717299999999994],[0,6.533333333333331],[-2.090666666666721,0],[0,-5.954666666666668],[0,-1.5307000000000102],[-0.5599999999999454,-1.082700000000003],[-1.008000000000038,-0.5973000000000042],[-1.270000000000095,0],[-1.007999999999953,0.5600000000000023],[-0.5600000000000023,1.082700000000003],[0,5.954666666666668],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[237.089,42.72800000000001],[229.697,40.040000000000006],[227.065,32.48],[227.065,17.808000000000007],[221.745,17.808000000000007],[221.745,12.096],[222.585,12.096],[225.889,10.864],[227.065,7.504000000000005],[227.065,5.152000000000001],[233.393,5.152000000000001],[233.393,12.096],[240.281,12.096],[240.281,17.808000000000007],[233.393,17.808000000000007],[233.393,32.2],[233.897,34.888000000000005],[235.521,36.568],[238.433,37.128],[239.441,37.072],[240.505,36.96000000000001],[240.505,42.392],[238.769,42.616],[237.089,42.72800000000001],[237.089,42.72800000000001],[237.089,42.72800000000001]],"i":[[0,0],[1.755000000000052,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7459999999999809,-0.4106999999999914],[-1.194000000000017,0],[-0.3729999999999905,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6350000000000477,-0.07469999999999288],[0.48599999999999,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.753999999999962,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418999999999983,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7470000000000141,0.3733000000000004],[0.2989999999999782,0],[0.3740000000000236,-0.03730000000000189],[0,1.810666666666663],[-0.5220000000000482,0.0747000000000071],[-0.6339999999999577,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,7.951999999999998],[210.259,0.6720000000000041],[216.587,0.6720000000000041],[216.587,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,42.392],[210.259,12.096],[216.587,12.096],[216.587,42.392],[210.259,42.392],[210.259,42.392],[210.259,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[169.688,42.392],[159.272,12.096],[165.992,12.096],[173.944,36.232],[171.592,36.232],[179.712,12.096],[185.48,12.096],[193.544,36.232],[191.192,36.232],[199.2,12.096],[205.92,12.096],[195.448,42.392],[189.736,42.392],[181.56,17.696],[183.632,17.696],[175.456,42.392],[169.688,42.392],[169.688,42.392],[169.688,42.392]],"i":[[0,0],[3.47199999999998,10.09866666666667],[-2.240000000000009,0],[-2.650666666666666,-8.045333333333332],[0.7839999999999918,0],[-2.706666666666649,8.045333333333332],[-1.922666666666657,0],[-2.687999999999988,-8.045333333333332],[0.7839999999999918,0],[-2.669333333333327,8.045333333333332],[-2.240000000000009,0],[3.490666666666641,-10.09866666666667],[1.903999999999996,0],[2.725333333333367,8.232],[-0.6906666666666865,0],[2.72533333333331,-8.232],[1.922666666666657,0],[0,0],[0,0]],"o":[[-3.47199999999998,-10.09866666666666],[2.240000000000009,0],[2.650666666666666,8.045333333333332],[-0.7839999999999918,0],[2.706666666666649,-8.045333333333332],[1.922666666666657,0],[2.687999999999988,8.045333333333332],[-0.7839999999999918,0],[2.669333333333327,-8.045333333333332],[2.240000000000009,0],[-3.490666666666641,10.09866666666666],[-1.903999999999996,0],[-2.725333333333367,-8.232],[0.6906666666666865,0],[-2.72533333333331,8.232],[-1.922666666666657,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[155.86146545410156,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[132.444,43.06400000000001],[124.38,40.992],[118.948,35.28],[116.988,27.16],[118.948,19.040000000000006],[124.324,13.49600000000001],[131.94,11.424000000000007],[138.044,12.600000000000009],[142.58,15.848],[145.38,20.49600000000001],[146.388,26.096],[146.332,27.608],[146.108,29.064000000000007],[121.972,29.064000000000007],[121.972,24.024],[142.524,24.024],[139.5,26.320000000000007],[139.108,21.448000000000008],[136.308,18.032],[131.94,16.744],[127.46,18.032],[124.436,21.616],[123.596,27.216000000000008],[124.436,32.592],[127.684,36.176],[132.5,37.464],[137.148,36.232],[140.228,33.040000000000006],[145.38,35.56],[142.58,39.42400000000001],[138.1,42.11200000000001],[132.444,43.06400000000001],[132.444,43.06400000000001],[132.444,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.305999999999983,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.838000000000022,0],[-1.79200000000003,-0.784000000000006],[-1.232000000000028,-1.381299999999996],[-0.6350000000000477,-1.754700000000014],[0,-1.978700000000003],[0.03699999999997772,-0.5227000000000004],[0.1120000000000232,-0.4480000000000075],[8.045333333333303,0],[0,1.680000000000007],[-6.850666666666655,0],[1.007999999999981,-0.7653333333333308],[0.6340000000000146,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.67999999999995,0],[1.305999999999983,-0.8586999999999989],[0.7090000000000032,-1.567999999999998],[-0.1499999999999773,-2.202700000000007],[-0.7470000000000141,-1.530699999999996],[-1.382000000000005,-0.8586999999999989],[-1.791999999999973,0],[-1.269999999999982,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333329,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.754000000000019,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.314999999999998,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999994,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.276999999999987,0],[1.791999999999973,0.7839999999999918],[1.231999999999971,1.344000000000008],[0.6719999999999686,1.7547],[0,0.4852999999999952],[-0.03800000000001091,0.5227000000000004],[-8.045333333333303,0],[0,-1.680000000000007],[6.850666666666655,0],[-1.007999999999981,0.7653333333333308],[0.3730000000000473,-1.829300000000003],[-0.5979999999999563,-1.456000000000003],[-1.232000000000028,-0.8586999999999989],[-1.680000000000007,0],[-1.307000000000016,0.8213000000000079],[-0.7100000000000364,1.530699999999996],[-0.186999999999955,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418000000000006,0.8586999999999989],[1.829000000000008,0],[1.305999999999983,-0.8212999999999937],[1.717333333333329,0.8400000000000034],[-0.5980000000000132,1.4187000000000012],[-1.232000000000028,1.11999999999999],[-1.718000000000018,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[95.32,37.184],[100.024,35.896],[103.328,32.36800000000001],[104.56,27.216000000000008],[103.328,22.12],[100.024,18.592],[95.32,17.304],[90.56,18.592],[87.256,22.12],[86.08000000000001,27.216000000000008],[87.256,32.36800000000001],[90.50399999999999,35.896],[95.32,37.184],[95.32,37.184],[95.32,37.184]],"i":[[0,0],[-1.381,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.820999999999998,1.493299999999991],[1.419000000000011,0.8586999999999989],[1.754999999999995,0],[1.418999999999983,-0.8586999999999989],[0.7839999999999918,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.381,-0.8586999999999989],[-1.792000000000002,0],[0,0],[0,0]],"o":[[1.754999999999995,0],[1.419000000000011,-0.8586999999999989],[0.820999999999998,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.381,-0.8586999999999989],[-1.754999999999995,0],[-1.419000000000011,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.7839999999999918,1.493299999999991],[1.419000000000011,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[94.70400000000001,43.06400000000001],[86.864,40.992],[81.43199999999999,35.28],[79.47200000000001,27.216000000000008],[81.488,19.152],[86.91999999999999,13.49600000000001],[94.648,11.424000000000007],[101.088,12.88],[105.512,16.912000000000006],[104.56,18.36800000000001],[104.56,0],[110.832,0],[110.832,42.392],[104.84,42.392],[104.84,36.232],[105.568,37.408],[101.088,41.608],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001]],"i":[[0,0],[2.314999999999998,1.381299999999996],[1.344000000000023,2.389300000000006],[0,2.986699999999999],[-1.343999999999994,2.389300000000006],[-2.276999999999987,1.381299999999996],[-2.875,0],[-1.8669999999999902,-0.9706999999999937],[-1.082999999999998,-1.717300000000009],[0.3173333333333233,-0.4853333333333296],[0,6.122666666666674],[-2.090666666666664,0],[0,-14.13066666666667],[1.99733333333333,0],[0,2.053333333333327],[-0.242666666666679,-0.3919999999999959],[1.941000000000003,-0.9707000000000079],[2.314999999999998,0],[0,0],[0,0]],"o":[[-2.912000000000006,0],[-2.277000000000015,-1.4187000000000012],[-1.306999999999988,-2.389300000000006],[0,-2.986699999999999],[1.343999999999994,-2.389300000000006],[2.277000000000015,-1.3813000000000102],[2.426999999999992,0],[1.867000000000019,0.9707000000000079],[-0.3173333333333233,0.4853333333333296],[0,-6.122666666666674],[2.090666666666664,0],[0,14.13066666666667],[-1.99733333333333,0],[0,-2.053333333333327],[0.242666666666679,0.3919999999999959],[-1.045000000000016,1.829300000000003],[-1.941000000000003,0.9706999999999937],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[57.40100000000001,43.06400000000001],[51.968999999999994,42],[48.38499999999999,38.976],[47.09700000000001,34.384],[48.161,29.960000000000008],[51.577,26.656000000000006],[57.345,24.752],[67.87299999999999,23.016000000000005],[67.87299999999999,28],[58.577,29.624],[54.881,31.248],[53.70500000000001,34.16],[54.992999999999995,37.016000000000005],[58.40899999999999,38.08],[62.88900000000001,36.96000000000001],[65.91300000000001,33.992],[66.977,29.792],[66.977,22.008],[65.297,18.36800000000001],[60.929,16.912000000000006],[56.505,18.256],[53.761,21.616],[48.496999999999986,18.98400000000001],[51.240999999999985,15.064000000000007],[55.66499999999999,12.376],[61.09700000000001,11.424000000000007],[67.42500000000001,12.768],[71.737,16.52],[73.305,22.008],[73.305,42.392],[67.257,42.392],[67.257,36.904],[68.54499999999999,37.072],[65.80099999999999,40.264],[62.04900000000001,42.336],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001]],"i":[[0,0],[1.568000000000012,0.7092999999999989],[0.8590000000000089,1.2693000000000012],[0,1.7547],[-0.7090000000000032,1.306699999999992],[-1.531000000000006,0.8960000000000008],[-2.314999999999998,0.3733000000000004],[-3.509333333333331,0.5786666666666633],[0,-1.661333333333332],[3.098666666666674,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8590000000000089,-0.7467000000000041],[-1.381,0],[-1.269000000000005,0.7466999999999899],[-0.7090000000000032,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829000000000008,0],[1.269000000000005,-0.8960000000000008],[0.5970000000000084,-1.381299999999996],[1.754666666666679,0.8773333333333255],[-1.268999999999977,1.11999999999999],[-1.680000000000007,0.6346999999999952],[-1.903999999999996,0],[-1.829000000000008,-0.8960000000000008],[-1.0080000000000098,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999991,0],[0,1.829333333333338],[-0.429333333333318,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.716999999999985,0],[0,0],[0,0]],"o":[[-2.053000000000026,0],[-1.531000000000006,-0.7467000000000041],[-0.8589999999999804,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999857,-1.306700000000006],[1.531000000000006,-0.8960000000000008],[3.509333333333331,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666674,0.541333333333327],[-1.680000000000007,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.896000000000015,0.7092999999999989],[1.717000000000013,0],[1.306999999999988,-0.7467000000000041],[0.7089999999999748,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.082999999999998,-0.9707000000000079],[-1.680000000000007,0],[-1.2319999999999993,0.8586999999999989],[-1.754666666666679,-0.8773333333333255],[0.5600000000000023,-1.493300000000005],[1.269000000000005,-1.157300000000006],[1.717000000000013,-0.6347000000000094],[2.388999999999982,0],[1.86699999999999,0.8960000000000008],[1.045000000000016,1.568000000000012],[0,6.794666666666672],[-2.015999999999991,0],[0,-1.829333333333338],[0.429333333333318,0.05599999999999739],[-0.7089999999999748,1.2319999999999993],[-1.082999999999998,0.8960000000000008],[-1.381,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.159999999999997,0.6720000000000041],[21.84,22.400000000000006],[18.75999999999999,22.400000000000006],[34.16,0.6720000000000041],[40.31999999999999,0.6720000000000041],[40.31999999999999,42.392],[33.768,42.392],[33.768,8.456000000000003],[36.232,9.128],[20.49600000000001,30.632000000000005],[19.824000000000012,30.632000000000005],[4.424000000000007,9.128],[6.608000000000004,8.456000000000003],[6.608000000000004,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.053333333333342,0],[-5.226666666666659,-7.242666666666665],[1.026666666666671,0],[-5.133333333333326,7.242666666666672],[-2.053333333333342,0],[0,-13.90666666666667],[2.183999999999997,0],[0,11.312],[-0.8213333333333424,-0.2240000000000038],[5.245333333333321,-7.168000000000006],[0.2239999999999895,0],[5.133333333333326,7.168000000000006],[-0.7280000000000086,0.2240000000000038],[0,-11.312],[2.202666666666659,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.053333333333342,0],[5.226666666666659,7.242666666666672],[-1.026666666666671,0],[5.133333333333326,-7.242666666666665],[2.053333333333342,0],[0,13.90666666666666],[-2.183999999999997,0],[0,-11.312],[0.8213333333333424,0.2240000000000038],[-5.245333333333321,7.168000000000006],[-0.2239999999999895,0],[-5.133333333333326,-7.168000000000006],[0.7280000000000086,-0.2240000000000038],[0,11.312],[-2.202666666666659,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[2.91259765625,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[702.6863719370097,144],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":72,"ix":2}},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[56.54167175292969,-0.000022762338630855083],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":80,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[122.0000003294881,25.00000012138912],"ix":2},"a":{"a":0,"k":[56.54167175292969,-0.00002288818359375],"ix":2},"s":{"a":0,"k":[34.403572049765366,34.403572049765366],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":151,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":1,"k":[{"t":70,"s":[-202.21206665039062,-204.2413330078125],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":81,"s":[-201.96864318847656,-195.74130249023438],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[60.26248335838318,60.53290367126465],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":151,"st":0,"bm":0},{"ddd":0,"refId":"1","w":702,"h":702,"ind":4,"ty":0,"nm":"Body (Masked)","sr":1,"ks":{"p":{"a":0,"k":[399,399],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":151,"st":0,"bm":0,"parent":9},{"ddd":0,"ind":10,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":1,"k":[{"t":39,"s":[-202.21206665039062,-204.2413330078125],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":81,"s":[-201.96864318847656,-195.74130249023438],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[60.26248335838318,60.53290367126465],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":151,"st":0,"bm":0},{"ddd":0,"refId":"3","w":702,"h":702,"ind":8,"ty":0,"nm":"Head (Masked)","sr":1,"ks":{"p":{"a":0,"k":[399,399],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":151,"st":0,"bm":0,"parent":10},{"ddd":0,"ind":11,"ty":4,"nm":"Circle","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":"Circle","it":[{"ty":"el","d":1,"s":{"a":0,"k":[422.5014900854009,424.39742128057475],"ix":2},"p":{"a":0,"k":[0,0],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":21,"ix":2},"lc":1,"lj":1,"ml":4},{"ty":"tr","p":{"a":1,"k":[{"t":0,"s":[249.75657653808594,233.2445068359375],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":81,"s":[250,241.74453735351562],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":151,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/frontend/public/lotties/verified.json b/frontend/public/lotties/verified.json new file mode 100644 index 000000000..9815ab2d9 --- /dev/null +++ b/frontend/public/lotties/verified.json @@ -0,0 +1 @@ +{"v":"5.7.5","fr":100,"ip":0,"op":102,"w":500,"h":500,"nm":"Comp 1","ddd":0,"metadata":{},"assets":[{"id":"0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"1","w":378,"h":377,"ind":2,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[61,61],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":100,"s":[100],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":1},{"ddd":0,"ind":3,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"2","w":378,"h":377,"ind":4,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[61,61],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"h":1},{"t":2,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":3},{"ddd":0,"ind":5,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"3","w":505,"h":503,"ind":6,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[-3,-2],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":100,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":5},{"ddd":0,"ind":7,"ty":3,"nm":".primary.design (Group)","sr":1,"ks":{"p":{"a":0,"k":[249.998,250.004],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":1,"k":[{"t":2,"s":[0],"i":{"x":[0.131],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":50,"s":[27],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.628],"y":[0]}},{"t":77,"s":[-11],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":100,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":7},{"ddd":0,"refId":"4","w":412,"h":342,"ind":9,"ty":0,"nm":".primary.design (In/Out)","sr":1,"ks":{"p":{"a":0,"k":[-181,-186],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":2,"s":[100],"h":1},{"t":100,"s":[0],"h":1}],"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":8}]},{"id":"1","layers":[{"ddd":0,"ind":10,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-61,-61],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-69.803,-8.539],[-91.936,-8.526],[-91.922,13.607],[-39.704,65.762],[-28.644,70.339],[-17.584,65.762],[91.922,-43.616],[91.936,-65.749],[69.803,-65.762],[-28.644,32.57],[-69.803,-8.539]],"i":[[0,0],[6.109,-6.116],[-6.115,-6.108],[0,0],[-4.002,0],[-3.055,3.051],[0,0],[6.107,6.115],[6.115,-6.107],[0,0],[0,0]],"o":[[-6.115,-6.107],[-6.107,6.116],[0,0],[3.056,3.052],[4.002,0],[0,0],[6.115,-6.108],[-6.109,-6.116],[0,0],[0,0],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[263.158,242.187],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","nm":"Group 2","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[149.277,-67.68],[15.49,143.298],[0,153.266],[-15.509,143.287],[-149.274,-67.655],[-154.479,-107.449],[0,-153.936],[154.479,-107.449],[149.277,-67.68]],"i":[[0,0],[73.521,-47.165],[0,0],[0,0],[11.506,86.895],[0,0],[-39.84,20.453],[-33.146,-7.159],[0,0]],"o":[[-11.51,86.919],[0,0],[0,0],[-73.503,-47.153],[0,0],[33.146,-7.159],[39.84,20.453],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[174.946,-135.138],[8.294,-185.147],[-8.294,-185.147],[-174.946,-135.138],[-187.394,-117.763],[-180.307,-63.572],[-32.428,169.62],[-8.469,185.036],[0,187.526],[8.469,185.036],[32.409,169.632],[180.31,-63.596],[187.394,-117.763],[174.946,-135.138]],"i":[[0,0],[32.173,20.107],[5.074,-3.172],[35.445,-7.092],[-1.068,-8.164],[0,0],[-81.252,-52.124],[0,0],[-2.944,0],[-2.579,1.66],[0,0],[-12.724,96.093],[0,0],[8.073,1.615]],"o":[[-35.445,-7.092],[-5.074,-3.172],[-32.173,20.107],[-8.073,1.615],[0,0],[12.72,96.068],[0,0],[2.579,1.66],[2.944,0],[0,0],[81.27,-52.136],[0,0],[1.068,-8.164],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[250.004,250.003],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"2","layers":[{"ddd":0,"ind":11,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[-61,-61],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","nm":"Group 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-69.803,-8.539],[-91.936,-8.526],[-91.922,13.607],[-39.704,65.762],[-28.644,70.339],[-17.584,65.762],[91.922,-43.616],[91.936,-65.749],[69.803,-65.762],[-28.644,32.57],[-69.803,-8.539]],"i":[[0,0],[6.109,-6.116],[-6.115,-6.108],[0,0],[-4.002,0],[-3.055,3.051],[0,0],[6.107,6.115],[6.115,-6.107],[0,0],[0,0]],"o":[[-6.115,-6.107],[-6.107,6.116],[0,0],[3.056,3.052],[4.002,0],[0,0],[6.115,-6.108],[-6.109,-6.116],[0,0],[0,0],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[263.158,242.187],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","nm":"Group 2","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[149.277,-67.68],[15.49,143.298],[0,153.266],[-15.509,143.287],[-149.274,-67.655],[-154.479,-107.449],[0,-153.936],[154.479,-107.449],[149.277,-67.68]],"i":[[0,0],[73.521,-47.165],[0,0],[0,0],[11.506,86.895],[0,0],[-39.84,20.453],[-33.146,-7.159],[0,0]],"o":[[-11.51,86.919],[0,0],[0,0],[-73.503,-47.153],[0,0],[33.146,-7.159],[39.84,20.453],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[174.946,-135.138],[8.294,-185.147],[-8.294,-185.147],[-174.946,-135.138],[-187.394,-117.763],[-180.307,-63.572],[-32.428,169.62],[-8.469,185.036],[0,187.526],[8.469,185.036],[32.409,169.632],[180.31,-63.596],[187.394,-117.763],[174.946,-135.138]],"i":[[0,0],[32.173,20.107],[5.074,-3.172],[35.445,-7.092],[-1.068,-8.164],[0,0],[-81.252,-52.124],[0,0],[-2.944,0],[-2.579,1.66],[0,0],[-12.724,96.093],[0,0],[8.073,1.615]],"o":[[-35.445,-7.092],[-5.074,-3.172],[-32.173,20.107],[-8.073,1.615],[0,0],[12.72,96.068],[0,0],[2.579,1.66],[2.944,0],[0,0],[81.27,-52.136],[0,0],[1.068,-8.164],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[250.004,250.003],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"3","layers":[{"ddd":0,"ind":12,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[3,2],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":2,"s":[{"c":true,"v":[[171.875,-119.795],[164.746,-65.6],[23.905,156.51],[0.007,171.873],[0,171.875],[-23.904,156.508],[-164.743,-65.592],[-171.875,-119.795],[0,-171.875],[171.875,-119.795]],"i":[[0,0],[0,0],[77.333,-49.716],[0,0],[0,0],[0,0],[11.993,91.145],[0,0],[-41.666,26.039],[0,0]],"o":[[0,0],[-11.99,91.149],[0,0],[0,0],[0,0],[-77.33,-49.714],[0,0],[0,0],[41.667,26.039],[0,0]]}],"i":{"x":[0.131],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":35,"s":[{"c":true,"v":[[191.412,-122.573],[182.245,-52.889],[1.15,232.704],[-29.579,252.458],[-29.588,252.46],[-60.324,232.701],[-241.417,-52.879],[-250.588,-122.573],[-29.588,-189.539],[191.412,-122.573]],"i":[[0,0],[0,0],[99.436,-63.925],[0,0],[0,0],[0,0],[15.421,117.196],[0,0],[-53.575,33.481],[0,0]],"o":[[0,0],[-15.417,117.201],[0,0],[0,0],[0,0],[-99.432,-63.922],[0,0],[0,0],[53.576,33.481],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.628],"y":[0]}},{"t":70,"s":[{"c":true,"v":[[125.996,-87.818],[120.77,-48.09],[17.524,114.733],[0.005,125.995],[0,125.996],[-17.523,114.731],[-120.768,-48.083],[-125.996,-87.818],[0,-125.996],[125.996,-87.818]],"i":[[0,0],[0,0],[56.69,-36.445],[0,0],[0,0],[0,0],[8.792,66.816],[0,0],[-30.544,19.088],[0,0]],"o":[[0,0],[-8.79,66.819],[0,0],[0,0],[0,0],[-56.688,-36.443],[0,0],[0,0],[30.545,19.088],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":100,"s":[{"c":true,"v":[[171.875,-119.795],[164.746,-65.6],[23.905,156.51],[0.007,171.873],[0,171.875],[-23.904,156.508],[-164.743,-65.592],[-171.875,-119.795],[0,-171.875],[171.875,-119.795]],"i":[[0,0],[0,0],[77.333,-49.716],[0,0],[0,0],[0,0],[11.993,91.145],[0,0],[-41.666,26.039],[0,0]],"o":[[0,0],[-11.99,91.149],[0,0],[0,0],[0,0],[-77.33,-49.714],[0,0],[0,0],[41.667,26.039],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[249.998,250.004],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":1,"k":[{"t":2,"s":[0],"i":{"x":[0.131],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":50,"s":[27],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.628],"y":[0]}},{"t":77,"s":[-11],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":100,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]},{"id":"4","layers":[{"ddd":0,"ind":13,"ty":4,"nm":".primary.design","sr":1,"ks":{"p":{"a":0,"k":[181,186],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":".primary.design","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"t":2,"s":[{"c":false,"v":[[153.363,-119.689],[-14.644,46.689],[-66.863,-5.466]],"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]]}],"i":{"x":[0.157],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":35,"s":[{"c":false,"v":[[167.916,-123.188],[-50.252,92.864],[-118.061,25.138]],"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.6],"y":[0]}},{"t":70,"s":[{"c":false,"v":[[114.144,-89.081],[-10.899,34.749],[-49.764,-4.068]],"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]]}],"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":100,"s":[{"c":false,"v":[[153.363,-119.689],[-14.644,46.689],[-66.863,-5.466]],"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]]}],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":2,"s":[100],"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]}},{"t":33,"s":[100],"h":1},{"t":47,"s":[0],"i":{"x":[0.1],"y":[1]},"o":{"x":[0.167],"y":[0.167]}},{"t":100,"s":[100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"e":{"a":1,"k":[{"t":2,"s":[26.2],"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]}},{"t":33,"s":[100],"h":1},{"t":47,"s":[0],"i":{"x":[0.1],"y":[1]},"o":{"x":[0.167],"y":[0.167]}},{"t":100,"s":[26.2],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":31.3,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[43.25600051879883,-36.500999450683594],"ix":2},"a":{"a":0,"k":[43.25,-36.5],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":12345679,"ty":4,"nm":"Group Layer 8","sr":1,"ks":{"p":{"a":0,"k":[365,464.38524590163934,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[51.229508196721305,51.229508196721305,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[220.741,37.184],[225.501,35.896],[228.749,32.36800000000001],[229.981,27.216000000000008],[228.749,22.12],[225.501,18.592],[220.741,17.304],[215.981,18.592],[212.677,22.12],[211.501,27.216000000000008],[212.677,32.36800000000001],[215.981,35.896],[220.741,37.184],[220.741,37.184],[220.741,37.184]],"i":[[0,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.382000000000062,0.8586999999999989],[1.79200000000003,0],[1.418999999999983,-0.8586999999999989],[0.8220000000000027,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.382000000000062,-0.8586999999999989],[0.8220000000000027,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.380999999999972,-0.8586999999999989],[-1.754000000000019,0],[-1.380999999999972,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.8220000000000027,1.493299999999991],[1.418999999999983,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[221.357,43.06400000000001],[214.917,41.608],[210.49300000000005,37.408],[211.221,36.232],[211.221,42.392],[205.173,42.392],[205.173,0],[211.501,0],[211.501,18.36800000000001],[210.49300000000005,16.912000000000006],[214.973,12.88],[221.357,11.424000000000007],[229.085,13.49600000000001],[234.51700000000005,19.152],[236.533,27.216000000000008],[234.51700000000005,35.28],[229.141,40.992],[221.357,43.06400000000001],[221.357,43.06400000000001],[221.357,43.06400000000001]],"i":[[0,0],[1.942000000000007,0.9706999999999937],[1.045999999999935,1.829300000000003],[-0.2426666666666506,0.3919999999999959],[0,-2.053333333333327],[2.015999999999963,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.122666666666674],[0.3360000000000127,0.4853333333333296],[-1.865999999999985,0.9707000000000079],[-2.38900000000001,0],[-2.277000000000044,-1.3813000000000102],[-1.30600000000004,-2.389300000000006],[0,-2.986699999999999],[1.343999999999937,-2.389300000000006],[2.27800000000002,-1.4187000000000012],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.351999999999975,0],[-1.903999999999996,-0.9707000000000079],[0.2426666666666506,-0.3919999999999959],[0,2.053333333333327],[-2.015999999999963,0],[0,-14.13066666666667],[2.109333333333325,0],[0,6.122666666666667],[-0.3360000000000127,-0.4853333333333296],[1.120000000000005,-1.717300000000009],[1.867000000000075,-0.9706999999999937],[2.875,0],[2.314999999999941,1.381299999999996],[1.343999999999937,2.389300000000006],[0,2.986699999999999],[-1.30600000000004,2.389300000000006],[-2.27699999999993,1.381299999999996],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[181.87,43.06400000000001],[176.438,42],[172.854,38.976],[171.566,34.384],[172.63,29.960000000000008],[176.046,26.656000000000006],[181.814,24.752],[192.342,23.016000000000005],[192.342,28],[183.046,29.624],[179.35,31.248],[178.174,34.16],[179.462,37.016000000000005],[182.878,38.08],[187.35799999999995,36.96000000000001],[190.38199999999995,33.992],[191.446,29.792],[191.446,22.008],[189.766,18.36800000000001],[185.398,16.912000000000006],[180.974,18.256],[178.23,21.616],[172.966,18.98400000000001],[175.71,15.064000000000007],[180.134,12.376],[185.566,11.424000000000007],[191.894,12.768],[196.206,16.52],[197.774,22.008],[197.774,42.392],[191.726,42.392],[191.726,36.904],[193.014,37.072],[190.27,40.264],[186.518,42.336],[181.87,43.06400000000001],[181.87,43.06400000000001],[181.87,43.06400000000001]],"i":[[0,0],[1.567999999999984,0.7092999999999989],[0.8589999999999236,1.2693000000000012],[0,1.7547],[-0.7089999999999463,1.306699999999992],[-1.530000000000086,0.8960000000000008],[-2.313999999999965,0.3733000000000004],[-3.509333333333302,0.5786666666666633],[0,-1.661333333333332],[3.098666666666645,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8579999999999472,-0.7467000000000041],[-1.381000000000085,0],[-1.268999999999892,0.7466999999999899],[-0.7089999999999463,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829999999999927,0],[1.269999999999982,-0.8960000000000008],[0.5979999999999563,-1.381299999999996],[1.754666666666708,0.8773333333333255],[-1.269000000000005,1.11999999999999],[-1.680000000000064,0.6346999999999952],[-1.903999999999996,0],[-1.828999999999951,-0.8960000000000008],[-1.008000000000038,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999963,0],[0,1.829333333333338],[-0.4293333333333749,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.717999999999961,0],[0,0],[0,0]],"o":[[-2.052999999999997,0],[-1.529999999999973,-0.7467000000000041],[-0.8580000000000609,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999573,-1.306700000000006],[1.530999999999949,-0.8960000000000008],[3.509333333333302,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666645,0.541333333333327],[-1.680000000000064,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.8959999999999582,0.7092999999999989],[1.717999999999961,0],[1.307000000000016,-0.7467000000000041],[0.7100000000000364,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.081999999999994,-0.9707000000000079],[-1.680000000000064,0],[-1.232000000000085,0.8586999999999989],[-1.754666666666708,-0.8773333333333255],[0.5599999999999454,-1.493300000000005],[1.269999999999982,-1.157300000000006],[1.717999999999961,-0.6347000000000094],[2.389999999999986,0],[1.866999999999962,0.8960000000000008],[1.045999999999935,1.568000000000012],[0,6.794666666666672],[-2.015999999999963,0],[0,-1.829333333333338],[0.4293333333333749,0.05599999999999739],[-0.70900000000006,1.2319999999999993],[-1.081999999999994,0.8960000000000008],[-1.380999999999972,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[159.072,42.392],[159.072,0],[165.4,0],[165.4,42.392],[159.072,42.392],[159.072,42.392],[159.072,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-14.13066666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,14.13066666666667],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[138.952,43.06400000000001],[130.888,40.992],[125.456,35.28],[123.496,27.16],[125.456,19.040000000000006],[130.832,13.49600000000001],[138.448,11.424000000000007],[144.552,12.600000000000009],[149.088,15.848],[151.888,20.49600000000001],[152.896,26.096],[152.84,27.608],[152.616,29.064000000000007],[128.48,29.064000000000007],[128.48,24.024],[149.032,24.024],[146.008,26.320000000000007],[145.616,21.448000000000008],[142.816,18.032],[138.448,16.744],[133.968,18.032],[130.944,21.616],[130.104,27.216000000000008],[130.944,32.592],[134.192,36.176],[139.008,37.464],[143.65599999999995,36.232],[146.736,33.040000000000006],[151.888,35.56],[149.088,39.42400000000001],[144.60799999999995,42.11200000000001],[138.952,43.06400000000001],[138.952,43.06400000000001],[138.952,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.307000000000016,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.836999999999989,0],[-1.79200000000003,-0.784000000000006],[-1.231999999999971,-1.381299999999996],[-0.6349999999999909,-1.754700000000014],[0,-1.978700000000003],[0.03699999999992087,-0.5227000000000004],[0.1119999999999663,-0.4480000000000075],[8.04533333333336,0],[0,1.680000000000007],[-6.850666666666712,0],[1.008000000000038,-0.7653333333333308],[0.6349999999999909,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.680000000000064,0],[1.307000000000016,-0.8586999999999989],[0.70900000000006,-1.567999999999998],[-0.1490000000000009,-2.202700000000007],[-0.7469999999999573,-1.530699999999996],[-1.380999999999972,-0.8586999999999989],[-1.79200000000003,0],[-1.268999999999892,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333386,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.755000000000109,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.315000000000055,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999937,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.277000000000044,0],[1.79200000000003,0.7839999999999918],[1.232000000000085,1.344000000000008],[0.6720000000000255,1.7547],[0,0.4852999999999952],[-0.03700000000003456,0.5227000000000004],[-8.04533333333336,0],[0,-1.680000000000007],[6.850666666666712,0],[-1.008000000000038,0.7653333333333308],[0.3729999999999336,-1.829300000000003],[-0.59699999999998,-1.456000000000003],[-1.232000000000085,-0.8586999999999989],[-1.67999999999995,0],[-1.306999999999903,0.8213000000000079],[-0.7089999999999463,1.530699999999996],[-0.1870000000000118,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418999999999983,0.8586999999999989],[1.828999999999951,0],[1.307000000000016,-0.8212999999999937],[1.717333333333386,0.8400000000000034],[-0.59699999999998,1.4187000000000012],[-1.231999999999971,1.11999999999999],[-1.716999999999985,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,7.951999999999998],[111.001,0.6720000000000041],[117.329,0.6720000000000041],[117.329,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998],[111.001,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[111.001,42.392],[111.001,12.096],[117.329,12.096],[117.329,42.392],[111.001,42.392],[111.001,42.392],[111.001,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[101.41,42.72800000000001],[94.01800000000003,40.040000000000006],[91.38599999999997,32.48],[91.38599999999997,17.808000000000007],[86.06600000000003,17.808000000000007],[86.06600000000003,12.096],[86.90599999999995,12.096],[90.21000000000004,10.864],[91.38599999999997,7.504000000000005],[91.38599999999997,5.152000000000001],[97.71400000000006,5.152000000000001],[97.71400000000006,12.096],[104.602,12.096],[104.602,17.808000000000007],[97.71400000000006,17.808000000000007],[97.71400000000006,32.2],[98.21799999999996,34.888000000000005],[99.84199999999998,36.568],[102.754,37.128],[103.76200000000006,37.072],[104.826,36.96000000000001],[104.826,42.392],[103.09,42.616],[101.41,42.72800000000001],[101.41,42.72800000000001],[101.41,42.72800000000001]],"i":[[0,0],[1.754999999999995,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333312,0],[0,1.903999999999996],[-0.2799999999999727,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999935,0],[0,-1.903999999999996],[2.295999999999935,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7469999999999573,-0.4106999999999914],[-1.19500000000005,0],[-0.3730000000000473,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6349999999999909,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333312,0],[0,-1.903999999999996],[0.2799999999999727,0],[1.419000000000096,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999935,0],[0,1.903999999999996],[-2.295999999999935,0],[0,4.797333333333327],[0,1.045299999999997],[0.3360000000000127,0.7092999999999989],[0.7470000000000709,0.3733000000000004],[0.2989999999999782,0],[0.3729999999999336,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[78.71499999999997,42.72800000000001],[71.32299999999998,40.040000000000006],[68.69100000000003,32.48],[68.69100000000003,17.808000000000007],[63.37099999999998,17.808000000000007],[63.37099999999998,12.096],[64.21100000000001,12.096],[67.51499999999999,10.864],[68.69100000000003,7.504000000000005],[68.69100000000003,5.152000000000001],[75.019,5.152000000000001],[75.019,12.096],[81.90700000000004,12.096],[81.90700000000004,17.808000000000007],[75.019,17.808000000000007],[75.019,32.2],[75.52300000000002,34.888000000000005],[77.14699999999999,36.568],[80.05900000000003,37.128],[81.06700000000001,37.072],[82.13099999999997,36.96000000000001],[82.13099999999997,42.392],[80.39499999999998,42.616],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001],[78.71499999999997,42.72800000000001]],"i":[[0,0],[1.754000000000019,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7470000000000141,-0.4106999999999914],[-1.19500000000005,0],[-0.3740000000000236,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6340000000000146,-0.07469999999999288],[0.4850000000000136,0],[0,0],[0,0]],"o":[[-3.173999999999978,0],[-1.754999999999995,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418000000000006,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7460000000000377,0.3733000000000004],[0.297999999999945,0],[0.3730000000000473,-0.03730000000000189],[0,1.810666666666663],[-0.5230000000000246,0.0747000000000071],[-0.6349999999999909,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,37.184],[48.94799999999998,35.896],[52.19600000000003,32.36800000000001],[53.428,27.216000000000008],[52.19600000000003,22.12],[48.94799999999998,18.592],[44.18799999999999,17.304],[39.428,18.592],[36.12400000000002,22.12],[34.94799999999998,27.216000000000008],[36.12400000000002,32.36800000000001],[39.428,35.896],[44.18799999999999,37.184],[44.18799999999999,37.184],[44.18799999999999,37.184]],"i":[[0,0],[-1.381999999999948,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.381000000000029,0.8586999999999989],[1.79200000000003,0],[1.418000000000006,-0.8586999999999989],[0.8209999999999695,-1.493300000000005],[0,-1.904000000000011],[-0.7840000000000487,-1.5307000000000102],[-1.382000000000005,-0.8586999999999989],[-1.754999999999995,0],[0,0],[0,0]],"o":[[1.79200000000003,0],[1.381000000000029,-0.8586999999999989],[0.8209999999999695,-1.5307000000000102],[0,-1.904000000000011],[-0.7840000000000487,-1.493300000000005],[-1.381999999999948,-0.8586999999999989],[-1.754999999999995,0],[-1.382000000000005,0.8586999999999989],[-0.7840000000000487,1.493299999999991],[0,1.903999999999996],[0.8209999999999695,1.493299999999991],[1.418000000000006,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[44.18799999999999,43.06400000000001],[36.18000000000001,40.992],[30.46800000000002,35.336],[28.33999999999997,27.216000000000008],[30.46800000000002,19.096],[36.18000000000001,13.49600000000001],[44.18799999999999,11.424000000000007],[52.19600000000003,13.49600000000001],[57.85199999999998,19.096],[59.98000000000002,27.216000000000008],[57.85199999999998,35.392],[52.139999999999986,41.048],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001],[44.18799999999999,43.06400000000001]],"i":[[0,0],[2.425999999999988,1.381299999999996],[1.418000000000006,2.389300000000006],[0,3.024000000000001],[-1.41900000000004,2.352000000000004],[-2.389999999999986,1.343999999999994],[-2.949999999999989,0],[-2.352000000000032,-1.3813000000000102],[-1.381999999999948,-2.389300000000006],[0,-3.061300000000003],[1.418000000000006,-2.389299999999992],[2.38900000000001,-1.381299999999996],[2.912000000000035,0],[0,0],[0,0]],"o":[[-2.911999999999978,0],[-2.389999999999986,-1.381299999999996],[-1.41900000000004,-2.389299999999992],[0,-3.061300000000003],[1.418000000000006,-2.389300000000006],[2.38900000000001,-1.3813000000000102],[2.98599999999999,0],[2.388999999999953,1.343999999999994],[1.418000000000006,2.352000000000004],[0,3.061299999999989],[-1.418999999999983,2.389300000000006],[-2.389999999999986,1.343999999999994],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.608000000000004,0.6720000000000041],[6.608000000000004,36.512],[24.639999999999986,36.512],[24.639999999999986,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.202666666666687,0],[0,-11.94666666666666],[-6.01066666666668,0],[0,-1.959999999999994],[8.21333333333331,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.202666666666687,0],[0,11.94666666666667],[6.01066666666668,0],[0,1.959999999999994],[-8.21333333333331,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[98.08047485351562,-21.67217254638672],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[246.681,42.392],[246.681,0],[253.009,0],[253.009,18.032],[252.001,17.248],[255.585,12.936000000000007],[261.297,11.424000000000007],[267.23299999999995,12.88],[271.265,16.912000000000006],[272.721,22.792],[272.721,42.392],[266.449,42.392],[266.449,24.528000000000006],[265.553,20.664],[263.201,18.2],[259.729,17.304],[256.25699999999995,18.2],[253.849,20.664],[253.009,24.528000000000006],[253.009,42.392],[246.681,42.392],[246.681,42.392],[246.681,42.392]],"i":[[0,0],[0,14.13066666666667],[-2.109333333333325,0],[0,-6.010666666666665],[0.3360000000000127,0.2613333333333259],[-1.643000000000029,0.9706999999999937],[-2.166000000000054,0],[-1.717999999999961,-0.9706999999999937],[-0.9710000000000036,-1.717300000000009],[0,-2.202699999999993],[0,-6.533333333333331],[2.090666666666721,0],[0,5.954666666666668],[0.59699999999998,1.045299999999997],[1.007999999999925,0.5600000000000023],[1.305999999999926,0],[1.045000000000073,-0.5973000000000042],[0.59699999999998,-1.082700000000003],[0,-1.493300000000005],[0,-5.954666666666668],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-14.13066666666667],[2.109333333333325,0],[0,6.010666666666665],[-0.3360000000000127,-0.2613333333333259],[0.7459999999999809,-1.903999999999996],[1.641999999999967,-1.0080000000000098],[2.240000000000009,0],[1.717000000000098,0.9707000000000079],[0.9700000000000273,1.717299999999994],[0,6.533333333333331],[-2.090666666666721,0],[0,-5.954666666666668],[0,-1.5307000000000102],[-0.5599999999999454,-1.082700000000003],[-1.008000000000038,-0.5973000000000042],[-1.270000000000095,0],[-1.007999999999953,0.5600000000000023],[-0.5600000000000023,1.082700000000003],[0,5.954666666666668],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[237.089,42.72800000000001],[229.697,40.040000000000006],[227.065,32.48],[227.065,17.808000000000007],[221.745,17.808000000000007],[221.745,12.096],[222.585,12.096],[225.889,10.864],[227.065,7.504000000000005],[227.065,5.152000000000001],[233.393,5.152000000000001],[233.393,12.096],[240.281,12.096],[240.281,17.808000000000007],[233.393,17.808000000000007],[233.393,32.2],[233.897,34.888000000000005],[235.521,36.568],[238.433,37.128],[239.441,37.072],[240.505,36.96000000000001],[240.505,42.392],[238.769,42.616],[237.089,42.72800000000001],[237.089,42.72800000000001],[237.089,42.72800000000001]],"i":[[0,0],[1.755000000000052,1.792000000000002],[0,3.248000000000005],[0,4.890666666666661],[1.773333333333369,0],[0,1.903999999999996],[-0.2800000000000296,0],[-0.7839999999999918,0.8212999999999937],[0,1.4187000000000012],[0,0.7839999999999989],[-2.109333333333325,0],[0,-2.314666666666668],[-2.295999999999992,0],[0,-1.903999999999996],[2.295999999999992,0],[0,-4.797333333333327],[-0.3360000000000127,-0.7467000000000041],[-0.7459999999999809,-0.4106999999999914],[-1.194000000000017,0],[-0.3729999999999905,0.03730000000000189],[-0.3360000000000127,0.03729999999998768],[0,-1.810666666666663],[0.6350000000000477,-0.07469999999999288],[0.48599999999999,0],[0,0],[0,0]],"o":[[-3.173000000000002,0],[-1.753999999999962,-1.792000000000002],[0,-4.890666666666661],[-1.773333333333369,0],[0,-1.903999999999996],[0.2800000000000296,0],[1.418999999999983,0],[0.7839999999999918,-0.8213000000000079],[0,-0.7839999999999989],[2.109333333333325,0],[0,2.314666666666668],[2.295999999999992,0],[0,1.903999999999996],[-2.295999999999992,0],[0,4.797333333333327],[0,1.045299999999997],[0.3359999999999559,0.7092999999999989],[0.7470000000000141,0.3733000000000004],[0.2989999999999782,0],[0.3740000000000236,-0.03730000000000189],[0,1.810666666666663],[-0.5220000000000482,0.0747000000000071],[-0.6339999999999577,0.0747000000000071],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,7.951999999999998],[210.259,0.6720000000000041],[216.587,0.6720000000000041],[216.587,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998],[210.259,7.951999999999998]],"i":[[0,0],[0,2.426666666666662],[-2.109333333333325,0],[0,-2.426666666666662],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-2.426666666666662],[2.109333333333325,0],[0,2.426666666666662],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[210.259,42.392],[210.259,12.096],[216.587,12.096],[216.587,42.392],[210.259,42.392],[210.259,42.392],[210.259,42.392]],"i":[[0,0],[0,10.09866666666667],[-2.109333333333325,0],[0,-10.09866666666667],[2.109333333333325,0],[0,0],[0,0]],"o":[[0,-10.09866666666666],[2.109333333333325,0],[0,10.09866666666666],[-2.109333333333325,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[169.688,42.392],[159.272,12.096],[165.992,12.096],[173.944,36.232],[171.592,36.232],[179.712,12.096],[185.48,12.096],[193.544,36.232],[191.192,36.232],[199.2,12.096],[205.92,12.096],[195.448,42.392],[189.736,42.392],[181.56,17.696],[183.632,17.696],[175.456,42.392],[169.688,42.392],[169.688,42.392],[169.688,42.392]],"i":[[0,0],[3.47199999999998,10.09866666666667],[-2.240000000000009,0],[-2.650666666666666,-8.045333333333332],[0.7839999999999918,0],[-2.706666666666649,8.045333333333332],[-1.922666666666657,0],[-2.687999999999988,-8.045333333333332],[0.7839999999999918,0],[-2.669333333333327,8.045333333333332],[-2.240000000000009,0],[3.490666666666641,-10.09866666666667],[1.903999999999996,0],[2.725333333333367,8.232],[-0.6906666666666865,0],[2.72533333333331,-8.232],[1.922666666666657,0],[0,0],[0,0]],"o":[[-3.47199999999998,-10.09866666666666],[2.240000000000009,0],[2.650666666666666,8.045333333333332],[-0.7839999999999918,0],[2.706666666666649,-8.045333333333332],[1.922666666666657,0],[2.687999999999988,8.045333333333332],[-0.7839999999999918,0],[2.669333333333327,-8.045333333333332],[2.240000000000009,0],[-3.490666666666641,10.09866666666666],[-1.903999999999996,0],[-2.725333333333367,-8.232],[0.6906666666666865,0],[-2.72533333333331,8.232],[-1.922666666666657,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-354.5325317382812,-77.50520324707031],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[155.86146545410156,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[132.444,43.06400000000001],[124.38,40.992],[118.948,35.28],[116.988,27.16],[118.948,19.040000000000006],[124.324,13.49600000000001],[131.94,11.424000000000007],[138.044,12.600000000000009],[142.58,15.848],[145.38,20.49600000000001],[146.388,26.096],[146.332,27.608],[146.108,29.064000000000007],[121.972,29.064000000000007],[121.972,24.024],[142.524,24.024],[139.5,26.320000000000007],[139.108,21.448000000000008],[136.308,18.032],[131.94,16.744],[127.46,18.032],[124.436,21.616],[123.596,27.216000000000008],[124.436,32.592],[127.684,36.176],[132.5,37.464],[137.148,36.232],[140.228,33.040000000000006],[145.38,35.56],[142.58,39.42400000000001],[138.1,42.11200000000001],[132.444,43.06400000000001],[132.444,43.06400000000001],[132.444,43.06400000000001]],"i":[[0,0],[2.351999999999975,1.381299999999996],[1.305999999999983,2.389300000000006],[0,2.986699999999999],[-1.307000000000016,2.35199999999999],[-2.240000000000009,1.343999999999994],[-2.838000000000022,0],[-1.79200000000003,-0.784000000000006],[-1.232000000000028,-1.381299999999996],[-0.6350000000000477,-1.754700000000014],[0,-1.978700000000003],[0.03699999999997772,-0.5227000000000004],[0.1120000000000232,-0.4480000000000075],[8.045333333333303,0],[0,1.680000000000007],[-6.850666666666655,0],[1.007999999999981,-0.7653333333333308],[0.6340000000000146,1.4187000000000012],[1.269000000000005,0.8213000000000079],[1.67999999999995,0],[1.305999999999983,-0.8586999999999989],[0.7090000000000032,-1.567999999999998],[-0.1499999999999773,-2.202700000000007],[-0.7470000000000141,-1.530699999999996],[-1.382000000000005,-0.8586999999999989],[-1.791999999999973,0],[-1.269999999999982,0.8213000000000079],[-0.7469999999999573,1.306699999999992],[-1.717333333333329,-0.8400000000000034],[1.269000000000005,-1.157300000000006],[1.754000000000019,-0.6720000000000113],[2.052999999999997,0],[0,0],[0,0]],"o":[[-3.024000000000001,0],[-2.314999999999998,-1.4187000000000012],[-1.307000000000016,-2.426699999999997],[0,-3.061299999999989],[1.343999999999994,-2.352000000000004],[2.240000000000009,-1.3813000000000102],[2.276999999999987,0],[1.791999999999973,0.7839999999999918],[1.231999999999971,1.344000000000008],[0.6719999999999686,1.7547],[0,0.4852999999999952],[-0.03800000000001091,0.5227000000000004],[-8.045333333333303,0],[0,-1.680000000000007],[6.850666666666655,0],[-1.007999999999981,0.7653333333333308],[0.3730000000000473,-1.829300000000003],[-0.5979999999999563,-1.456000000000003],[-1.232000000000028,-0.8586999999999989],[-1.680000000000007,0],[-1.307000000000016,0.8213000000000079],[-0.7100000000000364,1.530699999999996],[-0.186999999999955,2.053299999999993],[0.7839999999999918,1.53070000000001],[1.418000000000006,0.8586999999999989],[1.829000000000008,0],[1.305999999999983,-0.8212999999999937],[1.717333333333329,0.8400000000000034],[-0.5980000000000132,1.4187000000000012],[-1.232000000000028,1.11999999999999],[-1.718000000000018,0.6346999999999952],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[95.32,37.184],[100.024,35.896],[103.328,32.36800000000001],[104.56,27.216000000000008],[103.328,22.12],[100.024,18.592],[95.32,17.304],[90.56,18.592],[87.256,22.12],[86.08000000000001,27.216000000000008],[87.256,32.36800000000001],[90.50399999999999,35.896],[95.32,37.184],[95.32,37.184],[95.32,37.184]],"i":[[0,0],[-1.381,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.820999999999998,1.493299999999991],[1.419000000000011,0.8586999999999989],[1.754999999999995,0],[1.418999999999983,-0.8586999999999989],[0.7839999999999918,-1.493300000000005],[0,-1.904000000000011],[-0.7839999999999918,-1.5307000000000102],[-1.381,-0.8586999999999989],[-1.792000000000002,0],[0,0],[0,0]],"o":[[1.754999999999995,0],[1.419000000000011,-0.8586999999999989],[0.820999999999998,-1.5307000000000102],[0,-1.904000000000011],[-0.7839999999999918,-1.493300000000005],[-1.381,-0.8586999999999989],[-1.754999999999995,0],[-1.419000000000011,0.8586999999999989],[-0.7839999999999918,1.493299999999991],[0,1.903999999999996],[0.7839999999999918,1.493299999999991],[1.419000000000011,0.8586999999999989],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[94.70400000000001,43.06400000000001],[86.864,40.992],[81.43199999999999,35.28],[79.47200000000001,27.216000000000008],[81.488,19.152],[86.91999999999999,13.49600000000001],[94.648,11.424000000000007],[101.088,12.88],[105.512,16.912000000000006],[104.56,18.36800000000001],[104.56,0],[110.832,0],[110.832,42.392],[104.84,42.392],[104.84,36.232],[105.568,37.408],[101.088,41.608],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001],[94.70400000000001,43.06400000000001]],"i":[[0,0],[2.314999999999998,1.381299999999996],[1.344000000000023,2.389300000000006],[0,2.986699999999999],[-1.343999999999994,2.389300000000006],[-2.276999999999987,1.381299999999996],[-2.875,0],[-1.8669999999999902,-0.9706999999999937],[-1.082999999999998,-1.717300000000009],[0.3173333333333233,-0.4853333333333296],[0,6.122666666666674],[-2.090666666666664,0],[0,-14.13066666666667],[1.99733333333333,0],[0,2.053333333333327],[-0.242666666666679,-0.3919999999999959],[1.941000000000003,-0.9707000000000079],[2.314999999999998,0],[0,0],[0,0]],"o":[[-2.912000000000006,0],[-2.277000000000015,-1.4187000000000012],[-1.306999999999988,-2.389300000000006],[0,-2.986699999999999],[1.343999999999994,-2.389300000000006],[2.277000000000015,-1.3813000000000102],[2.426999999999992,0],[1.867000000000019,0.9707000000000079],[-0.3173333333333233,0.4853333333333296],[0,-6.122666666666674],[2.090666666666664,0],[0,14.13066666666667],[-1.99733333333333,0],[0,-2.053333333333327],[0.242666666666679,0.3919999999999959],[-1.045000000000016,1.829300000000003],[-1.941000000000003,0.9706999999999937],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[57.40100000000001,43.06400000000001],[51.968999999999994,42],[48.38499999999999,38.976],[47.09700000000001,34.384],[48.161,29.960000000000008],[51.577,26.656000000000006],[57.345,24.752],[67.87299999999999,23.016000000000005],[67.87299999999999,28],[58.577,29.624],[54.881,31.248],[53.70500000000001,34.16],[54.992999999999995,37.016000000000005],[58.40899999999999,38.08],[62.88900000000001,36.96000000000001],[65.91300000000001,33.992],[66.977,29.792],[66.977,22.008],[65.297,18.36800000000001],[60.929,16.912000000000006],[56.505,18.256],[53.761,21.616],[48.496999999999986,18.98400000000001],[51.240999999999985,15.064000000000007],[55.66499999999999,12.376],[61.09700000000001,11.424000000000007],[67.42500000000001,12.768],[71.737,16.52],[73.305,22.008],[73.305,42.392],[67.257,42.392],[67.257,36.904],[68.54499999999999,37.072],[65.80099999999999,40.264],[62.04900000000001,42.336],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001],[57.40100000000001,43.06400000000001]],"i":[[0,0],[1.568000000000012,0.7092999999999989],[0.8590000000000089,1.2693000000000012],[0,1.7547],[-0.7090000000000032,1.306699999999992],[-1.531000000000006,0.8960000000000008],[-2.314999999999998,0.3733000000000004],[-3.509333333333331,0.5786666666666633],[0,-1.661333333333332],[3.098666666666674,-0.541333333333327],[0.7839999999999918,-0.784000000000006],[0,-1.194699999999997],[-0.8590000000000089,-0.7467000000000041],[-1.381,0],[-1.269000000000005,0.7466999999999899],[-0.7090000000000032,1.2319999999999993],[0,1.530699999999996],[0,2.594666666666669],[1.120000000000005,0.9332999999999885],[1.829000000000008,0],[1.269000000000005,-0.8960000000000008],[0.5970000000000084,-1.381299999999996],[1.754666666666679,0.8773333333333255],[-1.268999999999977,1.11999999999999],[-1.680000000000007,0.6346999999999952],[-1.903999999999996,0],[-1.829000000000008,-0.8960000000000008],[-1.0080000000000098,-1.6053],[0,-2.090699999999998],[0,-6.794666666666672],[2.015999999999991,0],[0,1.829333333333338],[-0.429333333333318,-0.05599999999999739],[1.120000000000005,-0.8959999999999866],[1.418999999999983,-0.4852999999999952],[1.716999999999985,0],[0,0],[0,0]],"o":[[-2.053000000000026,0],[-1.531000000000006,-0.7467000000000041],[-0.8589999999999804,-1.306699999999992],[0,-1.642700000000005],[0.7469999999999857,-1.306700000000006],[1.531000000000006,-0.8960000000000008],[3.509333333333331,-0.5786666666666633],[0,1.661333333333332],[-3.098666666666674,0.541333333333327],[-1.680000000000007,0.2987000000000108],[-0.7839999999999918,0.7467000000000041],[0,1.157300000000006],[0.896000000000015,0.7092999999999989],[1.717000000000013,0],[1.306999999999988,-0.7467000000000041],[0.7089999999999748,-1.2693000000000012],[0,-2.594666666666669],[0,-1.493299999999991],[-1.082999999999998,-0.9707000000000079],[-1.680000000000007,0],[-1.2319999999999993,0.8586999999999989],[-1.754666666666679,-0.8773333333333255],[0.5600000000000023,-1.493300000000005],[1.269000000000005,-1.157300000000006],[1.717000000000013,-0.6347000000000094],[2.388999999999982,0],[1.86699999999999,0.8960000000000008],[1.045000000000016,1.568000000000012],[0,6.794666666666672],[-2.015999999999991,0],[0,-1.829333333333338],[0.429333333333318,0.05599999999999739],[-0.7089999999999748,1.2319999999999993],[-1.082999999999998,0.8960000000000008],[-1.381,0.4853000000000094],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[0,42.392],[0,0.6720000000000041],[6.159999999999997,0.6720000000000041],[21.84,22.400000000000006],[18.75999999999999,22.400000000000006],[34.16,0.6720000000000041],[40.31999999999999,0.6720000000000041],[40.31999999999999,42.392],[33.768,42.392],[33.768,8.456000000000003],[36.232,9.128],[20.49600000000001,30.632000000000005],[19.824000000000012,30.632000000000005],[4.424000000000007,9.128],[6.608000000000004,8.456000000000003],[6.608000000000004,42.392],[0,42.392],[0,42.392],[0,42.392]],"i":[[0,0],[0,13.90666666666666],[-2.053333333333342,0],[-5.226666666666659,-7.242666666666665],[1.026666666666671,0],[-5.133333333333326,7.242666666666672],[-2.053333333333342,0],[0,-13.90666666666667],[2.183999999999997,0],[0,11.312],[-0.8213333333333424,-0.2240000000000038],[5.245333333333321,-7.168000000000006],[0.2239999999999895,0],[5.133333333333326,7.168000000000006],[-0.7280000000000086,0.2240000000000038],[0,-11.312],[2.202666666666659,0],[0,0],[0,0]],"o":[[0,-13.90666666666667],[2.053333333333342,0],[5.226666666666659,7.242666666666672],[-1.026666666666671,0],[5.133333333333326,-7.242666666666665],[2.053333333333342,0],[0,13.90666666666666],[-2.183999999999997,0],[0,-11.312],[0.8213333333333424,0.2240000000000038],[-5.245333333333321,7.168000000000006],[-0.2239999999999895,0],[-5.133333333333326,-7.168000000000006],[0.7280000000000086,-0.2240000000000038],[0,11.312],[-2.202666666666659,0],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[-211.7300415039062,-77.67320251464844],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[2.91259765625,56.001014709472656],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[702.6863719370097,144],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":72,"ix":2}},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"tr","p":{"a":0,"k":[56.54167175292969,-0.000022762338630855083],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[99.99999403953552,99.99999403953552],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":80,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[122.0000003294881,25.00000012138912],"ix":2},"a":{"a":0,"k":[56.54167175292969,-0.00002288818359375],"ix":2},"s":{"a":0,"k":[34.403572049765366,34.403572049765366],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"ind":14,"ty":3,"nm":"Color & Stroke Change","sr":1,"ks":{"p":{"a":0,"k":[250,250],"ix":2},"a":{"a":0,"k":[50,50],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0},{"ddd":0,"refId":"0","w":500,"h":500,"ind":15,"ty":0,"nm":"hover-verified","sr":1,"ks":{"p":{"a":0,"k":[50,50],"ix":2},"a":{"a":0,"k":[250,250],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":103,"st":0,"bm":0,"parent":14}],"markers":[]} \ No newline at end of file diff --git a/frontend/public/vercel.svg b/frontend/public/vercel.svg deleted file mode 100644 index fbf0e25a6..000000000 --- a/frontend/public/vercel.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/scripts/initialize-standalone-build.sh b/frontend/scripts/initialize-standalone-build.sh index 644877d8f..972534326 100755 --- a/frontend/scripts/initialize-standalone-build.sh +++ b/frontend/scripts/initialize-standalone-build.sh @@ -6,6 +6,8 @@ scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_INTERCOM_ID" "$ scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY" "$NEXT_PUBLIC_CAPTCHA_SITE_KEY" +scripts/set-frontend-config.sh + if [ "$TELEMETRY_ENABLED" != "false" ]; then echo "Telemetry is enabled" scripts/set-standalone-build-telemetry.sh true diff --git a/frontend/scripts/replace-standalone-build-variable.sh b/frontend/scripts/replace-standalone-build-variable.sh index fde4ca282..c92397d93 100755 --- a/frontend/scripts/replace-standalone-build-variable.sh +++ b/frontend/scripts/replace-standalone-build-variable.sh @@ -10,7 +10,7 @@ fi echo "Replacing pre-baked value.." -find public .next -type f -name "*.js" | +find assets -type f -name "*.js" | while read file; do sed -i "s|$ORIGINAL|$REPLACEMENT|g" "$file" done diff --git a/frontend/scripts/set-frontend-config.sh b/frontend/scripts/set-frontend-config.sh new file mode 100755 index 000000000..423662781 --- /dev/null +++ b/frontend/scripts/set-frontend-config.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +# Configuration output file +CONFIG_FILE="runtime-config.js" + +# Replace content in the config file with SENTRY_DSN interpolation +echo "window.__CONFIG__ = Object.freeze({ CAPTCHA_SITE_KEY: \"${CAPTCHA_SITE_KEY}\", CAPTCHA_SITE_KEY: \"${CAPTCHA_SITE_KEY}\", CAPTCHA_SITE_KEY: \"${CAPTCHA_SITE_KEY}\" })" > $CONFIG_FILE + +echo "Configuration file updated at $CONFIG_FILE" diff --git a/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx b/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx deleted file mode 100644 index 4a1d3c803..000000000 --- a/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { faPlus } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { Button, Checkbox, PopoverContent } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; - -import { WsTag } from "../../hooks/api/tags/types"; -import { ProjectPermissionCan } from "../permissions"; - -interface Props { - wsTags: WsTag[] | undefined; - secKey: string; - selectedTagIds: Record; - handleSelectTag: (wsTag: WsTag) => void; - handleTagOnMouseEnter: (wsTag: WsTag) => void; - handleTagOnMouseLeave: () => void; - checkIfTagIsVisible: (wsTag: WsTag) => boolean; - handleOnCreateTagOpen: () => void; -} - -const AddTagPopoverContent = ({ - wsTags, - secKey, - selectedTagIds, - handleSelectTag, - handleTagOnMouseEnter, - handleTagOnMouseLeave, - checkIfTagIsVisible, - handleOnCreateTagOpen -}: Props) => { - return ( - -
- Add tags to {secKey || "this secret"} -
-
-
- {wsTags?.map((wsTag: WsTag) => ( -
handleSelectTag(wsTag)} - onMouseEnter={() => handleTagOnMouseEnter(wsTag)} - onMouseLeave={() => handleTagOnMouseLeave()} - tabIndex={0} - role="button" - onKeyDown={() => {}} - > - {(checkIfTagIsVisible(wsTag) || selectedTagIds?.[wsTag.slug]) && ( - - )} -
-
- {" "} -
- {wsTag.slug} -
-
- ))} - - {(isAllowed) => ( - - )} - -
- - ); -}; - -export default AddTagPopoverContent; diff --git a/frontend/src/components/RouteGuard.tsx b/frontend/src/components/RouteGuard.tsx deleted file mode 100644 index 5c874541a..000000000 --- a/frontend/src/components/RouteGuard.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { ReactNode, useEffect, useState } from "react"; -import { useRouter } from "next/router"; - -import { publicPaths } from "@app/const"; -import checkAuth from "@app/pages/api/auth/CheckAuth"; - -// #TODO: finish spinner only when the data loads fully -// #TODO: Redirect somewhere if the page does not exist - -type Prop = { - children: ReactNode; -}; - -export default function RouteGuard({ children }: Prop): JSX.Element { - const router = useRouter(); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [authorized, setAuthorized] = useState(false); - - /** - * redirect to login page if accessing a private page and not logged in - */ - async function authCheck(url: string) { - // Make sure that we don't redirect when the user is on the following pages. - const path = `/${url.split("?")[0].split("/")[1]}`; - - // Check if the user is authenticated - const response = await checkAuth(); - // #TODO: figure our why sometimes it doesn't output a response - // ANS(akhilmhdh): Because inside the security client the await token() doesn't have try/catch - if (!publicPaths.includes(path)) { - try { - if (response.status !== 200) { - router.push("/login"); - console.log("Unauthorized to access."); - setAuthorized(false); - } else { - setAuthorized(true); - console.log("Authorized to access."); - } - } catch (error) { - console.log("Error (probably the authCheck route is stuck again...):", error); - } - } - } - - useEffect(() => { - // on initial load - run auth check - (async () => { - await authCheck(router.asPath); - })(); - - // on route change start - hide page content by setting authorized to false - // #TODO: add the loading page when not yet authorized. - const hideContent = () => setAuthorized(false); - // const onError = () => setAuthorized(true) - router.events.on("routeChangeStart", hideContent); - // router.events.on("routeChangeError", onError); - - // on route change complete - run auth check - router.events.on("routeChangeComplete", authCheck); - - // unsubscribe from events in useEffect return function - return () => { - router.events.off("routeChangeStart", hideContent); - router.events.off("routeChangeComplete", authCheck); - // router.events.off("routeChangeError", onError); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return children as JSX.Element; -} diff --git a/frontend/src/components/analytics/posthog.ts b/frontend/src/components/analytics/posthog.ts index f9285012e..264399778 100644 --- a/frontend/src/components/analytics/posthog.ts +++ b/frontend/src/components/analytics/posthog.ts @@ -1,18 +1,18 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -/* eslint-disable no-undef */ import posthog from "posthog-js"; -import { ENV, POSTHOG_API_KEY, POSTHOG_HOST } from "../utilities/config"; +import { envConfig } from "@app/config/env"; export const initPostHog = () => { - // @ts-ignore console.log("Hi there 👋"); try { if (typeof window !== "undefined") { - // @ts-ignore - if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED === true) { - posthog.init(POSTHOG_API_KEY, { - api_host: POSTHOG_HOST + if ( + envConfig.ENV === "production" && + envConfig.TELEMETRY_CAPTURING_ENABLED === true && + envConfig.POSTHOG_API_KEY + ) { + posthog.init(envConfig.POSTHOG_API_KEY, { + api_host: envConfig.POSTHOG_HOST }); } } diff --git a/frontend/src/components/signup/CodeInputStep.tsx b/frontend/src/components/auth/CodeInputStep.tsx similarity index 97% rename from frontend/src/components/signup/CodeInputStep.tsx rename to frontend/src/components/auth/CodeInputStep.tsx index a959f8594..09958fafd 100644 --- a/frontend/src/components/signup/CodeInputStep.tsx +++ b/frontend/src/components/auth/CodeInputStep.tsx @@ -1,5 +1,5 @@ /* eslint-disable react/jsx-props-no-spreading */ -import React, { useState } from "react"; +import { useState } from "react"; import ReactCodeInput from "react-code-input"; import { useTranslation } from "react-i18next"; @@ -97,7 +97,7 @@ export default function CodeInputStep({ fields={6} onChange={setCode} {...props} - className="mt-6 mb-2" + className="mb-2 mt-6" />
@@ -108,7 +108,7 @@ export default function CodeInputStep({ fields={6} onChange={setCode} {...propsPhone} - className="mt-2 mb-2" + className="mb-2 mt-2" />
{codeError && } diff --git a/frontend/src/components/signup/EnterEmailStep.tsx b/frontend/src/components/auth/EnterEmailStep.tsx similarity index 92% rename from frontend/src/components/signup/EnterEmailStep.tsx rename to frontend/src/components/auth/EnterEmailStep.tsx index 1b1a5c8a3..a74e9611c 100644 --- a/frontend/src/components/signup/EnterEmailStep.tsx +++ b/frontend/src/components/auth/EnterEmailStep.tsx @@ -1,6 +1,6 @@ -import React, { useState } from "react"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; -import Link from "next/link"; +import { Link } from "@tanstack/react-router"; import axios from "axios"; import { createNotification } from "@app/components/notifications"; @@ -27,8 +27,7 @@ export default function EnterEmailStep({ setEmail, incrementStep }: DownloadBackupPDFStepProps): JSX.Element { - - const { mutateAsync, isLoading } = useSendVerificationEmail(); + const { mutateAsync, isPending } = useSendVerificationEmail(); const [emailError, setEmailError] = useState(false); const { t } = useTranslation(); @@ -51,7 +50,7 @@ export default function EnterEmailStep({ if (!emailCheckBool) { try { await mutateAsync({ email: email.toLowerCase() }); - setEmail(email.toLowerCase()) + setEmail(email.toLowerCase()); incrementStep(); } catch (e) { if (axios.isAxiosError(e)) { @@ -96,8 +95,8 @@ export default function EnterEmailStep({ className="h-14" colorSchema="primary" variant="outline_bg" - isLoading={isLoading} - isDisabled={isLoading} + isLoading={isPending} + isDisabled={isPending} > {" "} {String(t("signup.step1-submit"))}{" "} @@ -106,7 +105,7 @@ export default function EnterEmailStep({
- +
- + {t("signup.already-have-account")} diff --git a/frontend/src/components/auth/Mfa.tsx b/frontend/src/components/auth/Mfa.tsx new file mode 100644 index 000000000..3eff8a89c --- /dev/null +++ b/frontend/src/components/auth/Mfa.tsx @@ -0,0 +1,222 @@ +import React, { useEffect, useState } from "react"; +import ReactCodeInput from "react-code-input"; +import { Link, useNavigate } from "@tanstack/react-router"; +import { t } from "i18next"; + +import Error from "@app/components/basic/Error"; +import TotpRegistration from "@app/components/mfa/TotpRegistration"; +import SecurityClient from "@app/components/utilities/SecurityClient"; +import { Button, Input } from "@app/components/v2"; +import { useSendMfaToken } from "@app/hooks/api"; +import { checkUserTotpMfa, verifyMfaToken } from "@app/hooks/api/auth/queries"; +import { MfaMethod } from "@app/hooks/api/auth/types"; + +// The style for the verification code input +const codeInputProps = { + inputStyle: { + fontFamily: "monospace", + margin: "4px", + MozAppearance: "textfield", + width: "48px", + borderRadius: "5px", + fontSize: "24px", + height: "48px", + paddingLeft: "7", + backgroundColor: "#0d1117", + color: "white", + border: "1px solid #2d2f33", + textAlign: "center", + outlineColor: "#8ca542", + borderColor: "#2d2f33" + } +} as const; + +type Props = { + successCallback: () => void | Promise; + closeMfa?: () => void; + hideLogo?: boolean; + email: string; + method: MfaMethod; +}; + +export const Mfa = ({ successCallback, closeMfa, hideLogo, email, method }: Props) => { + const [mfaCode, setMfaCode] = useState(""); + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(false); + const [isLoadingResend, setIsLoadingResend] = useState(false); + const [triesLeft, setTriesLeft] = useState(undefined); + const [shouldShowTotpRegistration, setShouldShowTotpRegistration] = useState(false); + + const sendMfaToken = useSendMfaToken(); + + useEffect(() => { + if (method === MfaMethod.TOTP) { + checkUserTotpMfa().then((isVerified) => { + if (!isVerified) { + SecurityClient.setMfaToken(""); + setShouldShowTotpRegistration(true); + } + }); + } + }, []); + + const verifyMfa = async (event: React.FormEvent) => { + event.preventDefault(); + + setIsLoading(true); + try { + const { token } = await verifyMfaToken({ + email, + mfaCode, + mfaMethod: method + }); + + SecurityClient.setMfaToken(""); + SecurityClient.setToken(token); + + await successCallback(); + if (closeMfa) { + closeMfa(); + } + } catch { + if (triesLeft) { + setTriesLeft((left) => { + if (triesLeft === 1) { + navigate({ to: "/" }); + + SecurityClient.setMfaToken(""); + SecurityClient.setToken(""); + } + return (left as number) - 1; + }); + } else { + setTriesLeft(2); + } + } finally { + setIsLoading(false); + } + }; + + const handleResendMfaCode = async () => { + try { + setIsLoadingResend(true); + await sendMfaToken.mutateAsync({ email }); + setIsLoadingResend(false); + } catch (err) { + console.error(err); + setIsLoadingResend(false); + } + }; + + if (shouldShowTotpRegistration) { + return ( + <> +
+ Your organization requires mobile authentication to be configured. +
+
+ { + setShouldShowTotpRegistration(false); + await successCallback(); + }} + /> +
+ + ); + } + + return ( +
+ {!hideLogo && ( + +
+ Infisical logo +
+ + )} + {method === MfaMethod.EMAIL && ( + <> +

{t("mfa.step2-message")}

+

{email}

+ + )} + {method === MfaMethod.TOTP && ( + <> +

+ Authenticator MFA Required +

+

+ Open the authenticator app on your mobile device to get your verification code or enter + a recovery code. +

+ + )} +
+
+ {method === MfaMethod.EMAIL && ( + + )} + {method === MfaMethod.TOTP && ( +
+ setMfaCode(e.target.value)} /> +
+ )} +
+ {typeof triesLeft === "number" && ( + + )} +
+
+ +
+
+ + {method === MfaMethod.TOTP && ( +
+ + + Lost your recovery codes? Reset your account + + +
+ )} + {method === MfaMethod.EMAIL && ( +
+
+ {t("signup.step2-resend-alert")} +
+ +
+
+

{t("signup.step2-spam-alert")}

+
+ )} +
+ ); +}; diff --git a/frontend/src/components/signup/TeamInviteStep.tsx b/frontend/src/components/auth/TeamInviteStep.tsx similarity index 82% rename from frontend/src/components/signup/TeamInviteStep.tsx rename to frontend/src/components/auth/TeamInviteStep.tsx index 60276d217..5a9f11bde 100644 --- a/frontend/src/components/signup/TeamInviteStep.tsx +++ b/frontend/src/components/auth/TeamInviteStep.tsx @@ -1,9 +1,10 @@ -import React, { useState } from "react"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { useRouter } from "next/router"; +import { useNavigate } from "@tanstack/react-router"; import { useAddUsersToOrg } from "@app/hooks/api"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; +import { ProjectType } from "@app/hooks/api/workspace/types"; import { usePopUp } from "@app/hooks/usePopUp"; import { Button, EmailServiceSetupModal } from "../v2"; @@ -13,7 +14,7 @@ import { Button, EmailServiceSetupModal } from "../v2"; */ export default function TeamInviteStep(): JSX.Element { const { t } = useTranslation(); - const router = useRouter(); + const navigate = useNavigate(); const [emails, setEmails] = useState(""); const { data: serverDetails } = useFetchServerStatus(); @@ -22,7 +23,7 @@ export default function TeamInviteStep(): JSX.Element { // Redirect user to the getting started page const redirectToHome = async () => { - router.push(`/org/${localStorage.getItem("orgData.id")}/overview`); + navigate({ to: `/organization/${ProjectType.SecretManager}/overview` as const }); }; const inviteUsers = async ({ emails: inviteEmails }: { emails: string }) => { @@ -48,19 +49,19 @@ export default function TeamInviteStep(): JSX.Element {

{t("signup.step5-subtitle")}

-
+
-
+
Emails
+ + )} +
+ ); + })} +
+
+ +
+
+
+
Reviewers
+
+ {secretApprovalRequestDetails?.policy?.approvers + .filter( + (requiredApprover) => + !(shouldBlockSelfReview && requiredApprover.userId === userSession.id) + ) + .map((requiredApprover) => { + const reviewer = reviewedUsers?.[requiredApprover.userId]; + return ( +
+
+ + {requiredApprover?.email} + + * +
+
+ {reviewer?.comment && ( + + + + )} + + {getReviewedStatusSymbol(reviewer?.status)} + +
+
+ ); + })} + {secretApprovalRequestDetails?.reviewers + .filter( + (reviewer) => + !secretApprovalRequestDetails?.policy?.approvers?.some( + ({ userId }) => userId === reviewer.userId + ) + ) + .map((reviewer) => { + const status = reviewedUsers?.[reviewer.userId].status; + return ( +
+
+ + {reviewer?.email} + + * +
+
+ {reviewer.comment && ( + + + + )} + + {getReviewedStatusSymbol(status)} + +
+
+ ); + })} +
+
+
+ ); +}; diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/index.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/index.tsx similarity index 100% rename from frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/index.tsx rename to frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/index.tsx diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/route.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/route.tsx new file mode 100644 index 000000000..79a5c8fa6 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/route.tsx @@ -0,0 +1,26 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { zodValidator } from "@tanstack/zod-adapter"; +import { z } from "zod"; + +import { SecretApprovalsPage } from "./SecretApprovalsPage"; + +const SecretApprovalPageQueryParams = z.object({ + requestId: z.string().catch("") +}); + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/approval" +)({ + component: SecretApprovalsPage, + validateSearch: zodValidator(SecretApprovalPageQueryParams), + beforeLoad: ({ context }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Approvals" + } + ] + }; + } +}); diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx new file mode 100644 index 000000000..63398d152 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -0,0 +1,634 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Helmet } from "react-helmet"; +import { useTranslation } from "react-i18next"; +import { subject } from "@casl/ability"; +import { faArrowDown, faArrowUp } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useNavigate, useParams, useSearch } from "@tanstack/react-router"; +import { twMerge } from "tailwind-merge"; + +import { createNotification } from "@app/components/notifications"; +import { PermissionDeniedBanner } from "@app/components/permissions"; +import { + Checkbox, + ContentLoader, + Modal, + ModalContent, + Pagination, + Tooltip +} from "@app/components/v2"; +import { ROUTE_PATHS } from "@app/const/routes"; +import { + ProjectPermissionActions, + ProjectPermissionDynamicSecretActions, + ProjectPermissionSub, + useProjectPermission, + useWorkspace +} from "@app/context"; +import { + ProjectPermissionSecretActions, + ProjectPermissionSecretRotationActions +} from "@app/context/ProjectPermissionContext/types"; +import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; +import { + useGetImportedSecretsSingleEnv, + useGetSecretApprovalPolicyOfABoard, + useGetWorkspaceSnapshotList, + useGetWsSnapshotCount, + useGetWsTags +} from "@app/hooks/api"; +import { useGetProjectSecretsDetails } from "@app/hooks/api/dashboard"; +import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types"; +import { OrderByDirection } from "@app/hooks/api/generic/types"; +import { ProjectType } from "@app/hooks/api/workspace/types"; +import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; +import { SecretRotationListView } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView"; + +import { SecretTableResourceCount } from "../OverviewPage/components/SecretTableResourceCount"; +import { SecretV2MigrationSection } from "../OverviewPage/components/SecretV2MigrationSection"; +import { ActionBar } from "./components/ActionBar"; +import { CreateSecretForm } from "./components/CreateSecretForm"; +import { DynamicSecretListView } from "./components/DynamicSecretListView"; +import { FolderListView } from "./components/FolderListView"; +import { PitDrawer } from "./components/PitDrawer"; +import { SecretDropzone } from "./components/SecretDropzone"; +import { SecretImportListView } from "./components/SecretImportListView"; +import { SecretListView, SecretNoAccessListView } from "./components/SecretListView"; +import { SnapshotView } from "./components/SnapshotView"; +import { + PopUpNames, + StoreProvider, + usePopUpAction, + usePopUpState, + useSelectedSecretActions, + useSelectedSecrets +} from "./SecretMainPage.store"; +import { Filter, RowType } from "./SecretMainPage.types"; + +const LOADER_TEXT = [ + "Retrieving your encrypted secrets...", + "Fetching folders...", + "Getting secret import links..." +]; + +const Page = () => { + const { currentWorkspace } = useWorkspace(); + const navigate = useNavigate({ + from: ROUTE_PATHS.SecretManager.SecretDashboardPage.path + }); + const routerQueryParams = useSearch({ + from: ROUTE_PATHS.SecretManager.SecretDashboardPage.id + }); + const environment = useParams({ + from: ROUTE_PATHS.SecretManager.SecretDashboardPage.id, + select: (el) => el.envSlug + }); + + const { permission } = useProjectPermission(); + + const [isVisible, setIsVisible] = useState(false); + + const { + offset, + limit, + orderDirection, + setOrderDirection, + setPage, + perPage, + page, + setPerPage, + orderBy + } = usePagination(DashboardSecretsOrderBy.Name); + + const [snapshotId, setSnapshotId] = useState(null); + const isRollbackMode = Boolean(snapshotId); + const { popUp, handlePopUpClose, handlePopUpToggle } = usePopUp(["snapshots"] as const); + + // env slug + const workspaceId = currentWorkspace?.id || ""; + const projectSlug = currentWorkspace?.slug || ""; + const secretPath = (routerQueryParams.secretPath as string) || "/"; + + const canReadSecret = hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.DescribeSecret, + { + environment, + secretPath, + secretName: "*", + secretTags: ["*"] + } + ); + + const canReadSecretValue = hasSecretReadValueOrDescribePermission( + permission, + ProjectPermissionSecretActions.ReadValue, + { + environment, + secretPath, + secretName: "*", + secretTags: ["*"] + } + ); + + const canReadSecretImports = permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) + ); + + const canReadDynamicSecret = permission.can( + ProjectPermissionDynamicSecretActions.ReadRootCredential, + subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath, metadata: ["*"] }) + ); + + const canReadSecretRotations = permission.can( + ProjectPermissionSecretRotationActions.Read, + subject(ProjectPermissionSub.SecretRotation, { environment, secretPath }) + ); + + const canDoReadRollback = permission.can( + ProjectPermissionActions.Read, + ProjectPermissionSub.SecretRollback + ); + + const defaultFilterState = { + tags: {}, + searchFilter: (routerQueryParams.search as string) || "", + // these should always be on by default for the UI, they will be disabled for the query below based off permissions + include: { + [RowType.Folder]: true, + [RowType.Import]: true, + [RowType.DynamicSecret]: true, + [RowType.Secret]: true, + [RowType.SecretRotation]: true + } + }; + + const [filter, setFilter] = useState(defaultFilterState); + const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(filter.searchFilter); + const [filterHistory, setFilterHistory] = useState>(new Map()); + + const createSecretPopUp = usePopUpState(PopUpNames.CreateSecretForm); + const { togglePopUp } = usePopUpAction(); + + useEffect(() => { + if (!currentWorkspace?.environments.find((env) => env.slug === environment)) { + createNotification({ + text: "No environment found with given slug", + type: "error" + }); + navigate({ + to: `/${ProjectType.SecretManager}/$projectId/overview` as const, + params: { + projectId: workspaceId + } + }); + } + }, [currentWorkspace, environment]); + + const { + data, + isPending: isDetailsLoading, + isFetching: isDetailsFetching + } = useGetProjectSecretsDetails({ + environment, + projectId: workspaceId, + secretPath, + offset, + limit, + orderBy, + search: debouncedSearchFilter, + orderDirection, + includeImports: canReadSecretImports && filter.include.import, + includeFolders: filter.include.folder, + viewSecretValue: canReadSecretValue, + includeDynamicSecrets: canReadDynamicSecret && filter.include.dynamic, + includeSecrets: canReadSecret && filter.include.secret, + includeSecretRotations: canReadSecretRotations && filter.include.rotation, + tags: filter.tags + }); + + const { + imports, + folders, + dynamicSecrets, + secretRotations, + secrets, + totalImportCount = 0, + totalFolderCount = 0, + totalDynamicSecretCount = 0, + totalSecretCount = 0, + totalCount = 0, + importedBy, + totalSecretRotationCount = 0 + } = data ?? {}; + + useResetPageHelper({ + totalCount, + offset, + setPage + }); + + // fetch imported secrets to show user the overriden ones + const { data: importedSecrets } = useGetImportedSecretsSingleEnv({ + projectId: workspaceId, + environment, + path: secretPath, + options: { + enabled: canReadSecret + } + }); + + // fetch tags + const { data: tags } = useGetWsTags( + permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags) ? workspaceId : "" + ); + + const { data: boardPolicy } = useGetSecretApprovalPolicyOfABoard({ + workspaceId, + environment, + secretPath + }); + const isProtectedBranch = Boolean(boardPolicy); + + const { + data: snapshotList, + isFetchingNextPage: isFetchingNextSnapshotList, + fetchNextPage: fetchNextSnapshotList, + hasNextPage: hasNextSnapshotListPage + } = useGetWorkspaceSnapshotList({ + workspaceId, + directory: secretPath, + environment, + isPaused: !popUp.snapshots.isOpen || !canDoReadRollback, + limit: 10 + }); + + const { + data: snapshotCount, + isPending: isSnapshotCountLoading, + isFetching: isSnapshotCountFetching + } = useGetWsSnapshotCount({ + workspaceId, + environment, + directory: secretPath, + isPaused: !canDoReadRollback + }); + + const noAccessSecretCount = Math.max( + (page * perPage > totalCount ? totalCount % perPage : perPage) - + (imports?.length || 0) - + (folders?.length || 0) - + (secrets?.length || 0) - + (dynamicSecrets?.length || 0) - + (secretRotations?.length || 0), + 0 + ); + const isNotEmpty = Boolean( + secrets?.length || + folders?.length || + imports?.length || + dynamicSecrets?.length || + secretRotations?.length || + noAccessSecretCount + ); + + const handleSortToggle = () => + setOrderDirection((state) => + state === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC + ); + + const handleTagToggle = useCallback( + (tagSlug: string) => + setFilter((state) => { + const isTagPresent = Boolean(state.tags?.[tagSlug]); + const newTagFilter = { ...state.tags }; + if (isTagPresent) delete newTagFilter[tagSlug]; + else newTagFilter[tagSlug] = true; + return { ...state, tags: newTagFilter }; + }), + [] + ); + + const handleToggleRowType = useCallback( + (rowType: RowType) => + setFilter((state) => { + return { + ...state, + include: { + ...state.include, + [rowType]: !state.include[rowType] + } + }; + }), + [] + ); + + const handleSearchChange = useCallback( + (searchFilter: string) => setFilter((state) => ({ ...state, searchFilter })), + [] + ); + + const handleToggleVisibility = useCallback(() => setIsVisible((state) => !state), []); + + // snapshot functions + const handleSelectSnapshot = useCallback((snapId: string) => { + setSnapshotId(snapId); + }, []); + + const handleResetSnapshot = useCallback(() => { + setSnapshotId(null); + handlePopUpClose("snapshots"); + }, []); + + useEffect(() => { + // restore filters for path if set + const restore = filterHistory.get(secretPath); + setFilter(restore ?? defaultFilterState); + setDebouncedSearchFilter(restore?.searchFilter ?? ""); + }, [secretPath]); + + useEffect(() => { + if (!routerQueryParams.search && !routerQueryParams.tags) return; + + const queryTags = routerQueryParams.tags + ? (routerQueryParams.tags as string).split(",").filter((tag) => Boolean(tag.trim())) + : []; + const updatedTags: Record = {}; + queryTags.forEach((tag) => { + updatedTags[tag] = true; + }); + + setFilter((prev) => ({ + ...prev, + ...defaultFilterState, + searchFilter: (routerQueryParams.search as string) ?? "", + tags: updatedTags + })); + setDebouncedSearchFilter(routerQueryParams.search as string); + // this is a temp workaround until we fully transition state to query params, + navigate({ + search: (state) => { + const { search, tags: qTags, ...query } = state; + return query; + } + }); + }, [routerQueryParams.search, routerQueryParams.tags]); + + const selectedSecrets = useSelectedSecrets(); + const selectedSecretActions = useSelectedSecretActions(); + + const allRowsSelectedOnPage = useMemo(() => { + if (!secrets?.length) return { isChecked: false, isIndeterminate: false }; + + if (secrets?.every((secret) => selectedSecrets[secret.id])) + return { isChecked: true, isIndeterminate: false }; + + if (secrets?.some((secret) => selectedSecrets[secret.id])) + return { isChecked: true, isIndeterminate: true }; + + return { isChecked: false, isIndeterminate: false }; + }, [selectedSecrets, secrets]); + + const toggleSelectAllRows = () => { + const newChecks = { ...selectedSecrets }; + + secrets?.forEach((secret) => { + if (allRowsSelectedOnPage.isChecked) { + delete newChecks[secret.id]; + } else { + newChecks[secret.id] = secret; + } + }); + + selectedSecretActions.set(newChecks); + }; + + if (isDetailsLoading) { + return ; + } + + const handleResetFilter = () => { + // store for breadcrumb nav to restore previously used filters + setFilterHistory((prev) => { + const curr = new Map(prev); + curr.set(secretPath, filter); + return curr; + }); + + setFilter(defaultFilterState); + setDebouncedSearchFilter(""); + }; + return ( +
+ + {!isRollbackMode ? ( + <> + handlePopUpToggle("snapshots", true)} + protectedBranchPolicyName={boardPolicy?.name} + importedBy={importedBy} + /> +
+
+ {isNotEmpty && ( +
+ 0 + ? `${ + !allRowsSelectedOnPage.isChecked ? "Select" : "Unselect" + } all secrets on page` + : "" + } + > +
+ e.stopPropagation()} + isChecked={allRowsSelectedOnPage.isChecked} + isIndeterminate={allRowsSelectedOnPage.isIndeterminate} + onCheckedChange={toggleSelectAllRows} + /> +
+
+
{ + if (evt.key === "Enter") handleSortToggle(); + }} + > + Key + +
+
Value
+
+ )} + {canReadSecretImports && Boolean(imports?.length) && ( + + )} + {Boolean(folders?.length) && ( + + )} + {canReadDynamicSecret && Boolean(dynamicSecrets?.length) && ( + + )} + {canReadSecretRotations && Boolean(secretRotations?.length) && ( + + )} + {canReadSecret && Boolean(secrets?.length) && ( + + )} + {noAccessSecretCount > 0 && } + {!canReadSecret && + !canReadDynamicSecret && + !canReadSecretImports && + folders?.length === 0 && } +
+
+ {!isDetailsLoading && totalCount > 0 && ( + + } + className="rounded-b-md border-t border-solid border-t-mineshaft-600" + count={totalCount} + page={page} + perPage={perPage} + onChangePage={(newPage) => setPage(newPage)} + onChangePerPage={(newPerPage) => setPerPage(newPerPage)} + /> + )} + togglePopUp(PopUpNames.CreateSecretForm, state)} + > + + + + + + handlePopUpToggle("snapshots", isOpen)} + hasNextPage={hasNextSnapshotListPage} + fetchNextPage={fetchNextSnapshotList} + onSelectSnapshot={handleSelectSnapshot} + isFetchingNextPage={isFetchingNextSnapshotList} + /> + + ) : ( + handlePopUpToggle("snapshots", true)} + /> + )} +
+ ); +}; + +export const SecretDashboardPage = () => { + const { t } = useTranslation(); + + return ( + <> + + {t("common.head-title", { title: t("dashboard.title") })} + + + + + +
+ + + +
+ + ); +}; diff --git a/frontend/src/views/SecretMainPage/SecretMainPage.store.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretMainPage.store.tsx similarity index 78% rename from frontend/src/views/SecretMainPage/SecretMainPage.store.tsx rename to frontend/src/pages/secret-manager/SecretDashboardPage/SecretMainPage.store.tsx index 0b48d3db7..0e4ecca95 100644 --- a/frontend/src/views/SecretMainPage/SecretMainPage.store.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretMainPage.store.tsx @@ -1,6 +1,7 @@ import { createContext, ReactNode, useContext, useEffect, useRef } from "react"; -import { useRouter } from "next/router"; +import { useRouter } from "@tanstack/react-router"; import { createStore, StateCreator, StoreApi, useStore } from "zustand"; +import { useShallow } from "zustand/react/shallow"; import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types"; @@ -60,14 +61,13 @@ const createPopUpStore: StateCreator = (set) => ({ type CombinedState = SelectedSecretState & PopUpState; const StoreContext = createContext | null>(null); export const StoreProvider = ({ children }: { children: ReactNode }) => { - const storeRef = useRef>(); - const router = useRouter(); - if (!storeRef.current) { - storeRef.current = createStore((...a) => ({ + const storeRef = useRef>( + createStore((...a) => ({ ...createSelectedSecretStore(...a), ...createPopUpStore(...a) - })); - } + })) + ); + const router = useRouter(); useEffect(() => { const onRouteChangeStart = () => { @@ -75,26 +75,27 @@ export const StoreProvider = ({ children }: { children: ReactNode }) => { state?.action.reset(); }; - router.events.on("routeChangeStart", onRouteChangeStart); + const unsubscribe = router.subscribe("onBeforeLoad", onRouteChangeStart); return () => { - router.events.off("routeChangeStart", onRouteChangeStart); + unsubscribe(); }; }, []); return {children}; }; -const useStoreContext = (selector: (state: CombinedState) => T): T => { +const useStoreContext = (selector: (state: CombinedState) => T): T => { const ctx = useContext(StoreContext); if (!ctx) throw new Error("Missing "); return useStore(ctx, selector); }; // selected secret context -export const useSelectedSecrets = () => useStoreContext((state) => state.selectedSecret); -export const useSelectedSecretActions = () => useStoreContext((state) => state.action); +export const useSelectedSecrets = () => + useStoreContext(useShallow((state) => state.selectedSecret)); +export const useSelectedSecretActions = () => useStoreContext(useShallow((state) => state.action)); // popup context export const usePopUpState = (id: PopUpNames) => - useStoreContext((state) => state.popUp?.[id] || { isOpen: false }); -export const usePopUpAction = () => useStoreContext((state) => state.popUpActions); + useStoreContext(useShallow((state) => state.popUp?.[id] || { isOpen: false })); +export const usePopUpAction = () => useStoreContext(useShallow((state) => state.popUpActions)); diff --git a/frontend/src/views/SecretMainPage/SecretMainPage.types.ts b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretMainPage.types.ts similarity index 81% rename from frontend/src/views/SecretMainPage/SecretMainPage.types.ts rename to frontend/src/pages/secret-manager/SecretDashboardPage/SecretMainPage.types.ts index 848e8b91a..979f8de42 100644 --- a/frontend/src/views/SecretMainPage/SecretMainPage.types.ts +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretMainPage.types.ts @@ -10,5 +10,6 @@ export enum RowType { Folder = "folder", Import = "import", DynamicSecret = "dynamic", - Secret = "secret" + Secret = "secret", + SecretRotation = "rotation" } diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx new file mode 100644 index 000000000..4ac4ac804 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx @@ -0,0 +1,1164 @@ +import { TypeOptions } from "react-toastify"; +import { subject } from "@casl/ability"; +import { + faAngleDown, + faAnglesRight, + faCheckCircle, + faChevronRight, + faCodeCommit, + faDownload, + faEye, + faEyeSlash, + faFileImport, + faFilter, + faFingerprint, + faFolder, + faFolderPlus, + faKey, + faLock, + faMinusSquare, + faPaste, + faPlus, + faRotate, + faTrash +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useQueryClient } from "@tanstack/react-query"; +import { AxiosError } from "axios"; +import FileSaver from "file-saver"; +import { twMerge } from "tailwind-merge"; + +import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { CreateSecretRotationV2Modal } from "@app/components/secret-rotations-v2"; +import { + Button, + DeleteActionModal, + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, + DropdownSubMenu, + DropdownSubMenuContent, + DropdownSubMenuTrigger, + IconButton, + Modal, + ModalContent, + Tooltip +} from "@app/components/v2"; +import { + ProjectPermissionActions, + ProjectPermissionDynamicSecretActions, + ProjectPermissionSub, + useSubscription, + useWorkspace +} from "@app/context"; +import { ProjectPermissionSecretRotationActions } from "@app/context/ProjectPermissionContext/types"; +import { usePopUp } from "@app/hooks"; +import { + useCreateFolder, + useCreateSecretBatch, + useDeleteSecretBatch, + useMoveSecrets, + useUpdateSecretBatch +} from "@app/hooks/api"; +import { + dashboardKeys, + fetchDashboardProjectSecretsByKeys +} from "@app/hooks/api/dashboard/queries"; +import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries"; +import { fetchProjectSecrets, secretKeys } from "@app/hooks/api/secrets/queries"; +import { ApiErrorTypes, SecretType, TApiErrors, WsTag } from "@app/hooks/api/types"; +import { SecretSearchInput } from "@app/pages/secret-manager/OverviewPage/components/SecretSearchInput"; + +import { + PopUpNames, + usePopUpAction, + useSelectedSecretActions, + useSelectedSecrets +} from "../../SecretMainPage.store"; +import { Filter, RowType } from "../../SecretMainPage.types"; +import { CollapsibleSecretImports } from "../SecretListView/CollapsibleSecretImports"; +import { ReplicateFolderFromBoard } from "./ReplicateFolderFromBoard/ReplicateFolderFromBoard"; +import { CreateDynamicSecretForm } from "./CreateDynamicSecretForm"; +import { CreateSecretImportForm } from "./CreateSecretImportForm"; +import { FolderForm } from "./FolderForm"; +import { MoveSecretsModal } from "./MoveSecretsModal"; + +type TParsedEnv = Record; +type TParsedFolderEnv = Record< + string, + Record +>; +type TSecOverwriteOpt = { update: TParsedEnv; create: TParsedEnv }; + +type Props = { + // switch the secrets type as it gets decrypted after api call + environment: string; + // @depreciated will be moving all these details to zustand + workspaceId: string; + projectSlug: string; + secretPath?: string; + filter: Filter; + tags?: WsTag[]; + isVisible?: boolean; + snapshotCount: number; + isSnapshotCountLoading?: boolean; + protectedBranchPolicyName?: string; + onSearchChange: (term: string) => void; + onToggleTagFilter: (tagId: string) => void; + onVisibilityToggle: () => void; + onToggleRowType: (rowType: RowType) => void; + onClickRollbackMode: () => void; + importedBy?: { + environment: { name: string; slug: string }; + folders: { + name: string; + secrets?: { secretId: string; referencedSecretKey: string }[]; + isImported: boolean; + }[]; + }[]; +}; + +export const ActionBar = ({ + environment, + workspaceId, + projectSlug, + secretPath = "/", + filter, + tags = [], + isVisible, + snapshotCount, + isSnapshotCountLoading, + onSearchChange, + onToggleTagFilter, + onVisibilityToggle, + onClickRollbackMode, + onToggleRowType, + protectedBranchPolicyName, + importedBy +}: Props) => { + const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ + "addFolder", + "addDynamicSecret", + "addSecretImport", + "bulkDeleteSecrets", + "addSecretRotation", + "moveSecrets", + "misc", + "upgradePlan", + "replicateFolder", + "confirmUpload" + ] as const); + const isProtectedBranch = Boolean(protectedBranchPolicyName); + const { subscription } = useSubscription(); + const { openPopUp } = usePopUpAction(); + const { mutateAsync: createFolder } = useCreateFolder(); + const { mutateAsync: deleteBatchSecretV3 } = useDeleteSecretBatch(); + const { mutateAsync: moveSecrets } = useMoveSecrets(); + const { mutateAsync: updateSecretBatch, isPending: isUpdatingSecrets } = useUpdateSecretBatch({ + options: { onSuccess: undefined } + }); + const { mutateAsync: createSecretBatch, isPending: isCreatingSecrets } = useCreateSecretBatch({ + options: { onSuccess: undefined } + }); + const queryClient = useQueryClient(); + + const selectedSecrets = useSelectedSecrets(); + const { reset: resetSelectedSecret } = useSelectedSecretActions(); + const isMultiSelectActive = Boolean(Object.keys(selectedSecrets).length); + + const { currentWorkspace } = useWorkspace(); + + const handleFolderCreate = async (folderName: string, description: string | null) => { + try { + await createFolder({ + name: folderName, + path: secretPath, + environment, + projectId: workspaceId, + description + }); + handlePopUpClose("addFolder"); + createNotification({ + type: "success", + text: "Successfully created folder" + }); + } catch (error) { + console.log(error); + createNotification({ + type: "error", + text: "Failed to create folder" + }); + } + }; + + const handleSecretDownload = async () => { + try { + const { secrets: localSecrets, imports: localImportedSecrets } = await fetchProjectSecrets({ + workspaceId, + expandSecretReferences: true, + includeImports: true, + environment, + secretPath + }); + const secretsPicked = new Set(); + const secretsToDownload: { key: string; value?: string; comment?: string }[] = []; + localSecrets.forEach((el) => { + secretsPicked.add(el.secretKey); + secretsToDownload.push({ + key: el.secretKey, + value: el.secretValue, + comment: el.secretComment + }); + }); + + for (let i = localImportedSecrets.length - 1; i >= 0; i -= 1) { + for (let j = localImportedSecrets[i].secrets.length - 1; j >= 0; j -= 1) { + const secret = localImportedSecrets[i].secrets[j]; + if (!secretsPicked.has(secret.secretKey)) { + secretsToDownload.push({ + key: secret.secretKey, + value: secret.secretValue, + comment: secret.secretComment + }); + } + secretsPicked.add(secret.secretKey); + } + } + + const file = secretsToDownload + .sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase())) + .reduce( + (prev, { key, comment, value }, index) => + prev + + (comment + ? `${index === 0 ? "#" : "\n#"} ${comment}\n${key}=${value}\n` + : `${key}=${value}\n`), + "" + ); + + const blob = new Blob([file], { type: "text/plain;charset=utf-8" }); + FileSaver.saveAs(blob, `${environment}.env`); + } catch (err) { + if (err instanceof AxiosError) { + const error = err?.response?.data as TApiErrors; + + if (error?.error === ApiErrorTypes.ForbiddenError && error.message.includes("readValue")) { + createNotification({ + title: "You don't have permission to download secrets", + text: "You don't have permission to view one or more of the secrets in the current folder. Please contact your administrator.", + type: "error" + }); + return; + } + } + createNotification({ + title: "Failed to download secrets", + text: "Please try again later.", + type: "error" + }); + } + }; + + const handleSecretBulkDelete = async () => { + const bulkDeletedSecrets = Object.values(selectedSecrets); + try { + await deleteBatchSecretV3({ + secretPath, + workspaceId, + environment, + secrets: bulkDeletedSecrets.map(({ key }) => ({ secretKey: key, type: SecretType.Shared })) + }); + resetSelectedSecret(); + handlePopUpClose("bulkDeleteSecrets"); + createNotification({ + type: "success", + text: "Successfully deleted secrets" + }); + } catch (error) { + console.log(error); + createNotification({ + type: "error", + text: "Failed to delete secrets" + }); + } + }; + + const handleSecretsMove = async ({ + destinationEnvironment, + destinationSecretPath, + shouldOverwrite + }: { + destinationEnvironment: string; + destinationSecretPath: string; + shouldOverwrite: boolean; + }) => { + try { + const secretsToMove = Object.values(selectedSecrets); + const { isDestinationUpdated, isSourceUpdated } = await moveSecrets({ + projectSlug, + shouldOverwrite, + sourceEnvironment: environment, + sourceSecretPath: secretPath, + destinationEnvironment, + destinationSecretPath, + projectId: workspaceId, + secretIds: secretsToMove.map((sec) => sec.id) + }); + + let notificationMessage = ""; + let notificationType: TypeOptions = "info"; + + if (isDestinationUpdated && isSourceUpdated) { + notificationMessage = "Successfully moved selected secrets"; + notificationType = "success"; + } else if (isDestinationUpdated) { + notificationMessage = + "Successfully created secrets in destination. A secret approval request has been generated for the source."; + } else if (isSourceUpdated) { + notificationMessage = "A secret approval request has been generated in the destination"; + } else { + notificationMessage = + "A secret approval request has been generated in both the source and the destination."; + } + + createNotification({ + type: notificationType, + text: notificationMessage + }); + + resetSelectedSecret(); + } catch (error) { + console.error(error); + } + }; + + // Replicate Folder Logic + const createSecretCount = Object.keys( + (popUp.confirmUpload?.data as TSecOverwriteOpt)?.create || {} + ).length; + + const updateSecretCount = Object.keys( + (popUp.confirmUpload?.data as TSecOverwriteOpt)?.update || {} + ).length; + + const isNonConflictingUpload = !updateSecretCount; + const isSubmitting = isCreatingSecrets || isUpdatingSecrets; + + const handleParsedEnvMultiFolder = async (envByPath: TParsedFolderEnv) => { + if (Object.keys(envByPath).length === 0) { + createNotification({ + type: "error", + text: "Failed to find secrets" + }); + return; + } + + try { + const allUpdateSecrets: TParsedEnv = {}; + const allCreateSecrets: TParsedEnv = {}; + + await Promise.all( + Object.entries(envByPath).map(async ([folderPath, secrets]) => { + // Normalize the path + let normalizedPath = folderPath; + + // If the path is "/", use the current secretPath + if (normalizedPath === "/") { + normalizedPath = secretPath; + } else { + // Otherwise, concatenate with the current secretPath, avoiding double slashes + const baseSecretPath = secretPath.endsWith("/") ? secretPath.slice(0, -1) : secretPath; + // Remove leading slash from folder path if present to avoid double slashes + const cleanFolderPath = folderPath.startsWith("/") + ? folderPath.substring(1) + : folderPath; + normalizedPath = `${baseSecretPath}/${cleanFolderPath}`; + } + + const secretFolderKeys = Object.keys(secrets); + + if (secretFolderKeys.length === 0) return; + + // Check which secrets already exist in this path + const batchSize = 50; + const secretBatches = Array.from( + { length: Math.ceil(secretFolderKeys.length / batchSize) }, + (_, i) => secretFolderKeys.slice(i * batchSize, (i + 1) * batchSize) + ); + + const existingSecretLookup: Record = {}; + + const processBatches = async () => { + await secretBatches.reduce(async (previous, batch) => { + await previous; + + const { secrets: batchSecrets } = await fetchDashboardProjectSecretsByKeys({ + secretPath: normalizedPath, + environment, + projectId: workspaceId, + keys: batch + }); + + batchSecrets.forEach((secret) => { + existingSecretLookup[secret.secretKey] = true; + }); + }, Promise.resolve()); + }; + + await processBatches(); + + // Categorize each secret as update or create + secretFolderKeys.forEach((secretKey) => { + const secretData = secrets[secretKey]; + + // Store the path with the secret for later batch processing + const secretWithPath = { + ...secretData, + secretPath: normalizedPath + }; + + if (existingSecretLookup[secretKey]) { + allUpdateSecrets[secretKey] = secretWithPath; + } else { + allCreateSecrets[secretKey] = secretWithPath; + } + }); + }) + ); + + handlePopUpOpen("confirmUpload", { + update: allUpdateSecrets, + create: allCreateSecrets + }); + } catch (e) { + console.error(e); + createNotification({ + text: "Failed to check for secret conflicts", + type: "error" + }); + handlePopUpClose("confirmUpload"); + } + }; + + const handleSaveFolderImport = async () => { + const { update, create } = popUp?.confirmUpload?.data as TSecOverwriteOpt; + try { + // Group secrets by their path for batch operations + const groupedCreateSecrets: Record< + string, + Array<{ + type: SecretType; + secretComment: string; + secretValue: string; + secretKey: string; + }> + > = {}; + + const groupedUpdateSecrets: Record< + string, + Array<{ + type: SecretType; + secretComment: string; + secretValue: string; + secretKey: string; + }> + > = {}; + + // Collect all unique paths that need folders to be created + const allPaths = new Set(); + + // Add paths from create secrets + Object.values(create || {}).forEach((secData) => { + if (secData.secretPath && secData.secretPath !== secretPath) { + allPaths.add(secData.secretPath); + } + }); + + // Create a map of folder paths to their folder name (last segment) + const folderPaths = Array.from(allPaths).map((path) => { + // Remove trailing slash if it exists + const normalizedPath = path.endsWith("/") ? path.slice(0, -1) : path; + // Split by '/' to get path segments + const segments = normalizedPath.split("/"); + // Get the last segment as the folder name + const folderName = segments[segments.length - 1]; + // Get the parent path (everything except the last segment) + const parentPath = segments.slice(0, -1).join("/"); + + return { + folderName, + fullPath: normalizedPath, + parentPath: parentPath || "/" + }; + }); + + // Sort paths by depth (shortest first) to ensure parent folders are created before children + folderPaths.sort( + (a, b) => (a.fullPath.match(/\//g) || []).length - (b.fullPath.match(/\//g) || []).length + ); + + // Track created folders to avoid duplicates + const createdFolders = new Set(); + + // Create all necessary folders in order using Promise.all and reduce + await folderPaths.reduce(async (previousPromise, { folderName, fullPath, parentPath }) => { + // Wait for the previous promise to complete + await previousPromise; + + // Skip if we've already created this folder + if (createdFolders.has(fullPath)) return Promise.resolve(); + + try { + await createFolder({ + name: folderName, + path: parentPath, + environment, + projectId: workspaceId + }); + + createdFolders.add(fullPath); + } catch (err) { + console.log(`Folder ${folderName} may already exist:`, err); + } + + return Promise.resolve(); + }, Promise.resolve()); + + if (Object.keys(create || {}).length > 0) { + Object.entries(create).forEach(([secretKey, secData]) => { + // Use the stored secretPath or fall back to the current secretPath + const path = secData.secretPath || secretPath; + + if (!groupedCreateSecrets[path]) { + groupedCreateSecrets[path] = []; + } + + groupedCreateSecrets[path].push({ + type: SecretType.Shared, + secretComment: secData.comments.join("\n"), + secretValue: secData.value, + secretKey + }); + }); + + await Promise.all( + Object.entries(groupedCreateSecrets).map(([path, secrets]) => + createSecretBatch({ + secretPath: path, + workspaceId, + environment, + secrets + }) + ) + ); + } + + if (Object.keys(update || {}).length > 0) { + Object.entries(update).forEach(([secretKey, secData]) => { + // Use the stored secretPath or fall back to the current secretPath + const path = secData.secretPath || secretPath; + + if (!groupedUpdateSecrets[path]) { + groupedUpdateSecrets[path] = []; + } + + groupedUpdateSecrets[path].push({ + type: SecretType.Shared, + secretComment: secData.comments.join("\n"), + secretValue: secData.value, + secretKey + }); + }); + + // Update secrets for each path in parallel + await Promise.all( + Object.entries(groupedUpdateSecrets).map(([path, secrets]) => + updateSecretBatch({ + secretPath: path, + workspaceId, + environment, + secrets + }) + ) + ); + } + + // Invalidate appropriate queries to refresh UI + queryClient.invalidateQueries({ + queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }) + }); + queryClient.invalidateQueries({ + queryKey: dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath }) + }); + queryClient.invalidateQueries({ + queryKey: secretApprovalRequestKeys.count({ workspaceId }) + }); + + // Close the modal and show notification + handlePopUpClose("confirmUpload"); + createNotification({ + type: "success", + text: isProtectedBranch + ? "Uploaded changes have been sent for review" + : "Successfully uploaded secrets" + }); + } catch (err) { + console.log(err); + createNotification({ + type: "error", + text: "Failed to upload secrets" + }); + } + }; + + return ( + <> +
+ env.slug === environment)!]} + projectId={workspaceId} + tags={tags} + /> +
+ + + !include).length) && + "border-primary/50 text-primary" + )} + > + + + + + Filter By + { + e.preventDefault(); + onToggleRowType(RowType.Import); + }} + icon={filter?.include[RowType.Import] && } + iconPos="right" + > +
+ + Imports +
+
+ { + e.preventDefault(); + onToggleRowType(RowType.Folder); + }} + icon={filter?.include[RowType.Folder] && } + iconPos="right" + > +
+ + Folders +
+
+ { + e.preventDefault(); + onToggleRowType(RowType.DynamicSecret); + }} + icon={ + filter?.include[RowType.DynamicSecret] && + } + iconPos="right" + > +
+ + Dynamic Secrets +
+
+ { + e.preventDefault(); + onToggleRowType(RowType.SecretRotation); + }} + icon={ + filter?.include[RowType.SecretRotation] && ( + + ) + } + iconPos="right" + > +
+ + Secret Rotations +
+
+ { + e.preventDefault(); + onToggleRowType(RowType.Secret); + }} + icon={filter?.include[RowType.Secret] && } + iconPos="right" + > +
+ + Secrets +
+
+ + } + > + Tags + + + + Apply Tags to Filter Secrets + + {tags.map(({ id, slug, color }) => ( + { + evt.preventDefault(); + onToggleTagFilter(slug); + }} + key={id} + icon={filter?.tags[slug] && } + iconPos="right" + > +
+
+ {slug} +
+ + ))} + + + + +
+
+ {isProtectedBranch && ( + + + + + + )} +
+
+
+ + + +
+
+ + + +
+
+ + {(isAllowed) => ( + + )} + +
+
+ + {(isAllowed) => ( + + )} + + handlePopUpToggle("misc", isOpen)} + > + + + + + + +
+ + {(isAllowed) => ( + + )} + + + {(isAllowed) => ( + + )} + + + {(isAllowed) => ( + + )} + + + {(isAllowed) => ( + + )} + + + {(isAllowed) => ( + + )} + +
+
+
+
+
+
+
+ + + + + +
+ {Object.keys(selectedSecrets).length} Selected +
+ + {(isAllowed) => ( + + )} + + + {(isAllowed) => ( + + )} + +
+
+ {/* all the side triggers from actions like modals etc */} + handlePopUpOpen("upgradePlan")} + isOpen={popUp.addSecretImport.isOpen} + onClose={() => handlePopUpClose("addSecretImport")} + onTogglePopUp={(isOpen) => handlePopUpToggle("addSecretImport", isOpen)} + /> + handlePopUpToggle("addDynamicSecret", isOpen)} + projectSlug={projectSlug} + environments={[{ slug: environment, name: environment, id: "not-used" }]} + secretPath={secretPath} + isSingleEnvironmentMode + /> + handlePopUpToggle("addSecretRotation", isOpen)} + /> + handlePopUpToggle("addFolder", isOpen)} + > + + + + + handlePopUpToggle("bulkDeleteSecrets", isOpen)} + onDeleteApproved={handleSecretBulkDelete} + formContent={ + importedBy && + importedBy.length > 0 && ( + s.key)} + /> + ) + } + /> + + handlePopUpToggle("replicateFolder", isOpen)} + onParsedEnv={handleParsedEnvMultiFolder} + environment={environment} + environments={currentWorkspace.environments} + workspaceId={workspaceId} + secretPath={secretPath} + /> + {subscription && ( + handlePopUpToggle("upgradePlan", isOpen)} + text={ + subscription.slug === null + ? "You can perform this action under an Enterprise license" + : "You can perform this action if you switch to Infisical's Team plan" + } + /> + )} + handlePopUpToggle("confirmUpload", open)} + > + + {isNonConflictingUpload ? "Upload" : "Overwrite"} + , + + ]} + > + {isNonConflictingUpload ? ( +
+ Are you sure you want to import {createSecretCount} secret + {createSecretCount > 1 ? "s" : ""} to this environment? +
+ ) : ( +
+
Your project already contains the following {updateSecretCount} secrets:
+
+ {Object.keys((popUp?.confirmUpload?.data as TSecOverwriteOpt)?.update || {}) + ?.map((key) => key) + .join(", ")} +
+
+ Are you sure you want to overwrite these secrets + {createSecretCount > 0 + ? ` and import ${createSecretCount} new + one${createSecretCount > 1 ? "s" : ""}` + : ""} + ? +
+
+ )} +
+
+ + ); +}; + +ActionBar.displayName = "ActionBar"; diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AwsElastiCacheInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsElastiCacheInputForm.tsx similarity index 87% rename from frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AwsElastiCacheInputForm.tsx rename to frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsElastiCacheInputForm.tsx index 72db480db..1c513d44a 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AwsElastiCacheInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsElastiCacheInputForm.tsx @@ -11,6 +11,7 @@ import { AccordionItem, AccordionTrigger, Button, + FilterableSelect, FormControl, Input, SecretInput, @@ -18,6 +19,7 @@ import { } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; const formSchema = z.object({ provider: z.object({ @@ -50,7 +52,8 @@ const formSchema = z.object({ if (valMs > 24 * 60 * 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), - name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environment: z.object({ name: z.string(), slug: z.string() }) }); type TForm = z.infer; @@ -59,19 +62,21 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; + isSingleEnvironmentMode?: boolean; }; export const AwsElastiCacheInputForm = ({ onCompleted, onCancel, - environment, + environments, secretPath, - projectSlug + projectSlug, + isSingleEnvironmentMode }: Props) => { const { control, - formState: { isSubmitting, errors }, + formState: { isSubmitting }, handleSubmit } = useForm({ resolver: zodResolver(formSchema), @@ -87,16 +92,22 @@ export const AwsElastiCacheInputForm = ({ revocationStatement: `{ "UserId": "{{username}}" }` - } + }, + environment: isSingleEnvironmentMode ? environments[0] : undefined } }); const createDynamicSecret = useCreateDynamicSecret(); - console.log("formState", errors); - const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + const handleCreateDynamicSecret = async ({ + name, + maxTTL, + provider, + defaultTTL, + environment + }: TForm) => { // wait till previous request is finished - if (createDynamicSecret.isLoading) return; + if (createDynamicSecret.isPending) return; try { await createDynamicSecret.mutateAsync({ provider: { type: DynamicSecretProviders.AwsElastiCache, inputs: provider }, @@ -105,10 +116,10 @@ export const AwsElastiCacheInputForm = ({ path: secretPath, defaultTTL, projectSlug, - environmentSlug: environment + environmentSlug: environment.slug }); onCompleted(); - } catch (err) { + } catch { createNotification({ type: "error", text: "Failed to create dynamic secret" @@ -300,6 +311,28 @@ export const AwsElastiCacheInputForm = ({ + {!isSingleEnvironmentMode && ( + ( + + option.name} + getOptionValue={(option) => option.slug} + /> + + )} + /> + )}
diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx similarity index 85% rename from frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx rename to frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx index d1ff003c3..f2458bbf5 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx @@ -5,9 +5,10 @@ import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, Input, TextArea } from "@app/components/v2"; +import { Button, FilterableSelect, FormControl, Input, TextArea } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; const formSchema = z.object({ provider: z.object({ @@ -40,7 +41,8 @@ const formSchema = z.object({ if (valMs > 24 * 60 * 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), - name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environment: z.object({ name: z.string(), slug: z.string() }) }); type TForm = z.infer; @@ -49,29 +51,41 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; + isSingleEnvironmentMode?: boolean; }; export const AwsIamInputForm = ({ onCompleted, onCancel, - environment, + environments, secretPath, - projectSlug + projectSlug, + isSingleEnvironmentMode }: Props) => { const { control, formState: { isSubmitting }, handleSubmit } = useForm({ - resolver: zodResolver(formSchema) + resolver: zodResolver(formSchema), + defaultValues: { + environment: isSingleEnvironmentMode ? environments[0] : undefined + } }); const createDynamicSecret = useCreateDynamicSecret(); - const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + const handleCreateDynamicSecret = async ({ + name, + maxTTL, + provider, + defaultTTL, + environment + }: TForm) => { // wait till previous request is finished - if (createDynamicSecret.isLoading) return; + if (createDynamicSecret.isPending) return; + try { await createDynamicSecret.mutateAsync({ provider: { type: DynamicSecretProviders.AwsIam, inputs: provider }, @@ -80,10 +94,10 @@ export const AwsIamInputForm = ({ path: secretPath, defaultTTL, projectSlug, - environmentSlug: environment + environmentSlug: environment.slug }); onCompleted(); - } catch (err) { + } catch { createNotification({ type: "error", text: "Failed to create dynamic secret" @@ -286,6 +300,29 @@ export const AwsIamInputForm = ({ )} /> + {!isSingleEnvironmentMode && ( + ( + + option.name} + getOptionValue={(option) => option.slug} + menuPlacement="top" + /> + + )} + /> + )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx new file mode 100644 index 000000000..2fb68f399 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx @@ -0,0 +1,419 @@ +import { Controller, useForm } from "react-hook-form"; +import { + faArrowUpRightFromSquare, + faBookOpen, + faCheckCircle, + faWarning +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import ms from "ms"; +import { z } from "zod"; + +import { TtlFormLabel } from "@app/components/features"; +import { createNotification } from "@app/components/notifications"; +import { Button, FilterableSelect, FormControl, Input } from "@app/components/v2"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from "@app/components/v2/Dropdown/Dropdown"; +import { Tooltip } from "@app/components/v2/Tooltip"; +import { useCreateDynamicSecret } from "@app/hooks/api"; +import { useGetDynamicSecretProviderData } from "@app/hooks/api/dynamicSecret/queries"; +import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; + +const formSchema = z.object({ + selectedUsers: z.array( + z.object({ + id: z.string().min(1), + name: z.string().min(1), + email: z.string().min(1) + }) + ), + provider: z.object({ + tenantId: z.string().min(1), + applicationId: z.string().min(1), + clientSecret: z.string().min(1) + }), + defaultTTL: z.string().superRefine((val, ctx) => { + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + maxTTL: z + .string() + .optional() + .superRefine((val, ctx) => { + if (!val) return; + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + name: z + .string() + .min(1) + .refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environment: z.object({ name: z.string(), slug: z.string() }) +}); +type TForm = z.infer; + +type Props = { + onCompleted: () => void; + onCancel: () => void; + secretPath: string; + projectSlug: string; + environments: WorkspaceEnv[]; + isSingleEnvironmentMode?: boolean; +}; + +export const AzureEntraIdInputForm = ({ + onCompleted, + onCancel, + environments, + secretPath, + projectSlug, + isSingleEnvironmentMode +}: Props) => { + const { + control, + formState: { isSubmitting }, + watch, + handleSubmit + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + environment: isSingleEnvironmentMode ? environments[0] : undefined + } + }); + const tenantId = watch("provider.tenantId"); + const applicationId = watch("provider.applicationId"); + const clientSecret = watch("provider.clientSecret"); + + const configurationComplete = !!(tenantId && applicationId && clientSecret); + const { data, isLoading, isError, isFetching } = useGetDynamicSecretProviderData({ + tenantId, + applicationId, + clientSecret, + enabled: !!configurationComplete + }); + const loading = configurationComplete && isFetching; + const errored = configurationComplete && !isFetching && isError; + const createDynamicSecret = useCreateDynamicSecret(); + + const handleCreateDynamicSecret = async ({ + name, + selectedUsers, + provider, + maxTTL, + defaultTTL, + environment + }: TForm) => { + // wait till previous request is finished + if (createDynamicSecret.isPending) return; + try { + selectedUsers.map(async (user: { id: string; name: string; email: string }) => { + await createDynamicSecret.mutateAsync({ + provider: { + type: DynamicSecretProviders.AzureEntraId, + inputs: { + userId: user.id, + tenantId: provider.tenantId, + email: user.email, + applicationId: provider.applicationId, + clientSecret: provider.clientSecret + } + }, + maxTTL, + name: `${name}-${user.name}`, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); + }); + onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); + } + }; + + return ( +
+
+
+
+
+ ( + + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+
+ +
+
+ ( + + + + )} + /> +
+
+
+
+ ( + + + + )} + /> +
+
+
+
+ ( + + + + )} + /> +
+
+
+
+
+ Select Users +
+
+   We create a unique dynamic secret for each user in Entra Id. +
+
+
+ ( + + + +
+ } + > +
+ +
+ + + + {data && + data.map((user) => { + const ids = value?.map((selectedUser) => selectedUser.id); + const isChecked = ids?.includes(user.id); + return ( + { + evt.preventDefault(); + onChange( + isChecked + ? value?.filter((el) => el.id !== user.id) + : [...(value || []), user] + ); + }} + key={`create-policy-members-${user.id}`} + iconPos="right" + icon={isChecked && } + > + {user.name}
{`(${user.email})`} +
+ ); + })} +
+ + + )} + /> +
+
+
+ {!isSingleEnvironmentMode && ( + ( + + option.name} + getOptionValue={(option) => option.slug} + menuPlacement="top" + /> + + )} + /> + )} +
+
+ + +
+ +
+ ); +}; diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CassandraInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CassandraInputForm.tsx similarity index 88% rename from frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CassandraInputForm.tsx rename to frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CassandraInputForm.tsx index 950e7b2aa..b50f9f04b 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CassandraInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CassandraInputForm.tsx @@ -11,6 +11,7 @@ import { AccordionItem, AccordionTrigger, Button, + FilterableSelect, FormControl, Input, SecretInput, @@ -18,6 +19,7 @@ import { } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; const formSchema = z.object({ provider: z.object({ @@ -52,7 +54,8 @@ const formSchema = z.object({ if (valMs > 24 * 60 * 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), - name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environment: z.object({ name: z.string(), slug: z.string() }) }); type TForm = z.infer; @@ -61,7 +64,8 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; + isSingleEnvironmentMode?: boolean; }; const getSqlStatements = () => { @@ -76,9 +80,10 @@ const getSqlStatements = () => { export const CassandraInputForm = ({ onCompleted, onCancel, - environment, + environments, secretPath, - projectSlug + projectSlug, + isSingleEnvironmentMode }: Props) => { const { control, @@ -87,15 +92,23 @@ export const CassandraInputForm = ({ } = useForm({ resolver: zodResolver(formSchema), defaultValues: { - provider: getSqlStatements() + provider: getSqlStatements(), + environment: isSingleEnvironmentMode ? environments[0] : undefined } }); const createDynamicSecret = useCreateDynamicSecret(); - const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + const handleCreateDynamicSecret = async ({ + name, + maxTTL, + provider, + defaultTTL, + environment + }: TForm) => { // wait till previous request is finished - if (createDynamicSecret.isLoading) return; + if (createDynamicSecret.isPending) return; + try { await createDynamicSecret.mutateAsync({ provider: { type: DynamicSecretProviders.Cassandra, inputs: provider }, @@ -104,10 +117,10 @@ export const CassandraInputForm = ({ path: secretPath, defaultTTL, projectSlug, - environmentSlug: environment + environmentSlug: environment.slug }); onCompleted(); - } catch (err) { + } catch { createNotification({ type: "error", text: "Failed to create dynamic secret" @@ -345,6 +358,29 @@ export const CassandraInputForm = ({
+ {!isSingleEnvironmentMode && ( + ( + + option.name} + getOptionValue={(option) => option.slug} + menuPlacement="top" + /> + + )} + /> + )} diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx similarity index 79% rename from frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx rename to frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx index 490ad6f48..106a658d4 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx @@ -4,20 +4,20 @@ import { SiApachecassandra, SiElasticsearch, SiFiles, - SiMicrosoftazure, SiMongodb, SiRabbitmq, SiSap, SiSnowflake } from "react-icons/si"; +import { VscAzure } from "react-icons/vsc"; import { faAws } from "@fortawesome/free-brands-svg-icons"; -import { faDatabase } from "@fortawesome/free-solid-svg-icons"; +import { faClock, faDatabase } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { AnimatePresence, motion } from "framer-motion"; import { Modal, ModalContent } from "@app/components/v2"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; -import { SnowflakeInputForm } from "@app/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm"; +import { WorkspaceEnv } from "@app/hooks/api/types"; import { AwsElastiCacheInputForm } from "./AwsElastiCacheInputForm"; import { AwsIamInputForm } from "./AwsIamInputForm"; @@ -29,15 +29,19 @@ import { MongoAtlasInputForm } from "./MongoAtlasInputForm"; import { MongoDBDatabaseInputForm } from "./MongoDBInputForm"; import { RabbitMqInputForm } from "./RabbitMqInputForm"; import { RedisInputForm } from "./RedisInputForm"; +import { SapAseInputForm } from "./SapAseInputForm"; import { SapHanaInputForm } from "./SapHanaInputForm"; +import { SnowflakeInputForm } from "./SnowflakeInputForm"; import { SqlDatabaseInputForm } from "./SqlDatabaseInputForm"; +import { TotpInputForm } from "./TotpInputForm"; type Props = { isOpen?: boolean; onToggle: (isOpen: boolean) => void; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; secretPath: string; + isSingleEnvironmentMode?: boolean; }; enum WizardSteps { @@ -92,7 +96,7 @@ const DYNAMIC_SECRET_LIST = [ title: "RabbitMQ" }, { - icon: , + icon: , provider: DynamicSecretProviders.AzureEntraId, title: "Azure Entra ID" }, @@ -106,10 +110,20 @@ const DYNAMIC_SECRET_LIST = [ provider: DynamicSecretProviders.SapHana, title: "SAP HANA" }, + { + icon: , + provider: DynamicSecretProviders.SapAse, + title: "SAP ASE" + }, { icon: , provider: DynamicSecretProviders.Snowflake, title: "Snowflake" + }, + { + icon: , + provider: DynamicSecretProviders.Totp, + title: "TOTP" } ]; @@ -117,8 +131,9 @@ export const CreateDynamicSecretForm = ({ isOpen, onToggle, projectSlug, - environment, - secretPath + environments, + secretPath, + isSingleEnvironmentMode }: Props) => { const [wizardStep, setWizardStep] = useState(WizardSteps.SelectProvider); const [selectedProvider, setSelectedProvider] = useState(null); @@ -136,7 +151,7 @@ export const CreateDynamicSecretForm = ({ subTitle="Configure dynamic secret parameters" className="my-4 max-w-3xl" > - + {wizardStep === WizardSteps.SelectProvider && ( )} @@ -203,7 +219,8 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} @@ -221,7 +238,8 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} @@ -239,7 +257,8 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} @@ -257,7 +276,8 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} @@ -275,7 +295,8 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} @@ -293,7 +314,8 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} @@ -311,7 +333,8 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} @@ -329,7 +352,8 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} @@ -347,7 +371,8 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} @@ -365,7 +390,8 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} @@ -383,10 +409,31 @@ export const CreateDynamicSecretForm = ({ onCancel={handleFormReset} projectSlug={projectSlug} secretPath={secretPath} - environment={environment} + environments={environments} + isSingleEnvironmentMode={isSingleEnvironmentMode} /> )} + + {wizardStep === WizardSteps.ProviderInputs && + selectedProvider === DynamicSecretProviders.SapAse && ( + + + + )} + {wizardStep === WizardSteps.ProviderInputs && selectedProvider === DynamicSecretProviders.Snowflake && ( + + )} + {wizardStep === WizardSteps.ProviderInputs && + selectedProvider === DynamicSecretProviders.Totp && ( + + )} diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx similarity index 88% rename from frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx rename to frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx index ac232a725..a7c84cb50 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx @@ -1,5 +1,4 @@ import { Controller, useForm } from "react-hook-form"; -import Link from "next/link"; import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -10,6 +9,7 @@ import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; import { Button, + FilterableSelect, FormControl, FormLabel, IconButton, @@ -20,6 +20,7 @@ import { } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; const authMethods = [ { @@ -74,7 +75,8 @@ const formSchema = z.object({ if (valMs > 24 * 60 * 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), - name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environment: z.object({ name: z.string(), slug: z.string() }) }); type TForm = z.infer; @@ -83,15 +85,17 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; + isSingleEnvironmentMode?: boolean; }; export const ElasticSearchInputForm = ({ onCompleted, onCancel, - environment, + environments, secretPath, - projectSlug + projectSlug, + isSingleEnvironmentMode }: Props) => { const { control, @@ -108,15 +112,22 @@ export const ElasticSearchInputForm = ({ }, roles: ["superuser"], port: 443 - } + }, + environment: isSingleEnvironmentMode ? environments[0] : undefined } }); const createDynamicSecret = useCreateDynamicSecret(); - const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + const handleCreateDynamicSecret = async ({ + name, + maxTTL, + provider, + defaultTTL, + environment + }: TForm) => { // wait till previous request is finished - if (createDynamicSecret.isLoading) return; + if (createDynamicSecret.isPending) return; try { await createDynamicSecret.mutateAsync({ provider: { type: DynamicSecretProviders.ElasticSearch, inputs: provider }, @@ -125,10 +136,10 @@ export const ElasticSearchInputForm = ({ path: secretPath, defaultTTL, projectSlug, - environmentSlug: environment + environmentSlug: environment.slug }); onCompleted(); - } catch (err) { + } catch { createNotification({ type: "error", text: "Failed to create dynamic secret" @@ -319,16 +330,15 @@ export const ElasticSearchInputForm = ({

There is a wide range of in-built roles in Elastic Search. Some include, superuser, apm_user, kibana_admin, monitoring_user, and many more. You can{" "} - - - - read more about roles here - - - + + read more about roles here + + .

@@ -408,6 +418,29 @@ export const ElasticSearchInputForm = ({ )} /> + {!isSingleEnvironmentMode && ( + ( + + option.name} + getOptionValue={(option) => option.slug} + menuPlacement="top" + /> + + )} + /> + )} diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx similarity index 84% rename from frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx rename to frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx index 1a7e0e8fa..18f8c62dc 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx @@ -1,5 +1,4 @@ import { Controller, useForm } from "react-hook-form"; -import Link from "next/link"; import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -8,9 +7,18 @@ import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, Input, Select, SelectItem, TextArea } from "@app/components/v2"; +import { + Button, + FilterableSelect, + FormControl, + Input, + Select, + SelectItem, + TextArea +} from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; enum CredentialType { Dynamic = "dynamic", @@ -70,7 +78,8 @@ const formSchema = z.object({ if (valMs > 24 * 60 * 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), - name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environment: z.object({ name: z.string(), slug: z.string() }) }); type TForm = z.infer; @@ -80,7 +89,8 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; + isSingleEnvironmentMode?: boolean; }; export const LdapInputForm = ({ @@ -88,7 +98,8 @@ export const LdapInputForm = ({ onCancel, secretPath, projectSlug, - environment + environments, + isSingleEnvironmentMode }: Props) => { const { control, @@ -108,7 +119,8 @@ export const LdapInputForm = ({ revocationLdif: "", rollbackLdif: "", credentialType: CredentialType.Dynamic - } + }, + environment: isSingleEnvironmentMode ? environments[0] : undefined } }); @@ -116,9 +128,15 @@ export const LdapInputForm = ({ const createDynamicSecret = useCreateDynamicSecret(); - const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + const handleCreateDynamicSecret = async ({ + name, + maxTTL, + provider, + defaultTTL, + environment + }: TForm) => { // wait till previous request is finished - if (createDynamicSecret.isLoading) return; + if (createDynamicSecret.isPending) return; try { await createDynamicSecret.mutateAsync({ provider: { type: DynamicSecretProviders.Ldap, inputs: provider }, @@ -127,10 +145,10 @@ export const LdapInputForm = ({ path: secretPath, defaultTTL, projectSlug, - environmentSlug: environment + environmentSlug: environment.slug }); onCompleted(); - } catch (err) { + } catch { createNotification({ type: "error", text: "Failed to create dynamic secret" @@ -194,21 +212,20 @@ export const LdapInputForm = ({

Configuration - - -
- - Docs - -
-
- +
+ + Docs + +
+
@@ -371,6 +388,29 @@ export const LdapInputForm = ({ )} /> )} + {!isSingleEnvironmentMode && ( + ( + + option.name} + getOptionValue={(option) => option.slug} + menuPlacement="top" + /> + + )} + /> + )}
diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx similarity index 91% rename from frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx rename to frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx index 14f529fe0..821802954 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx @@ -13,6 +13,7 @@ import { AccordionItem, AccordionTrigger, Button, + FilterableSelect, FormControl, FormLabel, IconButton, @@ -22,6 +23,7 @@ import { } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; const formSchema = z.object({ provider: z.object({ @@ -63,7 +65,8 @@ const formSchema = z.object({ if (valMs > 24 * 60 * 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), - name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environment: z.object({ name: z.string(), slug: z.string() }) }); type TForm = z.infer; @@ -72,7 +75,8 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; + isSingleEnvironmentMode?: boolean; }; const ATLAS_SCOPE_TYPES = [ @@ -93,9 +97,10 @@ const ATLAS_SCOPE_TYPES = [ export const MongoAtlasInputForm = ({ onCompleted, onCancel, - environment, + environments, secretPath, - projectSlug + projectSlug, + isSingleEnvironmentMode }: Props) => { const { control, @@ -108,7 +113,8 @@ export const MongoAtlasInputForm = ({ defaultValues: { provider: { roles: [{ databaseName: "", roleName: "" }] - } + }, + environment: isSingleEnvironmentMode ? environments[0] : undefined } }); @@ -124,9 +130,15 @@ export const MongoAtlasInputForm = ({ const createDynamicSecret = useCreateDynamicSecret(); - const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + const handleCreateDynamicSecret = async ({ + name, + maxTTL, + provider, + defaultTTL, + environment + }: TForm) => { // wait till previous request is finished - if (createDynamicSecret.isLoading) return; + if (createDynamicSecret.isPending) return; try { await createDynamicSecret.mutateAsync({ provider: { type: DynamicSecretProviders.MongoAtlas, inputs: provider }, @@ -135,10 +147,10 @@ export const MongoAtlasInputForm = ({ path: secretPath, defaultTTL, projectSlug, - environmentSlug: environment + environmentSlug: environment.slug }); onCompleted(); - } catch (err) { + } catch { createNotification({ type: "error", text: "Failed to create dynamic secret" @@ -438,6 +450,29 @@ export const MongoAtlasInputForm = ({ + {!isSingleEnvironmentMode && ( + ( + + option.name} + getOptionValue={(option) => option.slug} + menuPlacement="top" + /> + + )} + /> + )} diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx similarity index 87% rename from frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx rename to frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx index d24850ca8..789338c87 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx @@ -7,9 +7,18 @@ import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, FormLabel, IconButton, Input, SecretInput } from "@app/components/v2"; +import { + Button, + FilterableSelect, + FormControl, + FormLabel, + IconButton, + Input, + SecretInput +} from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; const formSchema = z.object({ provider: z.object({ @@ -46,7 +55,8 @@ const formSchema = z.object({ if (valMs > 24 * 60 * 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), - name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environment: z.object({ name: z.string(), slug: z.string() }) }); type TForm = z.infer; @@ -55,15 +65,17 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; + isSingleEnvironmentMode?: boolean; }; export const MongoDBDatabaseInputForm = ({ onCompleted, onCancel, - environment, + environments, secretPath, - projectSlug + projectSlug, + isSingleEnvironmentMode }: Props) => { const { control, @@ -76,7 +88,8 @@ export const MongoDBDatabaseInputForm = ({ defaultValues: { provider: { roles: [{ roleName: "readWrite" }] - } + }, + environment: isSingleEnvironmentMode ? environments[0] : undefined } }); @@ -87,9 +100,15 @@ export const MongoDBDatabaseInputForm = ({ const createDynamicSecret = useCreateDynamicSecret(); - const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + const handleCreateDynamicSecret = async ({ + name, + maxTTL, + provider, + defaultTTL, + environment + }: TForm) => { // wait till previous request is finished - if (createDynamicSecret.isLoading) return; + if (createDynamicSecret.isPending) return; try { await createDynamicSecret.mutateAsync({ provider: { @@ -105,10 +124,10 @@ export const MongoDBDatabaseInputForm = ({ path: secretPath, defaultTTL, projectSlug, - environmentSlug: environment + environmentSlug: environment.slug }); onCompleted(); - } catch (err) { + } catch { createNotification({ type: "error", text: "Failed to create dynamic secret" @@ -321,6 +340,29 @@ export const MongoDBDatabaseInputForm = ({ )} /> + {!isSingleEnvironmentMode && ( + ( + + option.name} + getOptionValue={(option) => option.slug} + menuPlacement="top" + /> + + )} + /> + )} diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx similarity index 87% rename from frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx rename to frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx index dae0cd478..b593730cf 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx @@ -1,5 +1,4 @@ import { Controller, useForm } from "react-hook-form"; -import Link from "next/link"; import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -8,9 +7,18 @@ import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, FormLabel, IconButton, Input, SecretInput } from "@app/components/v2"; +import { + Button, + FilterableSelect, + FormControl, + FormLabel, + IconButton, + Input, + SecretInput +} from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; const formSchema = z.object({ provider: z.object({ @@ -51,7 +59,8 @@ const formSchema = z.object({ if (valMs > 24 * 60 * 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), - name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environment: z.object({ name: z.string(), slug: z.string() }) }); type TForm = z.infer; @@ -60,15 +69,17 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; + isSingleEnvironmentMode?: boolean; }; export const RabbitMqInputForm = ({ onCompleted, onCancel, - environment, + environments, secretPath, - projectSlug + projectSlug, + isSingleEnvironmentMode }: Props) => { const { control, @@ -90,15 +101,22 @@ export const RabbitMqInputForm = ({ } }, tags: [] - } + }, + environment: isSingleEnvironmentMode ? environments[0] : undefined } }); const createDynamicSecret = useCreateDynamicSecret(); - const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + const handleCreateDynamicSecret = async ({ + name, + maxTTL, + provider, + defaultTTL, + environment + }: TForm) => { // wait till previous request is finished - if (createDynamicSecret.isLoading) return; + if (createDynamicSecret.isPending) return; try { await createDynamicSecret.mutateAsync({ provider: { type: DynamicSecretProviders.RabbitMq, inputs: provider }, @@ -107,10 +125,10 @@ export const RabbitMqInputForm = ({ path: secretPath, defaultTTL, projectSlug, - environmentSlug: environment + environmentSlug: environment.slug }); onCompleted(); - } catch (err) { + } catch { createNotification({ type: "error", text: "Failed to create dynamic secret" @@ -318,13 +336,15 @@ export const RabbitMqInputForm = ({

There is a wide range of in-built roles in RabbitMQ. Some include, management, policymaker, monitoring, administrator.
- - - - Read more about management tags here - - - + + + Read more about management tags here + + .

@@ -404,6 +424,29 @@ export const RabbitMqInputForm = ({ )} /> + {!isSingleEnvironmentMode && ( + ( + + option.name} + getOptionValue={(option) => option.slug} + menuPlacement="top" + /> + + )} + /> + )} diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx similarity index 87% rename from frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx rename to frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx index 2bb6ba0f0..cec85381e 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx @@ -11,6 +11,7 @@ import { AccordionItem, AccordionTrigger, Button, + FilterableSelect, FormControl, Input, SecretInput, @@ -18,6 +19,7 @@ import { } from "@app/components/v2"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; const formSchema = z.object({ provider: z.object({ @@ -50,7 +52,8 @@ const formSchema = z.object({ if (valMs > 24 * 60 * 60 * 1000) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); }), - name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environment: z.object({ name: z.string(), slug: z.string() }) }); type TForm = z.infer; @@ -59,15 +62,17 @@ type Props = { onCancel: () => void; secretPath: string; projectSlug: string; - environment: string; + environments: WorkspaceEnv[]; + isSingleEnvironmentMode?: boolean; }; export const RedisInputForm = ({ onCompleted, onCancel, - environment, + environments, secretPath, - projectSlug + projectSlug, + isSingleEnvironmentMode }: Props) => { const { control, @@ -81,15 +86,22 @@ export const RedisInputForm = ({ port: 6379, creationStatement: "ACL SETUSER {{username}} on >{{password}} ~* &* +@all", revocationStatement: "ACL DELUSER {{username}}" - } + }, + environment: isSingleEnvironmentMode ? environments[0] : undefined } }); const createDynamicSecret = useCreateDynamicSecret(); - const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + const handleCreateDynamicSecret = async ({ + name, + maxTTL, + provider, + defaultTTL, + environment + }: TForm) => { // wait till previous request is finished - if (createDynamicSecret.isLoading) return; + if (createDynamicSecret.isPending) return; try { await createDynamicSecret.mutateAsync({ provider: { type: DynamicSecretProviders.Redis, inputs: provider }, @@ -98,10 +110,10 @@ export const RedisInputForm = ({ path: secretPath, defaultTTL, projectSlug, - environmentSlug: environment + environmentSlug: environment.slug }); onCompleted(); - } catch (err) { + } catch { createNotification({ type: "error", text: "Failed to create dynamic secret" @@ -313,6 +325,29 @@ export const RedisInputForm = ({ + {!isSingleEnvironmentMode && ( + ( + + option.name} + getOptionValue={(option) => option.slug} + menuPlacement="top" + /> + + )} + /> + )} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx new file mode 100644 index 000000000..e1ebebc6f --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx @@ -0,0 +1,344 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import ms from "ms"; +import { z } from "zod"; + +import { TtlFormLabel } from "@app/components/features"; +import { createNotification } from "@app/components/notifications"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + Button, + FilterableSelect, + FormControl, + Input, + TextArea +} from "@app/components/v2"; +import { useCreateDynamicSecret } from "@app/hooks/api"; +import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; + +const formSchema = z.object({ + provider: z.object({ + host: z.string().toLowerCase().min(1), + port: z.coerce.number(), + database: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + creationStatement: z.string().min(1), + revocationStatement: z.string().min(1) + }), + defaultTTL: z.string().superRefine((val, ctx) => { + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + maxTTL: z + .string() + .optional() + .superRefine((val, ctx) => { + if (!val) return; + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environment: z.object({ name: z.string(), slug: z.string() }) +}); +type TForm = z.infer; + +type Props = { + onCompleted: () => void; + onCancel: () => void; + secretPath: string; + projectSlug: string; + environments: WorkspaceEnv[]; + isSingleEnvironmentMode?: boolean; +}; + +export const SapAseInputForm = ({ + onCompleted, + onCancel, + environments, + secretPath, + projectSlug, + isSingleEnvironmentMode +}: Props) => { + const { + control, + formState: { isSubmitting }, + handleSubmit + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + provider: { + database: "master", + port: 5000, + creationStatement: `sp_addlogin '{{username}}', '{{password}}'; +sp_adduser '{{username}}', '{{username}}', null; +sp_role 'grant', 'mon_role', '{{username}}';`, + revocationStatement: `sp_dropuser '{{username}}'; +sp_droplogin '{{username}}';` + }, + environment: isSingleEnvironmentMode ? environments[0] : undefined + } + }); + + const createDynamicSecret = useCreateDynamicSecret(); + + const handleCreateDynamicSecret = async ({ + name, + maxTTL, + provider, + defaultTTL, + environment + }: TForm) => { + // wait till previous request is finished + if (createDynamicSecret.isPending) return; + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.SapAse, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); + onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); + } + }; + + return ( +

+
+
+
+
+ ( + + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+
+
+ Configuration +
+
+
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+
+ ( + + + + )} + /> + ( + + + + )} + /> +
+
+ + + Modify SQL Statements + + ( + +